Skip to content

ADR-004: Derived Translation Keys

Status

Accepted

Context

Translations lived in three hand-synchronized files: english.ts, czech.ts, and a 787-line keys.ts mapping every leaf to its dot-path. Every new key required three coordinated edits, and nothing verified the files stayed structurally identical.

Decision

english.ts is the single schema source of truth. TranslationKeys is derived from it at runtime — keys.ts recursively walks the object and replaces every leaf with its dot-path, typed via a TranslationKeyTree<typeof english> mapped type. czech.ts is structurally enforced with satisfies typeof english. Seam tests verify every derived key resolves to a string in both languages.

Codegen was rejected (build step plus a drift window between generation runs); the hand-maintained map was rejected (three files to keep in sync manually).

Consequences

  • Adding a translation is two edits (english.ts, czech.ts); the key appears on TranslationKeys automatically
  • Missing Czech entries fail compilation instead of falling back silently at runtime
  • Key leaf types widen from string literals to string — no consumer relied on literal types
  • The @/i18n/keys import path is unchanged; 134 consumer files were untouched