diff --git a/docs/localization.md b/docs/localization.md index 57a5af9b..9fa3bab6 100644 --- a/docs/localization.md +++ b/docs/localization.md @@ -25,35 +25,44 @@ the server does instead, which is the one decision that reaches outside this bou ## What exists today, and what it cannot do -There is already an i18n system. It is small, hand-rolled, has no npm dependency, and it works. It is -also about **5% deployed**, and that gap is the whole job. +**Updated 2026-08-23, after phases 0, 1 and 2.** This section described the pre-phase-0 world for long +enough that the next reader would have started from a false picture — bare `en.json`, 46 keys, three +components. All of that is now history and is kept below only as the "before" column, because the shape of +the gap is the argument for the plan and deleting it would leave the phases looking arbitrary. -| | | -| ----------------------------------------- | -------------------------------------------------------------------------- | -| `src/workspaces/i18n/` | the package: `I18nProvider`, `useTranslation`, `resolveKey`, `interpolate` | -| `src/apps/officer-web/lib/i18n.ts` | a typed wrapper binding keys to `en.json` | -| `src/apps/officer-web/lib/I18nBridge.tsx` | reads `settings.languages.default`, feeds the provider | -| `src/apps/officer-web/locales/` | `en.json` and `pt.json` — **46 keys each, in sync** | -| components actually using it | **3** — `UserMenu`, `Languages`, `ChangePassword` | +| | before phase 0 | now | +| -------------------------- | ---------------------- | --------------------------------------------- | +| catalogs | `en.json`, `pt.json` | `en-GB.json`, `pt-PT.json` per workspace | +| keys | 46 | **581** — 538 `app.*`, 43 `ui.*` | +| workspaces with a catalog | 1 (`officer-web`) | 3 (`officer-web`, `components`, `officerdev`) | +| components using it | 3 | all of `officer-web` and `components` | +| plural rules | none | `Intl.PluralRules`, per locale | +| dates and numbers | browser locale | `useFormat()`, the account's locale | +| a missing key | rendered its dot-path | falls back to English, warns in dev | +| locales offered vs shipped | ~20 offered, 2 existed | only shipped locales are offered | +| parity enforcement | none | **a gap fails the build** | -What it already does well, and which this plan keeps: dot-path lookup, `{{param}}` interpolation, -fallback to `en`, and **type-safe keys** via `TranslationKeys` — a wrong key is a -typecheck failure, which is worth more than it sounds when one agent is editing 400 files. +Still standing from the original description, and still true: the package is hand-rolled, has no npm +dependency, and gives type-safe keys — a wrong key is a typecheck failure, which is worth more than it +sounds when one agent is editing hundreds of files. -**What it cannot do yet**, each of which is a task below: +**What remains:** `src/workspaces/officerdev` — the panels — and then translation. `panel.*` is scaffolded, +wired into the bridge, and empty. -1. **No plural rules.** `1 file` / `2 files` has no expression. `Intl.PluralRules` is built into the - runtime and unused. -2. **No locale-aware dates or numbers.** ~23 `toLocaleDateString/TimeString/String()` call sites use - the _browser's_ locale, not the user's chosen one, so a Portuguese UI shows American dates. -3. **No fallback chain.** `resolveKey` falls back to `en` and nothing else, so `pt-BR` cannot inherit - from `pt`. -4. **One catalog, one workspace.** `locales/` lives inside `officer-web`. The 213 panel files in - `officerdev` and the 91 in `components` have no catalog of their own. -5. **Silent, ugly misses.** A missing key renders its own dot-path (`settings.profile.foo`) to the - user. No warning, no test. -6. **Twenty languages offered, two that exist.** The picker in `Languages.tsx` lists ~20 codes - including Hebrew and Greek. Choosing any of the eighteen silently yields English. +Two things learned doing phases 1 and 2 that the plan did not anticipate: + +- **`TranslationKeys` walked into a plural object**, so `t('key', { count })` — the entire point of D7 — + did not compile while `t('key.one')` did. The documented feature was uncompilable and the workaround was + not, which no test would ever have noticed because phase 1 contained no plurals. Fixed with type-level + assertions. +- **The typed wrappers rebuilt `t` on every render**, which is the trap `CLAUDE.md` already records against + `useClient()` and the Jellyfin player. It matters the moment `t` lands in a `useMemo` dependency list, + which phase 2 needed immediately. Memoised. + +**A shape this format cannot express:** a sentence with an inline link. Splitting around the anchor gives a +key whose Portuguese value is `"."`. The cheap fix is rephrasing so the link falls at a clause boundary; +there is no node-level interpolation helper and adding one is a design decision, not a patch. Worth +raising if a phase turns out to be full of them rather than absorbing it forty times. ### The size of the remaining work