ADR-0002: Every user-facing string lives in both locales

  • Status: Accepted
  • Date: 2026-05-30
  • Scope: core (any frontend app shipping more than one locale)

Context

Frontend apps here ship two locales — en.json (English) and pt.json
(Portuguese pt-BR) — under src/i18n/locales/ (in Expenses: both the customer
expenses-web and admin expenses-admin apps). react-i18next falls back to the key string at runtime
when a translation is missing, so a key present in en.json but absent from
pt.json does not fail the build and is invisible in English testing. It only
surfaces as a raw key (e.g. shell.menu) to a Portuguese user in production.

Decision

Every user-facing string must:

  • be wrapped in t('namespace.key') (never hardcoded UI text), and
  • exist in both en.json and pt.json, added in the same change,
    for every app that renders it.

Validation messages are translated too: schemas are i18n-aware via
createSchema(t).

What stays literal (these are identifiers/values, not prose, and must not be
translated): UPPER_SNAKE role names (SUPER_ADMIN), plan tiers (free/pro/
pro+), environment names (dev/prod), ISO-8601 timestamps, UUIDs, and
monetary amounts (their formatting differs by locale, but the value doesn’t get
“translated”).

Consequences

  • A missing translation is a defect, caught early rather than by a user.
  • Enforced mechanically by the handbook:i18n-parity skill, which diffs the
    leaf keys of en.json vs pt.json across every app declared in the workspace
    manifest and exits non-zero on any gap — so it is part of “definition of done” and hook-able pre-merge.
  • Slightly more bookkeeping per string (two files), accepted as the cost of not
    shipping untranslated UI.