Repository navigation
i18n infrastructure (SPA foundation): react-i18next + resolution + string sweep (tracker) #182
Description
Activity
- addedsliceThin vertical work itemThin vertical work itemarea:frontendReact/Vite web clientReact/Vite web client
on Jul 24, 2026 - changed the title
[-]i18n infrastructure (SPA half): react-i18next + string externalization + language resolution[/-][+]i18n infrastructure (SPA foundation): react-i18next + resolution + string sweep (tracker)[/+]on Jul 24, 2026 - added 4 commits that reference this issue
on Jul 27, 2026 Foundation shipped: PR #233 merged to main (ff0325b) — react-i18next + typed English catalog, coordinated no-flash bootstrap,
errorCodesmapping, per-user language selector (hidden while English-only), and a Login+Sales pilot.PR #234 (Spanish + Tagalog packs, machine-drafted subset — login/sales/Account→Preferences/errors) is open and now rebased onto main (no longer stacked; conflicts resolved). Awaiting review/merge.
This issue stays open to track the remaining work:
- Full string sweep of the ~18 un-externalized screens (shared components, nav, StatusBadge/method/unit enums, a11y labels, dialogs, Help page).
- Native-speaker review of the es/tl drafts before real users see them.
String sweep — progress update (2026-07-27)
The full SPA string sweep is underway, executed against a plan that was reviewed by 5 independent lenses before execution (findings folded in). Key decision: English-first. Every screen is externalized to the English catalog now; es/tl for the swept screens is deferred to a single scheduled native-review pass (rather than machine-drafting ~1000 strings inline). Only the already-shipped translated set stays translated, and HelpPage (B6) is the one machine-drafted es/tl exception. This aligns with the #46 phasing (infra vs. translation) and avoids shipping an unreviewed "authoritative" corpus.
The sweep ships as 8 area PRs (each independently reviewed + merged by a human). Hardcoded-string count on
src/routes+src/components: 843 → 791 so far.✅ Done / merged
- Foundation (F182: i18n infrastructure (SPA foundation) — react-i18next, coordinated bootstrap, errorCodes, pilot #233) and es/tl subset packs (F182 packs: Spanish + Tagalog UI-language packs (subset, machine-drafted) — stacked on #233 #234) — earlier.
- B0 — sweep foundation (PR F182 B0: i18n sweep foundation — English-first parity, typed enums, html-lang, pilot retrofit #237, merged
c21c0b5): English-first parity allowlist + shape/non-empty guards; a typedenumsmodule (11 closed-vocabulary families, flat keys, exhaustive-union helpers so catalog↔union drift is a compile error, raw-value fallback);<html lang>follows the UI language; completecommonatoms; SalesPage pilot retrofit; annpm run i18n:scanhardcoded-string detector +CONTRIBUTING-i18n.mdupdate.
🔄 In review / in progress
- B1 — shared chrome + primitives (PR F182 B1: i18n sweep — shared chrome + primitives (nav labelKey, AppLayout, dialogs, PWA) #238, open, awaiting merge): nav (typed
labelKeymodel), AppLayout (+ new skip-link & catalogdocument.title), BottomNav, Dialog, NumberField, ErrorBoundary, ThemeToggle, useConfirm, PWA UpdatePrompt. 6 English-only namespaces; scan 843→827. - B2 — daily ops (in progress): DailyEntryPage externalized + under review (scan →791); Dashboard, WaterPage, GradesPage pending.
⏳ Pending batches
- B3 — inventory: InventoryPage, ProductsPage, StockPage, FlocksPage.
- B4 — finance & people: SettingsPage, UsersPage, ExpensesPage, CustomersPage, + the remainder of AccountPage (only its preferences block was done in i18n infrastructure (SPA foundation): react-i18next + resolution + string sweep (tracker) #182).
- B5 — records & export: HistoryPage, ReportsPage (needs new render tests — 0% covered today), AuditPage, ExportPage. Also:
auditAction+entityTypeenum displays land here (deferred from the B0 enums module — auditAction is a screen-local vocabulary, entityType isn't enumerable from SPA code). - B6 — Help page (2 PRs): prose + in-app glossary. The one machine-drafted es/tl exception, flagged pending native review + staleness-tracked.
📌 Deferred / tracked (this issue stays open for)
- Native-speaker review of es/tl for all swept screens — the scheduled translation pass English-first defers to. This is the main reason i18n infrastructure (SPA foundation): react-i18next + resolution + string sweep (tracker) #182 stays open after the English sweep completes.
- Reconcile the temporary
sales:status*duplicate into a (then-translated) sharedenums:statusduring that pass. - Optional consolidation of
pwa.reload/errorBoundary.reloadinto a sharedcommon.reloadatom if a third "Reload" site appears.
Scope boundary: "all client-authored SPA copy" excludes server-authored text — uncoded validation messages render
ApiError.message= server English by design; coded errors already map via theerrorCodespath (#45).- added 4 commits that reference this issue
on Jul 27, 2026 26 remaining items
- added a commit that references this issue
on Sep 5, 2026 Amendment — the native-speaker es/tl review is declined (owner, 2026-09-13, during the issue cleanup).
The body above ticks "Native-speaker review of es/tl for all swept screens" while the status block and epic #15 both described it as the one remaining item. That contradiction is now resolved in the other direction: the pass will not be done. The machine-drafted es + tl packs are what ships.
Consequences, stated plainly so nobody re-derives them:
- The
// machine-drafted (#182) — pending native reviewcomments across the catalog describe a permanent state, not a scheduled one. They should not be read as a TODO. - i18n: catalogParity compares key SETS, so a translation that drifts in MEANING passes every gate #688's rule still holds and still matters: help prose naming a labelled control must use that control's label word in each locale, and nothing enforces it. Declining the native pass removes the safety net that would have caught a drift like feat(web): date-range filters on audit and expenses, and the stock lot filter gets its bounded toolbar (#666, #667, #653) #678's caducidad / vencimiento, so review is now the only check.
- i18n: two es/tl wording items raised in #720 review, for the native-speaker pass #738 (two es/tl wording items raised in the Sales: snapshot the list price on the order line (discount foundation) #720 review, explicitly held "for the native-speaker pass") has no pass to wait for. It needs its own decision — fix the two items directly, or close it as won't-fix.
- Epic EPIC: Phase 1.5 — Egg product hardening #15's "First non-English UI translation" bullet has been amended to match.
The only other open item here is the optional
pwa.reload/errorBoundary.reloadconsolidation, which is a one-line cleanup waiting on a third call site.- The
Closing as completed, 2026-09-13.
What shipped
The whole sweep. B0–B6b all merged, plus the login/refresh language memory (#253) and the
sales:status*reconciliation (#254). Hardcoded strings onsrc/routes+src/componentswent 843 → 3 (the three are scanner false positives — code fragments, not strings), withindex.html's<title>Cluckwork</title>the one intentional survivor. All 29 namespaces carry en plus es/tl.catalogParityguards the key sets,npm run i18n:scanguards regressions, and the typedenumsmodule gives compile-time drift detection on the 11 closed vocabulary families.What is deliberately not done
The native-speaker es/tl review is declined (owner, 2026-09-13 — see the amendment comment above). The machine-drafted packs are what ships, and the
// machine-drafted (#182) — pending native reviewcomments describe a permanent state. The consequence for #688's help-prose rule is recorded there: review is now the only check, because nothing enforces it.#738 (two es/tl wording items held for that pass) needs its own decision and is still open.
The one remaining body item
"Optional: consolidate
pwa.reload/errorBoundary.reloadinto a sharedcommon.reloadatom if a third site appears." That is conditional on a third call site existing, and none has appeared in seven weeks. It is a note for whoever adds the third one, not scheduled work — so it does not justify holding a tier2 tracker open. Recorded here rather than lost.
Split from #45. This is the SPA i18n FOUNDATION; the API half is #45 and lands first (this needs
GET /mefor language and theerrorCodesextension to translate validation errors). Ships English-only; first pack (Spanish) is Phase 1.5 (epic #15).Part of epic #14 (Phase 1.1). Spec: §4.5, §5.2, §24, UC-012.
📊 Status (updated 2026-07-28)
✅ SWEEP COMPLETE. All batches B0–B6b shipped (#252 merged 2026-07-28) + the login/refresh language memory fix (#253 merged). The ONLY remaining work is the native-speaker es/tl review (human translation reconciliation, not code) — this tracker stays open for it.
Foundation: shipped (#233 merged) + es/tl subset packs (#234 merged). Full string sweep: COMPLETE (#252 merged). Hardcoded-string count on
src/routes+src/components: 843 → 3 (the 3 are scanner false positives — code fragments, not strings). Every client-authored SPA string is in the catalog; the only intentional survivor isindex.html's<title>Cluckwork</title>(brand).Key decision — translate-now (revised 2026-07-28). The sweep began English-first (externalize English now, defer es/tl to one native pass), but partway through we switched: every swept screen now ships machine-drafted es/tl inline, marked
// machine-drafted (#182) — pending native review, and is added toTRANSLATED_NAMESPACES. So Spanish/Tagalog mode renders translated across the app now; the scheduled native-speaker review still reconciles word choices (this issue stays open for it). Rationale for the switch: the English-first fallback left most screens showing English in Spanish mode, which read as broken.Coverage: all 29 namespaces translated (en + machine-drafted es/tl, pending native review) — the already-translated set +
enums(incl.auditAction/entityType) + B4 + B5 (#248) + the B1–B3 catch-up (#249) +helpprose (#251) +helpglossary (#252). Spanish/Tagalog mode renders translated across the entire app.Sweep plan:
docs/superpowers/plans/2026-07-27-i18n-string-sweep.md(reviewed by 5 lenses pre-execution; findings folded).Batch tracker
enumsmodule (11 closed-vocabulary families, flat keys, exhaustive-union helpers → compile-time drift, raw fallback);<html lang>sync; completecommonatoms; SalesPage pilot retrofit;npm run i18n:scandetector + CONTRIBUTING update.labelKey), AppLayout (+ skip-link & catalogdocument.title), BottomNav, Dialog, NumberField, ErrorBoundary, ThemeToggle, useConfirm, PWA UpdatePrompt. Scan 843→827.ManagerAdjusted → "Adjusted"harmonization. Scan →707.ReadOnly→"Read-only") · ExpensesPage (no method enum — category is data) · CustomersPage · remainder of AccountPage. Scan →398. es/tl backfilled for this batch (mid-batch decision):settings/users/expenses/customers+ sharedenumsmachine-drafted into es/tl and added toTRANSLATED_NAMESPACES(pending native review) — so Spanish/Tagalog mode renders translated on these screens. Enum chips now translate app-wide (bonus). B1–B3 remain English-first (es/tl still deferred to the native pass).auditAction(28) +entityType(13) enum displays authored here as typed families (deferred from B0) — friendly-labelled + translated es/tl. Scan →329. Surfaced a pre-existing audit-filter drift (server 31 codes vs client 28) → filed Audit log: action filter + entityType labels drift from server (3 actions + FarmLogo missing) #247 (not a B5 regression).TRANSLATED_NAMESPACESto 24.helpns (150 keys, 74<Trans>sites), translated inline. Scan →81.helpns (76 keys) + the catalog-wide audit (scan clean, stale English-first comments corrected, GLOSSARY pass, coverage/build). Scan →3 (false positives). Sweep code-complete: all client-authored SPA copy externalized + machine-drafted es/tl app-wide.cluckwork.langhint seeds i18next at module load so the pre-auth login screen AND a signed-in F5 open in the last-used language instead of flashing back to English — realizes the optional "persisted non-sensitive local language hint" noted in the technical approach./mestays authoritative and self-heals the hint. +regression guardbootSeed.test.ts(mutation-probed).Stays open after the English sweep for
sales:status*duplicate into a then-translated sharedenums:status. ✅ PR refactor(web): reconcile sales:status* duplicate onto shared enums:status (#182) #254, merged 2026-07-28: SalesPage's filter dropdown + voided-payment badge render viastatusLabel(); the 4sales:status*keys deleted from en/es/tl (values byte-identical toenums:status, so behavior-preserving); wiring test added; reviewed codex + 2 Claude agents + pi (unanimous approve).pwa.reload/errorBoundary.reloadinto a sharedcommon.reloadatom if a third site appears.Scope boundary: "all client-authored SPA copy" excludes server-authored text — uncoded validation messages render
ApiError.message= server English by design; coded errors already map viaerrorCodes(#45).Goal
The i18n plumbing in the SPA, so new screens add catalog keys instead of hardcoded strings, and the stored §4.5 settings finally drive display. English is the source of truth and the fallback.
ⓡ Scope of "done." The spec requires all UI strings in the catalog (specs.md:710, :2128; Sprint E retrofit). Incremental PRs are fine; incremental completion is not. So this issue is the foundation (library + resolution + convention + a representative pilot), and it stays open as the tracker for the full string sweep, which blocks Phase 1.5 (no Spanish pack can ship against half-externalized screens). The sweep may be split into numbered migration issues when scheduled.
Decisions (inherited from #45)
en-USfarm still sees$and MM/DD/YYYY.users.language(if a pack exists) → farm-locale's language subtag → English. Stored-but-unsupported = unset, never an error.GET /accountalready returnslocale(AccountResponse.Locale) and the SPAAccounttype has it — the fallback has its source, no new API dependency.Technical approach
i18next). Standard, ~11 kB gz,useTranslation/<Trans>, built-in missing-key fallback. (FormatJS heavier; hand-rolled rejected per the framework-over-handrolled rule.)common,auth,dailyEntry,sales, …). ⓡ Typed keys viaCustomTypeOptionsmodule augmentation (types/i18next.d.ts,resources: typeof en) sot("missing")is a compile error — with the accepted burden of keeping the type in sync with the catalog.t()vs<Trans>convention:t()for plain strings;<Trans>only for strings interleaving JSX. Documented in a CONTRIBUTING/DESIGN note so usage stays consistent across the sweep.ProtectedRoute's silent-refresh gate, fetch/meand/accountconcurrently, resolve the language, initialise i18next with the resolved language (NOT at module load — initialising with a language before resolution causes an English→resolved flash), and only then expose the authenticated shell. A failed read settles to farm-locale/English fallback, never a permanent blank. Note the AuthContext hazard: it marks the user authenticated immediately on token receipt (AuthContext.tsx:84), so a provider reacting later in an effect could allow one English render — the bootstrap must gate BEFORE the shell renders./loginis outsideProtectedRouteand a logged-out browser has no/me, so public/login UI is unavoidably English (or, optionally, a persisted non-sensitive local language hint for returning users). This is the honest promise; do not claim login-screen language resolution.parseError(api/client.ts) reads the API'serrorCodeswhen present, maps each code to a catalog message, and falls back to the Englisherrorsmessage at the SAME array index when a code has no key or no code was emitted. Codes are explicit-only (i18n infrastructure (API half): validation error codes + users.language + GET/PATCH /me #45), so an uncoded field simply keeps its English message.Scope (foundation) — shipped in #233
CustomTypeOptions); missing key → English (never blank or raw key)/me+/account, resolution (§4.5), i18next init with resolved language, shell gated until ready, failure → fallbackparseErrorreadserrorCodes→ catalog message, index-aligned English fallbackPUT /me/language, hidden while English-onlyt()/<Trans>, key naming, sentence casing; new screens add keys)login+ one dense screen (daily entry or sales) as the worked patternparseErrorcode→message mapping, formatting-independent-of-language, hidden-selectorFull string sweep (tracked here; blocks Phase 1.5) — see the Batch tracker above
ⓡ Not just route headings — includes shared components, navigation, status badges, accessibility labels, confirmation dialogs, client-side validation messages, error-boundary copy, and the Help page. Being externalized batch-by-batch (B0–B6 above); this issue stays open until the sweep AND the native-review es/tl pass are complete.
Out of scope
Machine-drafting es/tl inline for swept screens— superseded by the translate-now switch (see Status): every swept screen now ships machine-drafted es/tl inline. The native-speaker review of those drafts remains the open pass; a reviewed pack is Phase 1.5, epic EPIC: Phase 1.5 — Egg product hardening #15.Sequencing
After #45. Foundation shipped (#233); string sweep in follow-up area PRs (B0–B6), tracked here.