Skip to content

F182: i18n infrastructure (SPA foundation) — react-i18next, coordinated bootstrap, errorCodes, pilot#233

Open
mforce wants to merge 12 commits into
mainfrom
f182-i18n-spa
Open

F182: i18n infrastructure (SPA foundation) — react-i18next, coordinated bootstrap, errorCodes, pilot#233
mforce wants to merge 12 commits into
mainfrom
f182-i18n-spa

Conversation

@mforce

@mforce mforce commented Jul 26, 2026

Copy link
Copy Markdown
Owner

The SPA i18n foundation for #182 — the plumbing so screens add catalog keys instead of hardcoded strings, and the stored §4.5 language finally drives display. English-only; no second language ships here.

#182 stays open as the tracker for the full string sweep of the remaining ~20 screens (which blocks Phase 1.5). This PR is the foundation + a worked pilot.

What this adds

  • react-i18next + a typed English catalog (src/i18n/en.ts) — namespaced (common/auth/sales/account/errors), CustomTypeOptions makes t("typo") a compile error, keySeparator:false (the errors keys are dotted API codes), init-once singleton with fallbackLng:"en" (missing key → English, never blank/raw key).
  • Coordinated no-flash bootstrap (src/session/SessionContext.tsx) — inside ProtectedRoute, SessionProvider reads /me + /account via Promise.allSettled, resolves the UI language (me.language → farm-locale subtag → English, case-insensitive, pack-gated), calls i18n.changeLanguage, and gates the shell (if (!result) return null) until the language is set — so the authenticated shell never paints one English frame first. It hands the account to FarmProvider (new tri-state initialAccount) so /account is read once. MeContext/useMe expose the user.
  • parseError → catalog (src/api/client.ts) — maps errorCodes (from i18n infrastructure (API half): validation error codes + users.language + GET/PATCH /me #45) to catalog messages, index-aligned with a per-slot English fallback (i18n.t(key, {defaultValue: serverMsg})); ApiError.message stays a single flattened string (all ~30 consumers untouched).
  • Formatting stays farm-locale-driven (§4.5) — money/date/number never read UI language; a guard test pins the invariant so a future change can't wire it in.
  • Per-user language selector — wired to PUT /me/language, hidden while English-only (renders nothing until a second pack ships).
  • Pilot — every user-facing string in Login.tsx + SalesPage.tsx externalized (111 sales keys) as the worked pattern; t() for plain, <Trans> for JSX-interleaved, t(key,{var}) for interpolation.
  • web/CONTRIBUTING-i18n.md — the t()/<Trans> convention, key naming, the formatting boundary, the singleton-for-non-render rule.

Deliberate decisions

  • SessionProvider owns the single /account read and seeds FarmProvider (not two independent providers) — this is what closes the flash window and avoids a double fetch. FarmProvider's standalone self-fetch path is preserved for its own tests.
  • Promise.allSettled, not all — a /me failure preserves a good /account (its timezone/locale drive date correctness) and vice versa; only an account-read failure surfaces FarmProvider's banner.
  • Pilot = Sales (over daily-entry) — its test already flows through renderWithProviders (the i18n wiring point) and it exercises dialogs/tables/interpolation/role-gating.
  • Coverage floors RAISED (statements 83→84, functions 77→78; lines/branches held) to the honest measured state — no locked path (src/lib/src/farm 100, src/auth, src/api/client.ts) lowered.

Out of scope (tracked follow-ups in #182)

Testing

npm run test:coverage 643 tests green; coverage 84.24% stmts / 73.91% branches / 78.27% funcs / 87.66% lines (above floors); npm run typecheck clean; npm run build succeeds. New coverage: catalog fallback, resolution chain (case-insensitive), no-flash gating + independent-settle + cancellation (observable-effect assertion), parseError code→message + index-alignment + English fallback, formatting-independence guard, hidden + multi-language selector.

Review

Reviewed per policy — plan reviewed pre-execution (codex + pi + Claude; findings folded in before any code), each of the 9 tasks task-reviewed, then a whole-branch review by two Claude agents + pi (all ready to merge; codex's run produced no usable verdict). One real (dormant) bug the reviews caught — the language selector read a stale me.language and would snap back after a switch — was fixed in this PR (current = i18n.language) with a regression test, since the selector is unreachable while English-only but would bite the moment a pack ships. Persist/switch failures now log rather than swallow.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant