From 1e62837c68f3be0d8d70489bba1148f721b55cda Mon Sep 17 00:00:00 2001 From: Ilia Denisov Date: Fri, 22 May 2026 08:48:13 +0200 Subject: [PATCH] feat(ui): locale persistence + i18n completeness guards (F3) An audit found the client already i18n-first: one hard-coded UI string (the battle-scene aria-label, now keyed) and en/ru already share an identical 692-key set. - Persist the locale: i18n.setLocale writes localStorage (galaxy-locale) and the store boots from stored > browser detection > default, so a language switch survives reloads. - tests/i18n-completeness.test.ts: en/ru key-set parity, non-empty values, and locale persistence. - Docs: ui/docs/i18n.md; mark F3 done in ui/PLAN-finalize.md. Co-Authored-By: Claude Opus 4.7 (1M context) --- ui/PLAN-finalize.md | 12 +++- ui/docs/i18n.md | 16 +++-- .../src/lib/battle-player/battle-scene.svelte | 3 +- ui/frontend/src/lib/i18n/index.svelte.ts | 25 +++++++- ui/frontend/src/lib/i18n/locales/en.ts | 1 + ui/frontend/src/lib/i18n/locales/ru.ts | 1 + ui/frontend/tests/i18n-completeness.test.ts | 59 +++++++++++++++++++ 7 files changed, 107 insertions(+), 10 deletions(-) create mode 100644 ui/frontend/tests/i18n-completeness.test.ts diff --git a/ui/PLAN-finalize.md b/ui/PLAN-finalize.md index 2e1dfed..efd3e37 100644 --- a/ui/PLAN-finalize.md +++ b/ui/PLAN-finalize.md @@ -69,7 +69,17 @@ axe-core scan; full keyboard reachability with visible focus. Tests: axe-core integration tests on every top-level view; Playwright keyboard-only navigation. -## F3 — Localisation completeness +## F3 — Localisation completeness — done + +An audit found the client already i18n-first (the single hard-coded UI +string, the battle-scene `aria-label`, is now keyed); en/ru already have +identical key sets (692 keys). Added locale **persistence** to the i18n +store (`localStorage` `galaxy-locale`: stored choice > browser detection +> default) so a switch survives reloads, and +`tests/i18n-completeness.test.ts` enforcing en/ru key-set parity, +non-empty values, and persistence. A noisy literal-text lint was +deliberately skipped — the structural parity test plus the i18n-first +convention cover drift. Docs: `ui/docs/i18n.md`. (From Phase 35.) Goal: every visible string is translated (en + ru). diff --git a/ui/docs/i18n.md b/ui/docs/i18n.md index 7cfe9e4..069b6c9 100644 --- a/ui/docs/i18n.md +++ b/ui/docs/i18n.md @@ -82,12 +82,13 @@ Native wrappers (Wails, Capacitor) will pass their system locale once the desktop/mobile targets land (see ../ROADMAP.md); the helper is platform-agnostic by design. -The detection runs once at module load — there is no asynchronous -init step. Callers that mutate the locale (e.g. the language picker -on `/login`) call `i18n.setLocale(next)` directly. The choice is -**not** persisted between page reloads; the next visit re-runs -detection. Persistence is deferred to the finalization plan -(../Plan-finalize.md). +The boot locale resolves once at module load (no async init): +an explicit stored choice wins, otherwise browser/system detection, +otherwise `DEFAULT_LOCALE`. Callers that mutate the locale (the language +pickers on `/login` and in the account menu) call `i18n.setLocale(next)`, +which **persists** the choice to `localStorage` (key `galaxy-locale`) so +it survives reloads. An unrecognised stored value is ignored and falls +back to detection. ## Forwarding the locale to the gateway @@ -135,6 +136,9 @@ at challenge issuance and replayed from the challenge row. - `tests/i18n.test.ts` covers `detectInitialLocale`, `i18n.setLocale`, parameter interpolation, and the unknown-key fallback. +- `tests/i18n-completeness.test.ts` enforces en/ru key-set parity (no + key present in one locale but missing in the other), non-empty values, + and locale persistence (a stored choice is restored on the next load). - `tests/login-page.test.ts` asserts the language picker renders with native names, switching the locale re-renders the form text, and `sendEmailCode` receives the active locale. diff --git a/ui/frontend/src/lib/battle-player/battle-scene.svelte b/ui/frontend/src/lib/battle-player/battle-scene.svelte index bcc2292..55e8a9e 100644 --- a/ui/frontend/src/lib/battle-player/battle-scene.svelte +++ b/ui/frontend/src/lib/battle-player/battle-scene.svelte @@ -23,6 +23,7 @@ combat filter, so a shot never collapses to a single visual node.