返回 CodeWhale
LOCALIZATION.md
根目录 / docs / LOCALIZATION.md
1 # Localization Matrix
2
3 > 阅读简体中文版:[zh_hans/LOCALIZATION.md](zh_hans/LOCALIZATION.md)。
4
5 Canonical tracking document for every locale Codewhale ships, is actively
6 building, is planning, or has explicitly deferred.
7
8 > **Scope note (2026-07-12):** this matrix covers three surfaces — the TUI
9 > locale packs (`crates/localization/locales/`), the translated READMEs (repo root),
10 > and the website (`web/`). The three ship on different cadences, so a
11 > locale can be **shipped** on one surface and **planned** on another; the
12 > per-surface tables below are the per-surface truth. The website registry
13 > is `web/lib/i18n/config.ts` (`ALL_LOCALES`): the locale switcher and route
14 > generation both derive from it.
15 >
16 > Docs translations are **not** a locale surface: they live under
17 > `docs/zh_hans/` and `docs/id/`, and their status is tracked in
18 > `docs/zh_hans/README.md` and issue #5482, not in this matrix.
19
20 Customer-visible copy also follows the [Codewhale voice and terminal
21 charter](VOICE.md); commands, key names, and glyphs remain code-owned around
22 localized prose.
23
24 Last updated: 2026-08-18 (docs/zh_hans/ restructure; docs translation
25 status tracked outside this matrix, per #5482).
26 Source-of-truth README: `README.md` (English, post-#3087).
27
28 ## Status legend
29
30 | Status | Meaning |
31 |--------|---------|
32 | **shipped** | Live on codewhale.net and/or published as a standalone README, or a TUI pack at exact `en.json` parity |
33 | **partial** | Shipped but intentionally incomplete; missing scope falls back to English and the partial status is visible |
34 | **planned** | Explicitly prioritized for the next wave |
35 | **deferred** | Acknowledged as wanted but not yet scheduled; needs layout QA, bridge support, or community champion |
36
37 ---
38
39 ## TUI locale packs
40
41 The TUI packs under `crates/localization/locales/` are the largest translation
42 surface in the repo. `en.json` is the reference; a pack is **complete**
43 only at exact raw key parity with it, enforced by
44 `scripts/check-tui-locale-parity.py` (CI) and the parity tests in
45 `crates/localization/src/lib.rs`. See `crates/localization/locales/AGENTS.md` for the
46 authoring contract.
47
48 | Locale | File | Keys vs `en.json` | Status | Notes |
49 |--------|------|--------------------------|--------|-------|
50 | English | `en.json` | all | **shipped** | Reference pack. |
51 | Japanese | `ja.json` | all | **shipped** | Complete. |
52 | Simplified Chinese | `zh-Hans.json` | all | **shipped** | Complete. |
53 | Traditional Chinese | `zh-Hant.json` | all | **shipped** | Complete (#5143). Awaiting native-speaker review. |
54 | Brazilian Portuguese | `pt-BR.json` | all | **shipped** | Complete. |
55 | Latin American Spanish | `es-419.json` | all | **shipped** | Complete. Note the website tracks `es` — the shipped TUI pack is Latin American Spanish, not `es-ES`. |
56 | Vietnamese | `vi.json` | all | **shipped** | Complete. |
57 | Korean | `ko.json` | all | **shipped** | Complete. |
58 | Catalan | `ca.json` | all | **shipped** | Complete (#4749/#4788). Awaiting native-speaker review. |
59 | German | `de.json` | all | **shipped** | Complete (#4788). Awaiting native-speaker review. |
60 | French | `fr.json` | all | **shipped** | Complete (#4788). Awaiting native-speaker review. |
61 | Indonesian | `id.json` | all | **shipped** | Complete (#4789). Awaiting native-speaker review. |
62 | Hindi | `hi.json` | all | **shipped** | Complete (#4790). The Devanagari shaping spike (moved out of this repository in `7242381022`) gave code-level guarantees only; terminal visual QA and native review still open. |
63 | Russian | `ru.json` | all | **shipped** | Complete (#3092). Cyrillic script fixtures guard against mixed-language copy. Awaiting native-speaker review. |
64 | Ukrainian | `uk.json` | all | **shipped** | Complete (#4791). Cyrillic script fixtures keep it distinct from Russian (no ы/э/ъ; і/ї/є/ґ present). Awaiting native-speaker review. |
65
66 ## Website locales
67
68 The website derives routing, the switcher, sitemap, and hreflang from
69 `ALL_LOCALES` in `web/lib/i18n/config.ts` — one canonical registry, no
70 second taxonomy. **partial** locales route and are selectable with a
71 visible `(partial)` badge in the switcher; their dictionaries
72 (`web/lib/i18n/dictionaries/<code>/`) cover shared chrome (masthead, nav,
73 mobile menu, theme toggle, live ticker, footer, switcher) and the home page,
74 held to exact key parity with the English reference by
75 `npm run check:locales` and `web/lib/i18n/dictionaries.test.ts`.
76 Everything outside that scope renders the English page copy — a deliberate
77 fallback, never a dictionary key on screen.
78
79 **As of #4934 (v0.9.4) there is one dictionary path for every routed
80 locale, Chinese included.** `web/app/[locale]/page.tsx`,
81 `web/components/nav.tsx`, and `web/components/footer.tsx` no longer carry an
82 `isZh` / `foreign` copy branch: they read `getHome(locale)` and
83 `getChrome(locale)`. `web/lib/i18n/dictionaries/zh/` now exists (it used to
84 be inline TSX), and nav/footer link sets are generated once in
85 `web/lib/i18n/links.ts` so every locale gets the identical route shape.
86
87 **Website/docs translation pipeline (General Translation CLI, 2026-08-28).**
88 Runtime stays the dictionaries above — do not add `gt-next` beside them.
89 `web/gt-catalog/[locale].json` is the local JSON interchange (`en` + live
90 `zh` first). `npm run i18n:gt -- export` writes catalogs from dictionaries;
91 `check` (hooked from `check:locales`) requires them to match; `import`
92 writes reviewed JSON back to website dictionary TS only. `translate` is
93 fail-closed unless BYOK `GT_API_KEY` and `GT_PROJECT_ID` are set in the
94 environment — never commit those values, never point this config at
95 `crates/localization/locales`, and never wrap model completions. `gt generate` is
96 not used: it is a framework JSX scanner, not a JSON-catalog tool.
97 Reference shape: **`ChromeDict` 52 keys, `HomeDict` 62 keys.** Bilingual
98 secondary nav labels, the masthead seal and issue line, the ticker live
99 label, and the per-locale `Intl` date tag are dictionary values — no locale
100 renders another language's script by accident.
101
102 | Locale | Code | Status | Notes |
103 |--------|------|--------|-------|
104 | English | `en` | **shipped** | Source text and the reference dictionary shape. Every page has an EN route. |
105 | 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. |
106 | Japanese | `ja` | **partial** | #3091. Chrome + home page localized via dictionary; other page bodies/metadata fall back to English. |
107 | Vietnamese | `vi` | **partial** | #3091. Same scope as Japanese. |
108 | Korean | `ko` | **partial** | #3093. Same scope as Japanese. |
109 | Russian | `ru` | **partial** | #3092. Same scope as Japanese. |
110 | Ukrainian | `uk` | **partial** | #4791 — shipped alongside Russian, same scope. |
111 | Spanish | `es` | **partial** | #3093. Same scope as Japanese. |
112 | Brazilian Portuguese | `pt-BR` | **partial** | #3093. Same scope as Japanese. |
113 | French | `fr` | **planned** | #4788 — TUI pack shipped in v0.9.2; website next wave. |
114 | German | `de` | **planned** | #4788 — TUI pack shipped in v0.9.2; website next wave. |
115 | Catalan | `ca` | **planned** | #4749/#4788 — TUI pack shipped in v0.9.2; website next wave. |
116 | Indonesian | `id` | **partial** | #4789. Same scope as Japanese. |
117 | Hindi | `hi` | **planned** | #4790 — TUI pack shipped in v0.9.2; website next wave. |
118 | Arabic | `ar` | **deferred** | RTL candidate. Deferred until layout/typography QA exists (bidirectional text, mirrored chrome, number formatting). |
119
120 Every partial locale carries the full 52/62 key set (see
121 `npm run check:locales`); the chrome and home page are genuinely translated,
122 not English pass-through — `dictionaries.test.ts` fails on an English
123 prose value in a non-English pack. The new v0.9.4 strings are
124 machine-translated to the same standard as the rest of each pack and are
125 **awaiting native-speaker review**, consistent with the TUI packs above.
126
127 Remaining website scope for the partial locales (next wave): per-page body
128 copy and `generateMetadata` titles/descriptions beyond the home page, the
129 `{ en, zh }` shared-content modules under `web/lib/content/`, the
130 TerminalPlayer scene excerpts in `web/components/thinking-trace.tsx`, and
131 the `KIND_LABEL` pairs in `web/components/feed-card.tsx`. The dictionary
132 layer, routing, hreflang, and switcher already cover them, so filling in a
133 page is a dictionary edit, not plumbing. That remaining English is exactly
134 what the `(partial)` badge is honest about.
135
136 ## README locales
137
138 | Locale | File | Status | Parity check |
139 |--------|------|--------|-------------|
140 | English | `README.md` | **shipped** | Canonical source |
141 | Simplified Chinese | `README.zh-CN.md` | **shipped** | `scripts/check-readme-translations.py` (stamp + fences + URLs + sections) |
142 | Japanese | `README.ja-JP.md` | **shipped** | Same |
143 | Vietnamese | `README.vi.md` | **shipped** | Same |
144 | Korean | `README.ko-KR.md` | **shipped** | Same |
145 | Latin American Spanish | `README.es-419.md` | **shipped** | Same |
146 | Brazilian Portuguese | `README.pt-BR.md` | **shipped** | Same |
147 | Russian | `README.ru.md` | **shipped** | Same (#3092). Awaiting native-speaker review. |
148 | Ukrainian | `README.uk.md` | **shipped** | Same (#4791). Awaiting native-speaker review. |
149 | Indonesian | `README.id.md` | **shipped** | Same (#4789). Awaiting native-speaker review. |
150 | French | `README.fr.md` | **shipped** | Same. Awaiting native-speaker review. |
151 | German | `README.de.md` | **shipped** | Same. Awaiting native-speaker review. |
152 | Traditional Chinese | `README.zh-TW.md` | **shipped** | Same. Awaiting native-speaker review. |
153 | Hindi | `README.hi.md` | **shipped** | Same. Awaiting native-speaker review. |
154 | Turkish | `README.tr.md` | **shipped** | Same. Awaiting native-speaker review. |
155 | Italian | `README.it.md` | **shipped** | Same. Awaiting native-speaker review. |
156 | Polish | `README.pl.md` | **shipped** | Same. Awaiting native-speaker review. |
157 | Arabic | `README.ar.md` | **shipped** | Same. Awaiting native-speaker review. Markdown only; no HTML `dir` attributes. |
158 | Catalan | `README.ca.md` | **shipped** | Same. Awaiting native-speaker review. |
159
160 ## Drift checks
161
162 | Check | Tool | Status |
163 |-------|------|--------|
164 | TUI pack key parity with `en.json` (complete packs) | `scripts/check-tui-locale-parity.py` + parity tests in `crates/localization/src/lib.rs` | **Shipped** (CI Lint job) |
165 | README translations stay in sync with `README.md` | `scripts/check-readme-translations.py` | **Shipped** (CI Lint job) |
166 | README locale links symmetric | `scripts/check-readme-locales.sh` | **Shipped** (CI Lint job) |
167 | 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) |
168 | 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) |
169 | 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) |
170 | Accept-Language routes deterministically to all routed locales | `web/lib/i18n/detect.test.ts` (middleware delegates to `lib/i18n/detect.ts`) | **Shipped** (#3091) |
171 | Locale selector lists all routed locales with partial badges | `web/lib/i18n/config.test.ts` (switcher + router derive from one registry) | **Shipped** (#3091) |
172 | hreflang alternates cover every routed locale | `web/lib/page-meta.test.ts` | **Shipped** (#3091) |
173 | Cyrillic packs stay script-pure (no mixed-language copy, ru≠uk) | `cyrillic_packs_have_script_purity_and_no_mixed_language_fixtures` in `crates/localization/src/lib.rs` + `dictionaries.test.ts` | **Shipped** (#3092/#4791) |
174 | Devanagari grapheme-safe clip/wrap at 40/60/80 columns | `truncate_to_width_never_splits_devanagari_clusters` + width fixtures in `crates/localization/src/lib.rs` | **Shipped** (#4790) |
175 | 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) |
176 | No shipped locale renders a missing-message marker | `no_shipped_locale_renders_a_missing_message_marker` in `crates/localization/src/lib.rs` | **Shipped** |
177
178 ## How to add a locale
179
180 A locale is not "added" until all three surfaces below either ship it or
181 carry an explicit `planned`/`partial`/`deferred` row in this matrix.
182
183 ### 1. TUI pack
184
185 1. Create `crates/localization/locales/<tag>.json` with every key in `en.json`,
186 following `crates/localization/locales/AGENTS.md` (placeholders stay literal;
187 product terms stay English per pack convention; preserve intentional
188 leading/trailing spaces).
189 2. Add the `Locale` variant plus its `tag`/`translation_target_name`/
190 `parse_locale`/`shipped`/`shipped_complete` arms in
191 `crates/localization/src/lib.rs`, and the `include_str!` arm in the
192 test module.
193 3. Wire the surfaces that still enumerate locales by hand. The `locale`
194 setting is a plain string row in `crates/config/src/settings_schema.rs`,
195 validated by `normalize_configured_locale` (which reuses `parse_locale`
196 from step 2), and the `/config` value list (`config_choice_values` in
197 `crates/tui/src/tui/views/mod.rs`) and hint text
198 (`configured_locale_values`) derive from `Locale::shipped()`, so those need
199 no edit. The exhaustive `match` arms the compiler will point at are the
200 setup wizard (`crates/tui/src/tui/setup/mod.rs`), `locale_display` in
201 `crates/tui/src/commands/groups/config/config.rs`, and
202 `public_site_locale_segment` (the `/links` site path) in
203 `crates/tui/src/commands/groups/core/core.rs`. Add a `LANGUAGE_OPTIONS`
204 entry to the onboarding language picker
205 (`crates/tui/src/tui/onboarding/language.rs`); its
206 `picker_offers_every_shipped_locale` test fails until you do. Several no-English-leak tests
207 (for example in `status_picker.rs` and `tool_card.rs`) list locales
208 explicitly; add the new tag there when the pack is complete.
209 4. Run `python3 scripts/check-tui-locale-parity.py` and
210 `cargo test -p codewhale-tui localization`.
211 5. If the pack must ship incomplete, declare it partial: keep it out of
212 `shipped_complete()`, mark it in `is_partial_pack()`, and add it to
213 `PARTIAL_PACKS` in `scripts/check-tui-locale-parity.py` with a tracking
214 issue. No pack is partial today — `PARTIAL_PACKS` is empty and
215 `is_partial_pack()` returns false for every shipped locale — so a new
216 entry is the only thing that reopens the English-fallback path.
217
218 ### 2. README
219
220 1. Translate `README.md` into `README.<tag>.md`, preserving structure,
221 commands, and the #3087 factual history.
222 2. Cross-link it from the language line in `README.md` and from the other
223 translated READMEs.
224 3. Restamp per `scripts/check-readme-translations.py`, then run
225 `python3 scripts/check-readme-translations.py` and
226 `bash scripts/check-readme-locales.sh`.
227
228 ### 3. Website
229
230 1. Add/flip the locale entry in `ALL_LOCALES` in `web/lib/i18n/config.ts` —
231 the switcher, routes, middleware, sitemap, and hreflang derive from it,
232 so no per-locale switcher edit is needed. Use the `partial` status for
233 locales that ship the chrome+home dictionary scope before full page
234 parity.
235 2. Create `web/lib/i18n/dictionaries/<code>/chrome.ts` and `home.ts`
236 following the English reference shape (`dictionaries/en/`).
237 3. Middleware detection needs no change for base tags; region variants and
238 base→variant mappings live in `web/lib/i18n/detect.ts`.
239 4. Run `cd web && npm run check:locales && npm test && npm run build`.
240
241 ### 4. Matrix
242
243 Update the TUI, README, and Website tables above — one row per surface,
244 with per-surface status.
245
246 ## Assessments
247
248 ### Galician (`gl`) and Basque (`eu`) — 2026-07-25, per #4749
249
250 Assessed alongside the Catalan pack (#4749 / #4788), which asked whether
251 Galician and Basque are "similar-value European additions" worth shipping
252 in the same wave.
253
254 **Decision: defer both.** Rationale:
255
256 - The case #4788 makes for Catalan is specifically that it "has an
257 unusually strong software-localization tradition and an active volunteer
258 community" — a review-capacity argument, not a market-size one. That
259 argument does not transfer: Galician and Basque have materially smaller
260 localization communities, so a pack for either would ship with no
261 realistic path to native-speaker review.
262 - Galician speakers have a workable fallback already: the shipped
263 `es-419` pack (and `pt-BR` is lexically close). Basque is a language
264 isolate with no fallback proximity — its per-string review cost is the
265 highest of the three, and machine-translated Basque is the least
266 trustworthy of the three.
267 - There is no natural "ship together" grouping: the v0.9.2 wave already
268 bundles the locales that share acceptance criteria (Latin-script
269 fr/de/ca/id, Cyrillic uk, Devanagari hi). gl/eu share only the
270 review-capacity constraint, which neither clears.
271
272 **Cost/demand evidence behind the decision:** a complete TUI pack is
273 1,299 keys (~8–12k words) plus an ongoing obligation to retranslate every
274 changed English string in lockstep — the parity gate makes silent drift a
275 CI failure, so an unmaintained pack is worse than none. No community
276 member has requested gl or eu (no issues, no PRs, no translations offered),
277 while the gl/eu base tags already route cleanly through
278 `web/middleware.ts` the day a champion appears. We do not ship packs we
279 cannot get natively reviewed, and we do not advertise unshipped packs.
280
281 Revisit when a native-speaker champion appears for either language, or if
282 Catalan uptake after v0.9.2 suggests demand. Both base tags (`gl`, `eu`)
283 route through `web/middleware.ts` with no middleware change when that
284 happens.
285
286 ## Related issues
287
288 - #3091 — Website parity with JA + VI README locales
289 - #3092 — Russian README + website localization
290 - #3093 — Korean, Spanish, Brazilian Portuguese next-wave locales
291 - #3087 — Post-rebrand README source text refresh
292 - #4057 — `zh-Hant` scoped as a partial TUI pack with English fallback
293 - #4787 — This matrix's TUI table + the locale-drift CI gates
294 - #4788 — French, German, Catalan TUI localization
295 - #4789 — Indonesian localization
296 - #4790 — Hindi localization + Devanagari terminal-shaping spike
297 - #4791 — Ukrainian localization alongside Russian
298 - #4749 — Catalan UI language + Galician/Basque assessment
299 - #5482 — EPIC(docs): review, partially restructure, and fully localize
300 documentation to Chinese
301
301 lines MARKDOWN