Skip to content

Load translations for the OS language, not only the languages Chromium ships - #632

Merged
ryanwelcher merged 4 commits into
trunkfrom
fix/584-os-language-catalogs
Oct 5, 2026
Merged

ryanwelcher merged 4 commits into
trunkfrom
fix/584-os-language-catalogs

Conversation

@ryanwelcher

Copy link
Copy Markdown
Collaborator

Why

The app picked its language from app.getLocale(), which is Chromium's UI language, folded into the 55 locales Chromium ships. So a Mac set to Spanish (Mexico) reported es-419 and never loaded es-mx.json, and Galician, Basque or Welsh never asked for a catalog at all. 53 of translate.wordpress.org's 202 locales were reachable. #584 has the details.

What changes

  • The i18n:locale handler walks app.getPreferredSystemLanguages() in order and loads the first language that has a catalog. app.getLocale() is the last fallback. A --lang switch replaces the OS list and is used as given, so --lang=es-MX is not folded either.
  • Each locale tries its exact tag, then language and region, then (for a tag with a script) the script's usual region, then the bare language. The script step is there because macOS adds the user's region: Simplified Chinese in the US is zh-Hans-US, and there is no bare zh catalog.
  • An English language with no catalog of its own ends the walk, so someone with English before German keeps English.
  • Slugs are matched to OS tags with Intl.getCanonicalLocales, which covers the 32 three-letter slugs (bel is be) and Filipino (tl is fil). Valencian (ca-val) is the one entry kept by hand. ELECTRON_LOCALES, its test and GLOTPRESS_SLUGS are gone.
  • The download script's "cannot select" note now lists only pirate and art-xemoji.

Not in this PR: a language picker (out of scope in #584), and the catalogs themselves, which ship with the version-bump PR.

How to test this

Platforms: macOS or Windows, from source on the current head. A Buildkite artifact won't show anything, because trunk has no catalogs committed in src/languages/.

Starting state: a checkout of this branch with dependencies installed. From the repository root, run npm run i18n:download to fetch the catalogs.

es and es-mx both translate the create-site button as Crear sitio, but they word the dialog differently, so the dialog shows which catalog loaded.

  1. From the repository root, run npm run start -- --lang=es-MX. Click Crear sitio. The dialog reads Aún no se ha seleccionado una carpeta. (from es-mx). Only about 15 strings are translated so far, so the rest stays English.
  2. Quit, then run npm run start -- --lang=gl. Click Crear sitio. The dialog reads Aínda non se seleccionou ningún cartafol. On trunk this is all English, because Chromium has no Galician and reports en-US.
  3. Quit, then run npm run start -- --lang=de. The button reads Website erstellen.
  4. Set the OS language to Spanish (Mexico) and put it first in the list: on macOS in System Settings → General → Language & Region, on Windows in Settings → Time & language → Language. Run npm run start with no --lang. Click Crear sitio. The dialog reads Aún no se ha seleccionado una carpeta.
  5. Put English back first in the OS list, with Spanish (Mexico) second, and run npm run start. The button reads Create site.

What must not have happened:

  • Step 1 or step 4 must not show Aún no has seleccionado ninguna carpeta. That's the es catalog, which is what trunk loads, because Chromium folds es-MX to es-419.
  • Step 5 must not show Spanish: English first means English, even when a later language has a catalog.
  • tests/unit/ipc-wiring.test.cjs "i18n:locale asks i18n.cjs for the catalog of the OS languages…" and the es-MX, zh-Hans-US and Valencian cases in tests/unit/i18n.test.cjs fail on trunk's code.

Risks and limitations

  • English variants with no catalog of their own (en-AU, en-NZ, en-IE) now get US English. Chromium used to fold them into en-GB, so they loaded en-gb.json. This only matters once en-gb passes the 80% cut-off (it's at 6%); they get their own catalogs when those ship.
  • Step 4 is the issue's "done when" check, and I haven't run it with the OS language changed. I checked steps 1 to 3 on macOS, and the OS list read en-CA there.
  • Review: 2 to fix here, both fixed. 1 follow-up, listed above.

Related

Fixes #584. Part of #540. Unblocks #623.


Design decisions and alternatives considered
  • The catalog directory is the list of slugs. Matching be-BY to bel.json needs the slugs, so the lookup reads the directory and maps each file to its tag. File names come only from readdir, so no OS or --lang input reaches a path.
  • The English stop. Without it, an English-first user with German second would suddenly get German. That's a bigger surprise than the en-AU change above.
  • "Selectable" means the canonical tag is a language with an optional script and region. I first tried Intl.DisplayNames, but Electron's ICU has no names for many real languages (kab, fur, oci), so it can't tell a real language from pirate.
  • bcc and bal both canonicalize to bal. It's the only collision among the 202 slugs. The slug that is its own tag wins.
Review outcome (required, see AGENTS.md)

2 [fix here] · 1 [follow-up]. Both fixed in eb3f70f; the follow-up is the en-AU note under Risks.

  • 🟡 src/i18n.cjs catalogCandidates: zh-Hans-US never reached zh-cn, and English second then stopped the walk. That's a regression from trunk, where Chromium folds it to zh-CN. Fixed by trying the script's usual region (Intl.Locale(…).maximize()), with tests for zh-Hans-US and zh-Hant-US.

  • 🔵 tests/unit/i18n.test.cjs: the bal/bcc tie test depended on readdir order. It now runs on catalogsFromNames with both orders. Also added a test for the unreadable-directory log.

  • 🔵 [follow-up] English variants without a catalog now get US English. Left as is; see Risks.

  • Review: completed. Separate agent context, fresh. Reviewed head b9df719, base d1616d2. Lint clean, npm test 1951/1951, Electron unit run clean. Checked: Intl on Electron 43 (Node 24.20, ICU 78.2), odd and empty OS lists, path safety, reading from app.asar, removed exports, e2e locale pinning.

  • Since review: b9df719 → eb3f70f (the two fixes, plus the README sentence). The author session checked it, not a fresh context. Lint clean, npm test 1953/1953, Electron 1954 passed.

  • CodeRabbit: not requested yet.

Implementation notes
  • resolveCatalog(locales, dir, log) now takes a list and returns { locale, messages } or null. The handler returns { locale, data } as before, and locale is the OS tag that matched, which ends up in <html lang>.
  • All 202 slugs were checked on Electron 43.6.0's ICU. 34 map to a different tag (the 32 three-letter ones, plus tl and ca-val). Only pirate and art-xemoji can't be selected.
Screenshots or recording

Nothing new on screen: the same strings load for more languages.

🤖 Generated with Claude Code

…m ships

app.getLocale() is Chromium's UI language, folded into the 55 locales
Chromium ships, so es-mx.json and gl.json were shipped and never loaded.
The i18n:locale handler now walks app.getPreferredSystemLanguages() and
loads the first language with a catalog, falling back to getLocale().
A --lang switch replaces the OS list, unfolded.

Slugs are matched to OS tags through Intl.getCanonicalLocales, which
maps the 32 three-letter slugs and Filipino; Valencian is the one entry
kept by hand. ELECTRON_LOCALES and its test go away, and the download
script's note now lists only the slugs no OS reports.

Fixes #584.
Simplified Chinese in the US is zh-Hans-US, and translate.wordpress.org
has no bare zh, so the walk fell through to English where trunk loaded
zh-cn. A tag with a script now tries its script's usual region before
the bare language.

The bal/bcc tie test now runs on a fixed list of names in both orders,
since readdir's order depends on the file system.
@coderabbitai

coderabbitai Bot commented Oct 5, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration
  • Configuration used: Repository: WordPress/contributor-toolkit/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 4cee7368-1d01-4299-8983-0c629c520c98

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository: WordPress/contributor-toolkit/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: be113505-7204-4c2b-a2b6-9cd1c78fc9a1
📥 Commits

Reviewing files that changed from the base of the PR and between d1616d2 and eb3f70f.

📒 Files selected for processing (8)
  • CONTRIBUTING.md
  • scripts/download-translations.cjs
  • src/i18n.cjs
  • src/languages/README.md
  • src/main.js
  • tests/unit/download-translations.test.cjs
  • tests/unit/i18n.test.cjs
  • tests/unit/ipc-wiring.test.cjs

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

Locale resolution now checks operating-system language preferences in order and loads the first matching catalog. Catalog lookup supports canonical locale tags and fallback candidates. The --lang option remains a direct locale override. If no catalog is found, the handler returns English with no catalog data. The download script now marks locale selectability using slugTag and describes locales that no operating-system language selects.

Objective Addressed Explanation
Load the first available catalog from preferred OS languages, then fall back to Electron’s locale and English [#584] ✅
Map OS locale tags to catalog slugs, including three-letter slugs and Valencian [#584] ✅
Preserve --lang behavior and update shipped-but-unselectable locale reporting [#584] ✅

Priority: ➖ Normal

Severity of issue fixed: Medium

Merge Risk: ⚪ Minimal · up to eb3f7

The change makes the app pick translations from the OS language list in order, with --lang still overriding it. No merge-blocking risk was identified. The one gap is that a real Spanish (Mexico) system has not been tested.

Security Architecture Review

Security architecture risk: 🔵 Low · up to eb3f7

More bundled translations become reachable, but catalog reads remain owned by the main process and the renderer gains no new selection or filesystem authority. No introduced security-boundary bypass was identified. Catalogs supplied with a later release were not assessed.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — The changed exposure is which application catalogs can be selected and returned for localization. Controlling an OS preference or launch argument does not, through the inspected flow, grant renderer-controlled paths or additional filesystem authority.

Trust Boundaries and Controls

  • observed — The renderer invokes a narrow, no-argument locale bridge. Main chooses preferences, owns catalog reads, and returns localization data. The PR changes matching behavior without moving catalog-selection or filesystem authority into the renderer.

Resilience and Maintainability Implications

  • inferred — Repeated or concurrent resolution calls use independent indexes and only read catalog assets. A file disappearing between indexing and reading follows the read-failure fallback, without leaving persistent configuration or ownership changes to recover.
🚥 Pre-merge checks | ✅ 1
✅ Passed checks (1 passed)
Check name Status Explanation
Description check ✅ Passed The description covers the reason for the change, the approach, test steps, expected outcomes, risks, related issues, design decisions, review outcome, and implementation notes. It also states which p…
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@ryanwelcher

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Oct 5, 2026 •

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@ryanwelcher
ryanwelcher merged commit 8dbed36 into trunk Oct 5, 2026
8 checks passed
@ryanwelcher
ryanwelcher deleted the fix/584-os-language-catalogs branch October 5, 2026 16:26
ryanwelcher added a commit that referenced this pull request Oct 5, 2026
…og titles (#634)

## Why

Several modules build sentences in main and send them to the window, and
main has its own menu and native dialogs. Main had no catalog, so a
translated window would still show those in English. This is the first
batch of #622, and #625, #627, #628 and #629 need it.

## What changes

- Main resolves the locale once (`localeReply()` in `src/main.js`) and
applies it with the same `applyLocale()` the renderer uses, before it
builds the menu and the window. The `i18n:locale` handler returns that
same reply, so main and the window cannot disagree, and the en-XA
pseudo-locale now covers main too.
- Wrapped: "Open App Log" and "Show Logs Folder" in the Help menu, and
the titles and filter names of the save-patch, choose-patch and
choose-application dialogs. 10 strings.
- Electron's role menus (File, Edit, View, Window) are deliberately left
alone, as the issue says.

## How to test this

Covered by a journey, red without this change: from the repository root,
`npx playwright test --project=journeys tests/e2e/journeys/i18n.spec.js
-g "Help menu"`. It reads the Help menu labels and the title and filters
the patch file dialog was opened with, under `--lang=en-XA`.

What the journey cannot reach is the real OS dialog. Platform: Windows
for the dialogs (macOS does not show an open or save panel's title),
either for the menu. Build: the current head.

**Starting state:** the app launched from the repository root with `npx
electron . --lang=en-XA`, with one site that has an edited file.

1. Open the **Help** menu. Expected: both "Open App Log" and "Show Logs
Folder" are accented and in brackets. "Toggle Developer Tools" stays in
English (a role menu).
2. Click **Review & submit changes**, then **Save**. Expected: the save
dialog's title ("Save Diff File") and its "Patch Files" filter are
accented and in brackets.
3. Cancel. In the **Apply a patch or PR** card, open the **Diff** tab
and click **Choose a .diff or .patch file…**. Expected: the dialog title
and both filters are accented and in brackets.
4. Cancel. Open **Site actions → Open in → Other application…**.
Expected: the title and the "Programs" filter are accented and in
brackets.

**What must not have happened:** launching with no `--lang` shows every
one of these in English, and the app opens one window, not two, when
started from a `wpct://ticket/…` link.

## Risks and limitations

- Main now awaits the locale before it creates the window, so startup
gains one catalog read (the same one the window already waited for). A
`wpct://` link that arrives in that gap opens the window itself; startup
then creates a window only if none exists.
- Not tested by a journey: the save dialog and the choose-application
dialog. Only the patch file dialog is checked.
- "Dev server stopped unexpectedly (see Help → Open App Log for
details)" still names the menu entry in English. It belongs in #627,
where it should take the label as `%s`.

## Related

Fixes #623. Part of #622. Follows #632 (#584).

---

<details>
<summary>Design decisions and alternatives considered</summary>

The reply is memoised rather than computed twice so the window and main
read the catalog once and always agree. `applyLocale()` was reused as
is: it has no DOM dependency, and with main's `setLocaleData` and
`addFilter` it installs the pseudo-locale filters for main too. Main's
CommonJS `@wordpress/i18n` builds its default instance on
`@wordpress/hooks`' `defaultHooks`, and there is one copy of
`@wordpress/hooks`, so the filters apply to `menu.js` as well.

</details>

<details>
<summary>Review outcome (required, see AGENTS.md)</summary>

- **Review:** completed. Reviewer: a separate agent context (Claude
Code, read-only) against
`.github/instructions/code-review.instructions.md`; head `08d29ef` /
base `eb3f70f`. `npm run lint` clean, `npm test` 1955 pass / 0 fail,
full journey suite 114 pass. Outcome: 1 [fix here] · 1 [follow-up], both
🔵 low.
- Architecture, `src/main.js` ready path: if the locale load rejected,
startup would stop before the menu and the window. **Not changed:** the
reviewer found no path that rejects (`resolveCatalog` catches its own
read and parse errors), and AGENTS.md asks for no defensive branches
without a realistic path.
- Tests, `tests/unit/ipc-wiring.test.cjs`: the `ready` path runs after
the harness removes its require hook, so a lazy `require` added there
would load real modules. **Fixed** with a note beside the option.
- **Since review:** `08d29ef → 4c34787` checked; the only change is that
comment. Then rebased onto `trunk` after #632's squash merge: head
`a6fb7da` / base `8dbed36`. The diff against the new base is line for
line the reviewed one plus that comment; lint clean, `npm test` 1955
pass / 0 fail, journeys 114 pass on the new head.

</details>

<details>
<summary>Implementation notes</summary>

- `tests/unit/ipc-wiring.test.cjs` gains a `ready` option so one test
can run the ready path. It checks that main applies the locale before
any menu or window exists, that the handler returns the same reply
object, and that the catalog is read once.
- `tests/unit/menu.test.cjs` checks the labels are translated when the
template is built, not at require time.

</details>

<details>
<summary>Screenshots or recording</summary>

Nothing changes in English. In the pseudo-locale the change is the Help
menu labels and the native dialog titles, which the journey asserts.

</details>

🤖 Generated with [Claude Code](https://claude.com/claude-code)
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.

Load translations for the OS language, not only the languages Chromium ships

1 participant