Files
hmbown--codewhale/docs/LOCALIZATION.md
Hmbown 88167ed7b7 fix(web): correct locale-inverted headings, stale provider list, and dead components
Shortcut facts (this is why the contract test moved):
docs/public-surface-facts.json declared both `Tab` and `Shift+Tab` as
`when: "composer idle"`, and web/lib/public-surface-contract.test.ts pinned
that string into README.md and docs/KEYBINDINGS.md. Both halves are false —
crates/tui/src/tui/ui.rs:6978 gates Tab on `!app.input.is_empty()`, and
ui.rs:6363-6367 gates Shift+Tab only on the modal stack. The matrix now says
"composer empty" / "always (suppressed only under a non-Config modal)" and the
test asserts the corrected wording plus a negative guard so the idle claim
cannot come back. docs/MODES.md:24 already said "when the composer is empty";
it was the matrix and the test that were stale.

/runtime rendered the Chinese-primary H1 for every locale:
web/app/[locale]/runtime/page.tsx:84-88 had two byte-identical ternary
branches, both `Runtime & 集成 Integrations`. That contradicts
web/lib/i18n/dictionaries/types.ts:33-46 ("Never hardcode Han characters at a
call site") and docs/LOCALIZATION.md:80-82 ("no locale renders another
language's script by accident"). Every other page follows the correct pattern
(constitution/page.tsx:60-65). Fixed both branches; same class, smaller blast
radius, in feed/page.tsx:114 ("Section 03 · 动态" hardcoded in the EN branch).

FAQ provider list was 25 hand-maintained names against 40 real routes:
web/lib/facts.generated.ts derives 40 providers from `pub enum ApiProvider`,
and models/page.tsx already renders `facts.providers.length`. The FAQ omitted
15, including xAI, Baidu Qianfan, Meta Model API, and all four Model Studio
routes. Because ja/ko/ru/es/pt-BR/uk/vi/id fall back to English page bodies,
9 of 10 locales saw the stale list. The count is now derived from FACTS and
the enumeration is explicitly a sample ("including …") pointing at /models.

Other corrected claims:
- docs/subagents/page.tsx omitted the `consultant` role. It is one of the eight
  in FLEET_ROLE_SCHEMA_VALUES (crates/tui/src/tools/subagent/mod.rs:376-385),
  advertised to the model, and already in web/lib/content/vocabulary.ts:183-186.
- lib/media-manifest.ts:99-100 promised a recording "from the v0.9.2 release
  candidate" on a live page (/docs/guide via components/session-media.tsx:47).
  v0.9.2 and v0.9.3 both shipped; restated against 0.9.4.
- lib/docs-map.ts:39-40 advertised Nix and Scoop as contents of /install. Both
  are real (docs/INSTALL.md:356, :507) but the site page has neither; the
  description now names what the page actually contains.
- web/README.md:99/101/122/127 — "en / zh, every page is bilingual" (10 locales
  route and non-zh get English bodies), a home-page section list naming four
  sections that do not exist, and "EN ↔ ZH toggle" for what
  components/locale-switcher.tsx:13-14 builds as an N-locale dropdown.
- docs/LOCALIZATION.md stated three different TUI key counts (1248 in the table,
  1,153 at :234). `python3 scripts/check-tui-locale-parity.py` prints 1299 for
  every pack. The zh-Hant row claimed 499/1248 "missing keys fall back to
  English at runtime" — it is 1299/1299, so nothing falls back; the row now says
  so and flags the PARTIAL_PACKS promotion as an open decision. The web
  reference shape said 51/60 keys twenty-three lines above its own correct
  52/62; check:locales prints 52/62.

Deleted (verified zero importers, no barrel, no glob loader, no CI reference):
- components/stat-grid.tsx — pulled from community/page.tsx in c268cc375, which
  added the guard at lib/public-copy.test.ts:141 asserting the page must NOT
  use it. That guard reads page source as a string and is kept.
- components/mermaid-diagram.tsx — its only consumer was removed in 5abe250f3e
  ("Cut: … the Mermaid architecture diagram"). Removed with it: the dead
  `.mermaid-frame` rules in app/globals.css and the `mermaid` production
  dependency, via `npm uninstall mermaid` so package-lock.json stays consistent
  with package.json (CI runs `npm ci`, which hard-fails on drift).

Verified in web/: npm test exit=0 (28 files, 235 tests), check:facts exit=0,
check:locales exit=0, check:docs exit=0, lint exit=0.
2026-08-03 22:48:21 -07:00

16 KiB
Raw Permalink Blame History

Localization Matrix

Canonical tracking document for every locale Codewhale ships, is actively building, is planning, or has explicitly deferred.

Scope note (2026-07-12): this matrix covers three surfaces — the TUI locale packs (crates/tui/locales/), the translated READMEs (repo root), and the website (web/). The three ship on different cadences, so a locale can be shipped on one surface and planned on another; the per-surface tables below are the per-surface truth. The website registry is web/lib/i18n/config.ts (ALL_LOCALES): the locale switcher and route generation both derive from it.

Customer-visible copy also follows the Codewhale voice and terminal charter; commands, key names, and glyphs remain code-owned around localized prose.

Last updated: 2026-08-03 (v0.9.4 website dictionary spine, #4934). Source-of-truth README: README.md (English, post-#3087).

Status legend

Status Meaning
shipped Live on codewhale.net and/or published as a standalone README, or a TUI pack at exact en.json parity
partial Shipped but intentionally incomplete; missing scope falls back to English and the partial status is visible
planned Explicitly prioritized for the next wave
deferred Acknowledged as wanted but not yet scheduled; needs layout QA, bridge support, or community champion

TUI locale packs

The TUI packs under crates/tui/locales/ are the largest translation surface in the repo. en.json is the reference; a pack is complete only at exact raw key parity with it, enforced by scripts/check-tui-locale-parity.py (CI) and the parity tests in crates/tui/src/localization.rs. See crates/tui/locales/AGENTS.md for the authoring contract.

Locale File Keys vs en.json (1299) Status Notes
English en.json 1299/1299 shipped Reference pack.
Japanese ja.json 1299/1299 shipped Complete.
Simplified Chinese zh-Hans.json 1299/1299 shipped Complete.
Traditional Chinese zh-Hant.json 1299/1299 partial Key-complete — nothing falls back at runtime. Still declared partial in PARTIAL_PACKS per #4057; promoting it out is an open decision.
Brazilian Portuguese pt-BR.json 1299/1299 shipped Complete.
Latin American Spanish es-419.json 1299/1299 shipped Complete. Note the website tracks es — the shipped TUI pack is Latin American Spanish, not es-ES.
Vietnamese vi.json 1299/1299 shipped Complete.
Korean ko.json 1299/1299 shipped Complete.
Catalan ca.json 1299/1299 shipped Complete (#4749/#4788). Awaiting native-speaker review.
German de.json 1299/1299 shipped Complete (#4788). Awaiting native-speaker review.
French fr.json 1299/1299 shipped Complete (#4788). Awaiting native-speaker review.
Indonesian id.json 1299/1299 shipped Complete (#4789). Awaiting native-speaker review.
Hindi hi.json 1299/1299 shipped Complete (#4790). Devanagari shaping spike: docs/evidence/v092-devanagari-terminal-shaping.md — code-level guarantees only; terminal visual QA and native review still open.
Russian ru.json 1299/1299 shipped Complete (#3092). Cyrillic script fixtures guard against mixed-language copy. Awaiting native-speaker review.
Ukrainian uk.json 1299/1299 shipped Complete (#4791). Cyrillic script fixtures keep it distinct from Russian (no ы/э/ъ; і/ї/є/ґ present). Awaiting native-speaker review.

Website locales

The website derives routing, the switcher, sitemap, and hreflang from ALL_LOCALES in web/lib/i18n/config.ts — one canonical registry, no second taxonomy. partial locales route and are selectable with a visible (partial) badge in the switcher; their dictionaries (web/lib/i18n/dictionaries/<code>/) cover shared chrome (masthead, nav, mobile menu, theme toggle, live ticker, footer, switcher) and the home page, held to exact key parity with the English reference by npm run check:locales and web/lib/i18n/dictionaries.test.ts. Everything outside that scope renders the English page copy — a deliberate fallback, never a dictionary key on screen.

As of #4934 (v0.9.4) there is one dictionary path for every routed locale, Chinese included. web/app/[locale]/page.tsx, web/components/nav.tsx, and web/components/footer.tsx no longer carry an isZh / foreign copy branch: they read getHome(locale) and getChrome(locale). web/lib/i18n/dictionaries/zh/ now exists (it used to be inline TSX), and nav/footer link sets are generated once in web/lib/i18n/links.ts so every locale gets the identical route shape. Reference shape: ChromeDict 52 keys, HomeDict 62 keys. Bilingual secondary nav labels, the masthead seal and issue line, the ticker live label, and the per-locale Intl date tag are dictionary values — no locale renders another language's script by accident.

Locale Code Status Notes
English en shipped Source text and the reference dictionary shape. Every page has an EN route.
Simplified Chinese zh shipped Full parity with EN on all first-class pages. Chrome + home are dictionary-backed (dictionaries/zh/) as of #4934; the remaining page bodies are still inline { en, zh } content modules.
Japanese ja partial #3091. Chrome + home page localized via dictionary; other page bodies/metadata fall back to English.
Vietnamese vi partial #3091. Same scope as Japanese.
Korean ko partial #3093. Same scope as Japanese.
Russian ru partial #3092. Same scope as Japanese.
Ukrainian uk partial #4791 — shipped alongside Russian, same scope.
Spanish es partial #3093. Same scope as Japanese.
Brazilian Portuguese pt-BR partial #3093. Same scope as Japanese.
French fr planned #4788 — TUI pack shipped in v0.9.2; website next wave.
German de planned #4788 — TUI pack shipped in v0.9.2; website next wave.
Catalan ca planned #4749/#4788 — TUI pack shipped in v0.9.2; website next wave.
Indonesian id partial #4789. Same scope as Japanese.
Hindi hi planned #4790 — TUI pack shipped in v0.9.2; website next wave.
Arabic ar deferred RTL candidate. Deferred until layout/typography QA exists (bidirectional text, mirrored chrome, number formatting).

Every partial locale carries the full 52/62 key set (see npm run check:locales); the chrome and home page are genuinely translated, not English pass-through — dictionaries.test.ts fails on an English prose value in a non-English pack. The new v0.9.4 strings are machine-translated to the same standard as the rest of each pack and are awaiting native-speaker review, consistent with the TUI packs above.

Remaining website scope for the partial locales (next wave): per-page body copy and generateMetadata titles/descriptions beyond the home page, the { en, zh } shared-content modules under web/lib/content/, the TerminalPlayer scene excerpts in web/components/thinking-trace.tsx, and the KIND_LABEL pairs in web/components/feed-card.tsx. The dictionary layer, routing, hreflang, and switcher already cover them, so filling in a page is a dictionary edit, not plumbing. That remaining English is exactly what the (partial) badge is honest about.

README locales

Locale File Status Parity check
English README.md shipped Canonical source
Simplified Chinese README.zh-CN.md shipped scripts/check-readme-translations.py (stamp + fences + URLs + sections)
Japanese README.ja-JP.md shipped Same
Vietnamese README.vi.md shipped Same
Korean README.ko-KR.md shipped Same
Latin American Spanish README.es-419.md shipped Same
Brazilian Portuguese README.pt-BR.md shipped Same
Russian README.ru.md shipped Same (#3092). Awaiting native-speaker review.
Ukrainian README.uk.md shipped Same (#4791). Awaiting native-speaker review.
Indonesian README.id.md shipped Same (#4789). Awaiting native-speaker review.

Drift checks

Check Tool Status
TUI pack key parity with en.json (complete packs) scripts/check-tui-locale-parity.py + parity tests in crates/tui/src/localization.rs Shipped (CI Lint job)
README translations stay in sync with README.md scripts/check-readme-translations.py Shipped (CI Lint job)
README locale links symmetric scripts/check-readme-locales.sh Shipped (CI Lint job)
Website dictionaries cover every routed locale except the en reference npm run check:locales + web/lib/i18n/dictionaries.test.ts Shipped (#3091, extended to zh in #4934)
No unmarked English prose survives in a non-English website dictionary leaves no unmarked English prose in any non-English dictionary in web/lib/i18n/dictionaries.test.ts Shipped (#4934)
Nav/footer routes stay in locale-swap parity for every routed locale web/lib/docs-ia.test.ts over web/lib/i18n/links.ts Shipped (#4934)
Accept-Language routes deterministically to all routed locales web/lib/i18n/detect.test.ts (middleware delegates to lib/i18n/detect.ts) Shipped (#3091)
Locale selector lists all routed locales with partial badges web/lib/i18n/config.test.ts (switcher + router derive from one registry) Shipped (#3091)
hreflang alternates cover every routed locale web/lib/page-meta.test.ts Shipped (#3091)
Cyrillic packs stay script-pure (no mixed-language copy, ru≠uk) cyrillic_packs_have_script_purity_and_no_mixed_language_fixtures in crates/tui/src/localization.rs + dictionaries.test.ts Shipped (#3092/#4791)
Devanagari grapheme-safe clip/wrap at 40/60/80 columns truncate_to_width_never_splits_devanagari_clusters + width fixtures in crates/tui/src/localization.rs Shipped (#4790)
Adding a UI locale never changes model-visible prompt bytes v092_locales_add_no_prompt_bookends_so_prompt_bytes_stay_stable in crates/tui/src/prompts.rs Shipped (cache-stability contract)
No shipped locale renders a missing-message marker no_shipped_locale_renders_a_missing_message_marker in crates/tui/src/localization.rs Shipped

How to add a locale

A locale is not "added" until all three surfaces below either ship it or carry an explicit planned/partial/deferred row in this matrix.

1. TUI pack

  1. Create crates/tui/locales/<tag>.json with every key in en.json, following crates/tui/locales/AGENTS.md (placeholders stay literal; product terms stay English per pack convention; preserve intentional leading/trailing spaces).
  2. Add the Locale variant plus its tag/translation_target_name/ parse_locale/shipped/shipped_complete arms in crates/tui/src/localization.rs, and the include_str! arm in the test module.
  3. Wire the typed settings schema (UiLocale in crates/tui/src/config_ui.rs) plus the pickers and displays that enumerate locales: onboarding language picker (crates/tui/src/tui/onboarding/language.rs — a test forces every shipped locale to be offered), setup-wizard match arms, and the locale display arms in the /config and changelog commands. Keep the schema/round-trip invariant tied to Locale::shipped() so these surfaces cannot silently drift.
  4. Run python3 scripts/check-tui-locale-parity.py and cargo test -p codewhale-tui localization.
  5. If the pack must ship incomplete, declare it partial (see zh-Hant / #4057): keep it out of shipped_complete(), mark it in is_partial_pack(), and add it to PARTIAL_PACKS in scripts/check-tui-locale-parity.py with a tracking issue.

2. README

  1. Translate README.md into README.<tag>.md, preserving structure, commands, and the #3087 factual history.
  2. Cross-link it from the language line in README.md and from the other translated READMEs.
  3. Restamp per scripts/check-readme-translations.py, then run python3 scripts/check-readme-translations.py and bash scripts/check-readme-locales.sh.

3. Website

  1. Add/flip the locale entry in ALL_LOCALES in web/lib/i18n/config.ts — the switcher, routes, middleware, sitemap, and hreflang derive from it, so no per-locale switcher edit is needed. Use the partial status for locales that ship the chrome+home dictionary scope before full page parity.
  2. Create web/lib/i18n/dictionaries/<code>/chrome.ts and home.ts following the English reference shape (dictionaries/en/).
  3. Middleware detection needs no change for base tags; region variants and base→variant mappings live in web/lib/i18n/detect.ts.
  4. Run cd web && npm run check:locales && npm test && npm run build.

4. Matrix

Update the TUI, README, and Website tables above — one row per surface, with per-surface status.

Assessments

Galician (gl) and Basque (eu) — 2026-07-25, per #4749

Assessed alongside the Catalan pack (#4749 / #4788), which asked whether Galician and Basque are "similar-value European additions" worth shipping in the same wave.

Decision: defer both. Rationale:

  • The case #4788 makes for Catalan is specifically that it "has an unusually strong software-localization tradition and an active volunteer community" — a review-capacity argument, not a market-size one. That argument does not transfer: Galician and Basque have materially smaller localization communities, so a pack for either would ship with no realistic path to native-speaker review.
  • Galician speakers have a workable fallback already: the shipped es-419 pack (and pt-BR is lexically close). Basque is a language isolate with no fallback proximity — its per-string review cost is the highest of the three, and machine-translated Basque is the least trustworthy of the three.
  • There is no natural "ship together" grouping: the v0.9.2 wave already bundles the locales that share acceptance criteria (Latin-script fr/de/ca/id, Cyrillic uk, Devanagari hi). gl/eu share only the review-capacity constraint, which neither clears.

Cost/demand evidence behind the decision: a complete TUI pack is 1,299 keys (~812k words) plus an ongoing obligation to retranslate every changed English string in lockstep — the parity gate makes silent drift a CI failure, so an unmaintained pack is worse than none. No community member has requested gl or eu (no issues, no PRs, no translations offered), while the gl/eu base tags already route cleanly through web/middleware.ts the day a champion appears. We do not ship packs we cannot get natively reviewed, and we do not advertise unshipped packs.

Revisit when a native-speaker champion appears for either language, or if Catalan uptake after v0.9.2 suggests demand. Both base tags (gl, eu) route through web/middleware.ts with no middleware change when that happens.

  • #3091 — Website parity with JA + VI README locales
  • #3092 — Russian README + website localization
  • #3093 — Korean, Spanish, Brazilian Portuguese next-wave locales
  • #3087 — Post-rebrand README source text refresh
  • #4057 — zh-Hant scoped as a partial TUI pack with English fallback
  • #4787 — This matrix's TUI table + the locale-drift CI gates
  • #4788 — French, German, Catalan TUI localization
  • #4789 — Indonesian localization
  • #4790 — Hindi localization + Devanagari terminal-shaping spike
  • #4791 — Ukrainian localization alongside Russian
  • #4749 — Catalan UI language + Galician/Basque assessment