ADR 14: Frontend Internationalization (Lingui, Catalogs, UI Boundaries)
Status: Accepted
Last Updated: 2026-06-11 (pds-portal per ADR 42)
Context
The SaaS dashboard (ADR 08: Hosting and Frontend Stack) must support multiple locales without bloating the shared UI package or coupling design tokens to a specific i18n runtime. We need:
- Predictable, reviewable translations (not only English at runtime).
- A single source of truth for message identifiers used in Mithril components.
- Clear boundaries so
@substratum/ui-kitremains reusable and does not import Lingui.
Decision
Library and pipeline: Use Lingui in
apps/file-explorer,apps/installer-gui,apps/landing,apps/admin, andapps/pds-portal(ADR 42). Catalogs are hand-maintained JSON under each app’ssrc/locales/<locale>/messages.json(orsrc/i18n/locales/where an app uses that layout). Thelingui compilestep (Nx targetcompile-localesper app) produces ES module runtime catalogs (e.g.messages.mjswithcompileNamespace: "es") so the browser never loads CommonJS globals such asmodule.Explicit IDs, no extract-as-truth: Message keys live in per-app
message-ids.tsfiles (e.g.apps/file-explorer/src/i18n/message-ids.ts,apps/installer-gui/src/i18n/message-ids.ts). Call sites usei18n._({ id: MSG…, message: 'English default' })for developer-visible fallbacks while catalogs supply translated strings.UI kit boundary:
@substratum/ui-kitdoes not depend on Lingui. Shell chrome (navigation labels, help, language control accessibility text, etc.) receives plainstringprops from the app (e.g.Layoutshell copy andlanguagePickerAriaCopy). The app resolves strings withi18n._and passes them down.Locale set consistency: Shipped locales must match across each app’s:
lingui.config.ts(per app or aligned with repository root),supportedLocalesin that app’s i18n bootstrap,- one
messages.jsonper locale per app with the same key set within that app.
Apps:
file-explorer,installer-gui,landing,admin,pds-portal— keep locale lists aligned where product requires parity (ADR 33 §8; portal Phase 1 requiresLanguagePickerper ADR 42 NR2).Every new message ID must be added to every shipped
messages.jsonfor that app in the same change as the code (see each app’sAGENTS.md).Activation and persistence: Locale is detected from
localStorage(e.g.substratum.locale) with a browser-language fallback; switching locale reloads the compiled catalog and triggers a Mithril redraw. Route renders that depend oni18nmust not run before a locale is active (guard oni18n.localewhere needed).
Consequences
- Positive: Translators and reviewers work in JSON; IDs are stable and grep-friendly; ui-kit stays framework-agnostic; Vite + ESM catalogs avoid
module is not definedin the browser. - Negative: No
lingui extractas the workflow source of truth—each string requires updating all locale files; forgetting a locale breaks parity. Build depends on running compile (or CI) whenever catalogs change. - Operational detail: Step-by-step checklist for agents lives in
apps/file-explorer/AGENTS.md,apps/installer-gui/AGENTS.md, and (when added) the operator admin appAGENTS.md; this ADR is the architectural record only. - Operator admin UI: ADR 38 —
apps/admin: same Lingui rules and locale set; isolated app code, shared@substratum/ui-kit(ADR 33 §8). - PDS portal: ADR 42 —
apps/pds-portal: same Lingui rules, locale set,LanguagePicker, andsubstratum.localepersistence from Phase 1.
Related
- ADR 08: Hosting and Frontend Stack — Mithril + Materialize baseline.
- ADR 13: Twelve-Factor App Methodology — config and environment discipline
- ADR 33: Frontend Modular Monolith — shared ui-kit, isolated sibling apps