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.jsonandpt.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-parityskill, which diffs the
leaf keys ofen.jsonvspt.jsonacross 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.
Related
- 0001-three-step-git-flow
- Enforcement: skill
handbook:i18n-parity(kit/core/skills/i18n-parity/)