fix(i18n): make real-locale catalog parity advisory, not a gate (#2272)

Co-authored-by: Kandev Agent <agent@kandev.dev>
This commit is contained in:
João Salavisa
2026-08-05 11:22:36 +02:00
committed by GitHub
parent c3fd434a24
commit a2db334adc
4 changed files with 240 additions and 46 deletions
+53 -5
View File
@@ -44,8 +44,12 @@ Two enforcement layers, and it matters which is which:
`sidebar`, `slack`, `system`, `workflows`, and `workspaces`. Each migrated area
can add the namespace it needs (`statusBar`, `kanban`, `task`, …). A string used
in two or more areas belongs in `common` so it is translated once. A new
namespace needs no registration: `lib/i18n/index.ts` discovers catalogs by glob,
but every real locale must add the same namespace in the same change.
namespace needs no registration: `lib/i18n/index.ts` discovers catalogs by glob.
Adding the same namespace to every real locale in the same change is the ideal,
but it is **not required and does not gate** — translation is handled out of
band, so `i18n:check` reports a real-locale gap as a warning. See
[Real locales warn, they do not gate](#real-locales-warn-they-do-not-gate).
### Interpolation
@@ -841,9 +845,10 @@ case variants such as `zh-CN`, but cookies and `<html lang>` use the canonical i
tests for canonicalization, cookie and `Accept-Language` negotiation, message
lookup, exact catalog parity, and the first shell response.
5. Run `pnpm run i18n:check` from `apps/web`. It discovers every real locale
except `en` and generated `pseudo`, then fails on any missing or extra
namespace or key. Regenerate `pseudo` only from English with
`pnpm run i18n:pseudo`; never copy a human translation into it.
except `en` and generated `pseudo`, and **warns** on any missing or extra
namespace or key — advisory, not a build failure; see below. Regenerate
`pseudo` only from English with `pnpm run i18n:pseudo`; never copy a human
translation into it.
6. In a development or E2E build, select the new language under **Settings >
General > Appearance**, reload, and switch back to English. Confirm translated
copy, the locale cookie, `<html lang>`, interpolation, layout, and then use
@@ -853,3 +858,46 @@ Kandev's migration remains incremental. A complete locale catalog guarantees
that every currently externalized message has a translation; it does not mean
that every screen has been externalized. Unmigrated surfaces continue to render
English until their directory joins `i18nGuardFiles`.
### Real locales warn, they do not gate
`en` and `pseudo` gate. `en` is authored in the same PR as the code and `pseudo`
is **generated** from it, so a mismatch between them is always the author's to
fix in the change that caused it. Real-locale parity is a **warning**.
That is a deliberate policy, not an oversight. Translation happens out of band,
on a third party's cadence, so gating on it makes an ordinary English-only PR
fail CI for work nobody in the merge path can do. It is not hypothetical: #2243
introduced `zh-cn` together with the parity gate, #2261 added 13 English `chat`
keys twenty minutes later, and `main` went red — not because either change was
wrong, but because the two could not have been ordered to avoid it. The gate
fired exactly as designed and blocked the wrong people.
So: **add the keys to every real locale when you can, and never let a missing
one stop you merging.** The warning is addressed to whoever owns the
translation.
#### It is a structural check, not a translation check
Worth stating plainly, because assuming otherwise has already produced two wrong
conclusions: a green run does **not** mean "translated". The pass compares
shapes. Measured, one mutation at a time:
| mutation | result |
|---|---|
| value identical to English | **not caught** |
| whole catalog copy-pasted from `en` | **not caught** |
| swapped `_one` / `_other` | **not caught** |
| empty / whitespace / non-string value | caught |
| missing key, extra key | caught |
| missing namespace, extra namespace | caught |
| dropped `{{placeholder}}` | caught |
| dropped or renumbered `<n>` tag | caught |
The identical-to-English tolerance is deliberate and cannot be tightened
naively: brand nouns and technical literals must stay verbatim (`automations`
alone has seven — `GitHub`, `Webhook`, `Webhook URL`, …), so `value === en` would
report each of them. Closing it needs a per-key exemption list or a
whole-file proportion threshold. Reviewing a translation is a human job;
`scripts/check-i18n-keys.test.ts` pins both the caught and the uncaught cases so
this stays honest.