Skip to content

ADR-013: Frontend i18n (i18next) and locale strategy

Status: Accepted
Date: 2026-03-24
Deciders: Engineering
Related: ADR-004 (React SPA), Frontend i18n docs, Locale tooling suite

Context

CepatEdge ships a React SPA with multiple user-facing surfaces (layouts, navigation, domain pages, router error states). We need:

  • Consistent, maintainable user-visible copy without scattering English literals.
  • User language driven by /me (and GET /me/settings) settings.language, with English as the canonical fallback.
  • Room to grow: backend STRINGS / error codes mapped under errors.codes, not raw API prose.
  • Alignment between filesystem domains (src/domains/shared, auth, hod, …) and translation key hierarchy.

Decision

  1. Use i18next + react-i18next with a single default namespace (translation) and static locale bundles (apps/web/locales/*.json) for predictable builds.
  2. Fallback language is always English (en); unknown language values from the API are normalized to en in normalizeLanguageTag() (apps/web/src/i18n/config.ts).
  3. Runtime language sync: LanguageSync (apps/web/src/i18n/sync.tsx) applies user.settings.language after auth hydration; the Settings page applies language after save/load.
  4. Key naming follows a stable hierarchy (see docs/frontend/i18n/README.md and linked tooling docs for generate/sync/test):
    • common.* — shared actions and labels
    • layouts.* — shell chrome (header, footer, auth layout)
    • navigation.* — sidebar sections and nav items (machine-oriented labelKeys, not English prose in data)
    • pages.domain.<area>.<feature>.* — route-level screens
    • errors.codes.* — reserved for backend code → message mapping
  5. TypeScript key prefixes live in apps/web/src/i18n/keys.ts to reduce typos when composing t(\${PREFIX}.field`)`.

Consequences

  • Positive: One place to add languages (config + JSON + resources in i18n/index.ts); SEO-friendly document.documentElement.lang updates via setAppLanguage.
  • Positive: Navigation and recents can use labelKey so stored recents stay stable when copy changes (display is always translated at render).
  • Trade-off: Adding a language requires updating SUPPORTED_LANGUAGES, locale files, and resources (acceptable until many locales justify lazy-loading split).

Implementation references

AreaLocation
Init + setAppLanguageapps/web/src/i18n/index.ts
Supported langs + normalizationapps/web/src/i18n/config.ts
Key prefix constantsapps/web/src/i18n/keys.ts
Locale toolingdocs/frontend/i18n/*.md, apps/web/scripts/locales/
Auth language syncapps/web/src/i18n/sync.tsx
App bootstrap importapps/web/src/main.tsx (import './i18n')