Skip to content

Serve three languages instead of shipping ten and serving one - #119

Merged
openipc-ai merged 3 commits into
masterfrom
three-languages
Aug 25, 2026
Merged

Serve three languages instead of shipping ten and serving one#119
openipc-ai merged 3 commits into
masterfrom
three-languages

Conversation

@openipc-ai

@openipc-ai openipc-ai commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator

The site carried ten locale files and rendered every request in English.
Multilang's before_action :set_locale was commented out at multilang.rb:21
and the switcher was commented out of the layout, so ?locale=ru did nothing
and no visitor ever saw a translated page.

That was deliberate, not rot — the project did not have the people to keep ten
translations in step with the English copy, and a stale translation of the
flashing instructions is worse than an English one. The maintainers' decision
now is three: English as the source, Russian and Chinese for the two
largest communities.

The change

  • available_locales and config/i18n-tasks.yml both list en, ru, zh.
  • The 35 locale files for de, es, fa, fr, it, pl, pt are removed.
  • ru and zh were four keys short of English each — written, so both are complete.

i18n-tasks missing goes from 37 to 0; unused from 307 to 103.

Seven things that had to be fixed before switching it on

Every one was harmless only because the switcher was commented out.

Browser detection ignored the visitor's first choice. browser_locale
scanned /[a-z]{2}(?=;)/, which only matches a tag followed by a q-value — and
the first entry of an Accept-Language header does not carry one. ru,en;q=0.9
answered en for a Russian speaker.

q=0 was treated as a preference. It means "not acceptable" (RFC 9110
12.4.2), so ru;q=0,de;q=0.9 answered ru — a language the visitor had
explicitly ruled out — because de is not served.

;q= was matched as a literal. The grammar allows whitespace around the
separator and either case of the name, so en; q=0.1,ru;q=0.9 ranked English at
the default 1 and picked it.

The switcher icon bypassed the asset pipeline. A hardcoded
<img src="/assets/translate.svg">, against a production config with
assets.compile = false and fingerprinted assets — a 404 on every page,
since the switcher is in the layout. IconsHelper#icon_language had rendered the
same file correctly all along.

The switcher discarded the query string. Every link was a bare ?locale=xx,
which replaces rather than edits. Switching language on a paginated list lost the
page — and on a Camera#permalink URL it lost the whole camera configuration.

LOCALES and available_locales disagreed. LOCALES listed Japanese: no
locale file, never available. The switcher would have offered a language the app
cannot render. A test now holds the two together.

locales_for_select was broken. It looked up t("locales.#{l}") and no
locale file has locales.* keys, so every entry would have been a
translation-missing span.

The first two switcher bugs came from locale_switcher building HTML by hand
inside a controller concern, where image_tag and the request are out of reach.
It is a partial now. While rewriting it: aria-labelledby said
dropdownLanguage while the button was id="dropsownLanguage".

And one design change: set_locale is an around_action wrapping
I18n.with_locale rather than a before_action assigning I18n.locale, which
is per-thread with nothing resetting it at request end.

Three older i18n gaps, closed

  • pages.qr_code.title was a typo, not a missing translation — the key is
    pages.qr_code_generator.title. That page's browser tab has been reading
    translation missing: en.pages.qr_code.title in every language.
  • site.snapshot.view_heif existed only as an inline English default, so it
    read as English on a Russian page and i18n-tasks could not see it.
  • Russian plurals. Adding few/many to the file alone does nothing —
    there is no rails-i18n, so I18n's default pluralizer knows two forms and
    every count from 2 up kept taking other. lib/locale/plurals.rb carries the
    CLDR rule and the initializer mixes in I18n::Backend::Pluralization:
  1 ошибка    2 ошибки    5 ошибок   11 ошибок
 21 ошибка   22 ошибки   25 ошибок  101 ошибка

Verification

bin/rails test231 runs, 873 assertions, 0 failures (208 on master).

Every fix checked by reverting it: old scan 2, set_locale off 4, Japanese back
2, q=0 kept 1, literal ;q= 2, hardcoded asset + bare query string 2, the
title typo 1, no pluralization include 1, Russian key removed 1.

9 rubocop offences across the touched files, against 14 on master — the
hand-built HTML took several with it. i18n-tasks missing is 0.

Read in all three:

en  OpenIPC is an alternative open firmware for your IP camera.
ru  OpenIPC — альтернативная открытая прошивка для IP-камер
zh  OpenIPC是一款专为网络摄像机设计的第三方开源固件。

🤖 Generated with Claude Code

https://claude.ai/code/session_01YFg9PRy2T6cr983S8gran5

The site carried ten locale files and rendered every request in English.
Multilang's `before_action :set_locale` was commented out and the
locale switcher was commented out of the layout, so `?locale=ru` did
nothing and no visitor ever saw a translated page.

That was deliberate, not rot: the project did not have the people to
keep ten translations in step with the English copy, and a stale
translation of the flashing instructions is worse than an English one.
The maintainers' decision now is three -- English as the source,
Russian and Chinese for the two largest communities. Translating got
cheap; keeping translations honest through every later edit did not,
which is why the answer is three and not ten again.

  - available_locales and i18n-tasks both list en, ru, zh.
  - The 35 locale files for de, es, fa, fr, it, pl, pt are removed.
  - ru and zh were four keys short of English each; those are written.
    i18n-tasks missing goes 37 -> 4 and unused 307 -> 103. The four that
    remain are older: two keys referenced from code that exist in no
    locale at all, and Russian plural forms for errors.messages.not_saved.

Three defects had to be fixed before switching it on, all of them
invisible while it was off.

`browser_locale` scanned `/[a-z]{2}(?=;)/`, which only matches a tag
followed by a q-value. The first entry of an Accept-Language header
does not carry one, so the visitor's top preference was the single
entry the scan could never see: `ru,en;q=0.9` answered `en`.

LOCALES fed the switcher while available_locales fed everything else,
and they disagreed -- LOCALES listed Japanese, which has no locale file
and was never available. The switcher would have offered a language the
app cannot render. A test now holds the two together.

`locales_for_select` looked up `t("locales.#{l}")`, and no locale file
has `locales.*` keys, so every entry would have come back as a
translation-missing span. It uses LOCALES, like the switcher always has.

set_locale is an around_action wrapping I18n.with_locale rather than a
before_action assigning I18n.locale. I18n.locale is per-thread and
nothing resets it at the end of a request.

Three tests elsewhere used I18n.with_locale to reach Russian, because
the parameter did nothing. They use ?locale=ru now -- the path a
visitor actually takes.

221 runs, 828 assertions. Restoring the old scan fails 2; switching
set_locale back off fails 4; putting Japanese back fails 2. 10 rubocop
offences across the touched files, against 15 on master.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YFg9PRy2T6cr983S8gran5
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Enable maintained three-language localization

✨ Enhancement 🐞 Bug fix 🧪 Tests ⚙️ Configuration changes 🕐 20-40 Minutes

Grey Divider

AI Description

• Enables request-scoped English, Russian, and Chinese locale selection.
• Fixes browser preference parsing, invalid locale handling, and switcher labels.
• Removes unmaintained translations and adds end-to-end localization coverage.
Diagram

sequenceDiagram
  actor Browser
  participant Controller
  participant Multilang
  participant Session
  participant I18n
  participant Layout
  Browser->>Controller: Request and preferences
  Controller->>Multilang: Run around action
  Multilang->>Session: Read saved locale
  alt Valid locale parameter
    Multilang->>Session: Save selection
  else First visit
    Multilang->>Multilang: Rank browser languages
  end
  Multilang->>I18n: Scope request locale
  I18n->>Controller: Execute action
  Controller->>Layout: Render response
  Layout-->>Browser: Translated page and switcher
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Central locale registry
  • ➕ Makes locale codes and display names a single source of truth.
  • ➕ Eliminates reliance on a consistency test between configuration and switcher metadata.
  • ➖ Requires a shared configuration object available safely during Rails initialization.
  • ➖ Adds indirection for a deliberately small, stable three-locale set.
2. Use a locale negotiation library
  • ➕ Provides broader RFC-compliant Accept-Language parsing and edge-case handling.
  • ➕ Reduces custom ranking logic.
  • ➖ Adds a dependency for a narrow two-letter locale requirement.
  • ➖ May be disproportionate to the supported language set and current tests.

Recommendation: Keep the PR's focused implementation: request-scoped I18n.with_locale, explicit supported locales, and integration tests are appropriate for three languages. A central locale registry is the best future refinement if locale metadata expands; a negotiation dependency is unnecessary for the current scope.

Files changed (10) +198 / -43

Enhancement (1) +1 / -1
application.html.erbExpose the locale switcher in the navigation +1/-1

Expose the locale switcher in the navigation

• Restores the language switcher so visitors can select one of the supported locales from the application layout.

app/views/layouts/application.html.erb

Bug fix (1) +56 / -31
multilang.rbActivate safe request-scoped locale negotiation +56/-31

Activate safe request-scoped locale negotiation

• Enables locale selection as an around action using 'I18n.with_locale', preventing thread-local state from leaking after requests. Restricts the switcher to English, Russian, and Chinese, ranks Accept-Language entries correctly, validates session and parameter values, and uses native locale names instead of missing translation keys.

app/controllers/concerns/multilang.rb

Tests (2) +121 / -8
multilang_test.rbCover locale negotiation and request isolation +113/-0

Cover locale negotiation and request isolation

• Adds integration coverage for the supported locale set, switcher visibility, Accept-Language ranking, fallback behavior, parameter validation, session persistence, HTML language metadata, and post-request I18n reset.

test/controllers/multilang_test.rb

socs_controller_test.rbExercise translations through visitor locale parameters +8/-8

Exercise translations through visitor locale parameters

• Extends the request helper with a locale parameter and changes Russian rendering assertions to use '?locale=ru', validating the same path visitors use instead of directly scoping I18n in tests.

test/controllers/socs_controller_test.rb

Documentation (4) +8 / -0
pages.ru.ymlComplete missing Russian page translations +3/-0

Complete missing Russian page translations

• Adds Russian labels for gaming solutions and the QR-code privacy notice, closing gaps against the English page locale.

config/locales/pages.ru.yml

pages.zh.ymlComplete missing Chinese page translations +3/-0

Complete missing Chinese page translations

• Adds Chinese labels for gaming solutions and the QR-code privacy notice, closing gaps against the English page locale.

config/locales/pages.zh.yml

ru.ymlAdd Russian NAND flashing warning +1/-0

Add Russian NAND flashing warning

• Adds the Russian caveat explaining that NAND images omit OOB/ECC data and factory bad-block markers and should not be written with a programmer.

config/locales/ru.yml

zh.ymlAdd Chinese NAND flashing warning +1/-0

Add Chinese NAND flashing warning

• Adds the Chinese caveat explaining that NAND images omit OOB/ECC data and factory bad-block markers and should not be written with a programmer.

config/locales/zh.yml

Other (2) +12 / -3
i18n-tasks.ymlAudit only maintained locales +1/-1

Audit only maintained locales

• Limits i18n-tasks analysis to English, Russian, and Chinese so translation reports match the languages the site serves.

config/i18n-tasks.yml

locale.rbRestrict Rails I18n to three supported locales +11/-2

Restrict Rails I18n to three supported locales

• Sets 'I18n.available_locales' to English, Russian, and Chinese, aligning runtime behavior with the maintained translation set.

config/initializers/locale.rb

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Aug 25, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. locale_switcher bypasses asset pipeline ✓ Resolved 📘 Rule violation ☼ Reliability
Description
Enabling locale_switcher makes its hardcoded /assets/translate.svg URL production-facing, but
production only serves precompiled fingerprinted assets and disables runtime compilation. The
language icon can therefore return 404 after deployment.
Code

app/views/layouts/application.html.erb[50]

+      <%= locale_switcher %>
Evidence
Rule 1 requires production-facing references to use an asset-pipeline mechanism. The PR exposes
locale_switcher from the layout, while the switcher hardcodes /assets/translate.svg; the
existing icon_language helper demonstrates the configured pipeline-safe reference.

CLAUDE.md: Route Production Asset References Through the Asset Pipeline: CLAUDE.md: Route Production Asset References Through the Asset Pipeline: CLAUDE.md: Route Production Asset References Through the Asset Pipeline: CLAUDE.md: Route Production Asset References Through the Asset Pipeline: CLAUDE.md: Route Production Asset References Through the Asset Pipeline: CLAUDE.md: Route Production Asset References Through the Asset Pipeline: CLAUDE.md: Route Production Asset References Through the Asset Pipeline: CLAUDE.md: Route Production Asset References Through the Asset Pipeline
app/views/layouts/application.html.erb[50-50]
app/controllers/concerns/multilang.rb[90-106]
app/helpers/icons_helper.rb[14-16]
config/environments/production.rb[35-38]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The newly enabled locale switcher emits a hardcoded `/assets/translate.svg` URL, bypassing the Rails asset pipeline and risking a production 404.
## Issue Context
`translate.svg` is stored under `app/assets/images`, production disables runtime asset compilation, and `IconsHelper#icon_language` already renders this image through `image_tag`.
## Fix Focus Areas
- app/views/layouts/application.html.erb[50-50]
- app/controllers/concerns/multilang.rb[90-106]
- app/helpers/icons_helper.rb[14-16]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. Spaced q-values rank incorrectly ✓ Resolved 🐞 Bug ≡ Correctness
Description
rank recognizes only the exact lowercase substring ;q=, so valid headers with optional
whitespace or case variants—such as en; q=0.1,ru;q=0.9—treat English as quality 1 and select it
instead of Russian. That incorrect first-request choice is then persisted in session[:locale].
Code

app/controllers/concerns/multilang.rb[R50-51]

+    tag, quality = part.split(';q=')
+    [tag.to_s.strip.downcase[0, 2].to_s, -(quality || '1').to_f, index]
Evidence
The parser splits only on exact ;q= and defaults an unrecognized quality to 1; sorting then puts
that entry first, browser_locale selects it, and set_locale stores it in the session. Since all
example tags are configured locales, the incorrect selection is directly reachable.

app/controllers/concerns/multilang.rb[31-39]
app/controllers/concerns/multilang.rb[49-51]
app/controllers/concerns/multilang.rb[64-75]
config/initializers/locale.rb[18-18]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The Accept-Language parser only recognizes the exact `;q=` spelling. Parse parameter separators with optional whitespace and recognize `q` case-insensitively so valid quality values affect ranking.
## Issue Context
For example, `en; q=0.1,ru;q=0.9` currently assigns English the default quality of 1 and persists the wrong locale in the session.
## Fix Focus Areas
- app/controllers/concerns/multilang.rb[49-51]
- test/controllers/multilang_test.rb[47-57]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Rejected locale still selected ✓ Resolved 🐞 Bug ≡ Correctness
Description
rank keeps entries with quality q=0, so browser_locale can select a language the client
explicitly marked unacceptable when no higher-ranked supported language exists. For example,
Accept-Language: ru;q=0,de;q=0.9 persists Russian instead of falling back to English.
Code

app/controllers/concerns/multilang.rb[R50-51]

+    tag, quality = part.split(';q=')
+    [tag.to_s.strip.downcase[0, 2].to_s, -(quality || '1').to_f, index]
Evidence
rank converts q=0 to -0.0, while accepted_languages returns every ranked entry without
filtering; browser_locale then accepts the first returned supported tag, and set_locale persists
it because it is available.

app/controllers/concerns/multilang.rb[31-51]
app/controllers/concerns/multilang.rb[64-75]
config/initializers/locale.rb[14-17]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The Accept-Language parser retains `q=0` entries, allowing an explicitly unacceptable supported locale to be selected and persisted.
## Issue Context
Parse and validate quality values, and exclude entries whose quality is zero before `browser_locale` searches for a supported locale. Preserve descending quality and original-order tie-breaking.
## Fix Focus Areas
- app/controllers/concerns/multilang.rb[35-51]
- test/controllers/multilang_test.rb[36-69]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. Switcher discards query state ✓ Resolved 🐞 Bug ≡ Correctness
Description
The globally enabled locale switcher links only to ?locale=..., which replaces the current query
string and drops unrelated page state. On /snapshots?page=2, changing language removes page=2
and sends the visitor back to the first page.
Code

app/views/layouts/application.html.erb[50]

+      <%= locale_switcher %>
Evidence
The shared layout now renders the switcher; its links contain only the locale query parameter, while
SnapshotsController#index directly uses the dropped page parameter for pagination.

app/views/layouts/application.html.erb[47-50]
app/controllers/concerns/multilang.rb[90-106]
app/controllers/snapshots_controller.rb[8-16]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Enabling the locale switcher globally exposes links that replace the complete query string, dropping pagination and other request state.
## Issue Context
Generate each switcher URL from the current path and existing query parameters, replacing only the `locale` value. Ensure unsafe or routing-only parameters are not introduced.
## Fix Focus Areas
- app/views/layouts/application.html.erb[50-50]
- app/controllers/concerns/multilang.rb[90-106]
- test/controllers/multilang_test.rb[29-34]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Tip of the day
💡 Did you know, you can hide the parts of a finding you never read, like the evidence or the agent prompt

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread app/views/layouts/application.html.erb Outdated
Comment thread app/controllers/concerns/multilang.rb Outdated
Comment thread app/views/layouts/application.html.erb Outdated
These were flagged and not fixed in the commit before this one. With
three languages actually being served they stop being bookkeeping.

`pages.qr_code.title` was never a missing translation. The controller
asks for it; the key is `pages.qr_code_generator.title`, and has been
all along. The QR generator's browser tab has been reading "translation
missing: en.pages.qr_code.title" in every language, English included.

`site.snapshot.view_heif` existed only as an inline English default in
the view, so it rendered as English on a Russian page and i18n-tasks
could not tell it was untranslated. It is a key in all three locales now
and the default is gone.

Russian `errors.messages.not_saved` had `one` and `other`. Russian has
four forms, and adding `few` and `many` to the file changes nothing on
its own: I18n's default pluralizer knows two, so every count from 2 up
kept taking `other` -- "5 ошибки" where Russian wants "5 ошибок", "21
ошибки" where it wants "21 ошибка". lib/locale/plurals.rb carries the
CLDR rule and config/initializers/locale.rb mixes in
I18n::Backend::Pluralization, without which the rule is read and
ignored. rails-i18n would be the answer for ten locales; for three, one
of which needs no rule, it is more than the problem is worth.

Checked across the boundaries the rule turns on:

    1 ошибка    2 ошибки    5 ошибок   11 ошибок
   21 ошибка   22 ошибки   25 ошибок  101 ошибка

`i18n-tasks missing` is 0, from 37 on master.

Five tests. One of them passed against the unfixed code on the first
attempt: it looked for /translation missing/ and I18n writes
"Translation missing". Case-insensitive now, and it fails when the
Russian key is removed.

225 runs, 853 assertions. Restoring the typo fails 1, removing the
pluralization include fails 1, removing the Russian key fails 1.
13 rubocop offences across the touched files, against 14 on master.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YFg9PRy2T6cr983S8gran5
@openipc-ai

Copy link
Copy Markdown
Collaborator Author

/review

Comment thread app/controllers/concerns/multilang.rb Outdated
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 7fdbcb4

Qodo's review of the two commits before this. Four findings, all real,
and all of them things that were harmless only while the switcher was
commented out of the layout.

The icon was a hardcoded <img src="/assets/translate.svg">. Production
serves fingerprinted assets with compile off, so that is a 404 -- and
the switcher is in the layout, so it is a 404 on every page of the site.
IconsHelper#icon_language has rendered the same file through image_tag
all along.

Every link was a bare "?locale=xx", which replaces the query string
rather than editing it. Switching language on a paginated list lost the
page, and on a permanent link to a camera configuration -- the thing
three commits of this session were spent making work -- it lost the
configuration.

Both of those come from locale_switcher building HTML by hand in a
controller concern, where image_tag and the request are out of reach.
It is a partial now, which is also where a reviewer would look for it.
While rewriting: aria-labelledby said "dropdownLanguage" and the button
was id="dropsownLanguage", so it labelled nothing.

The Accept-Language parser kept q=0 entries. q=0 means "not acceptable"
(RFC 9110 12.4.2), so `ru;q=0,de;q=0.9` answered `ru` -- a language the
visitor had explicitly ruled out -- because `de` is not served.

And it matched ';q=' as a literal, where the grammar allows whitespace
around the separator and either case of the name. `en; q=0.1,ru;q=0.9`
ranked English at the default 1 and picked it, which is the reverse of
what the header says.

231 runs, 873 assertions. Six new tests, each checked by reverting its
fix: q=0 fails 1, the literal ';q=' fails 2, the hardcoded asset and the
bare query string fail 2. 9 rubocop offences across the touched files
against 14 on master -- the hand-built HTML took several with it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YFg9PRy2T6cr983S8gran5
@openipc-ai
openipc-ai merged commit bd6fb59 into master Aug 25, 2026
2 checks passed
@openipc-ai
openipc-ai deleted the three-languages branch August 25, 2026 13:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant