Skip to content

Internationalization

Draft

Every user-facing string in the apps, the web client and the server’s errors is authored once in the repo-root locales/ directory and compiled into each platform’s native localization format. Translators never touch application code. The one exception is the CLI and TUI, which are English-only.

PieceRole
locales/Canonical ICU MessageFormat catalogs and config.json, the supported-locale set
xtask i18nCompiles the catalogs into every platform’s files; --check is the drift gate in check-rust
capsule-i18nRust runtime: locale negotiation, message formatting, error_codes constants
Generated filesWeb JSON, Android strings.xml, Apple .xcstrings; committed, with a do-not-edit banner
  • locales/config.json is the closed set of supported locales: sourceLocale, supportedLocales and per-locale fallbacks. A locale ships exactly when its catalog lands and its tag enters supportedLocales.
  • locales/<locale>.json maps a key to an ICU message. en.json is the source; every key is defined there first, with the translator context. A translation entry carries its origin instead.
  • Every catalog defines every source key. A key a translation lacks still renders, through the English fallback, so coverage is restored by backfilling rather than enforced by the build.
  • Keys use dotted namespaces (area.subarea.name), named by surface, not by client: a concept present on several platforms takes no platform prefix, because the Android and Apple catalogs are generated from the same keys.

The committed language set is English plus zh-Hans, zh-Hant, ja, ko, fr, de, es, pt-BR, it, ru, hi and ar. Arabic puts RTL layout in scope for every app. Fallbacks go straight to English: zh-Hant never falls back to zh-Hans.

Final for every app. The user’s language and their region are two separate settings:

SettingControlsExample
LanguageWhich catalog renders UI text; text directionzh-Hant-HK (Hong Kong Traditional Chinese)
RegionFormatting: dates, times, numbers, first weekday, measurement units, calendarGB (British formatting)
  • Both default from the operating system. Where the platform offers a per-app language (iOS, Android 13+), the app uses it rather than an in-app picker; elsewhere the app offers one.
  • Language selection uses the platform’s own negotiation against supportedLocales, falling back by the catalog’s fallbacks. Region never changes which strings are shown, and language never changes how a date is formatted.
  • Formatting uses each platform’s native formatters with the region; catalog messages pass raw values (dates, counts) as arguments rather than pre-formatted text.
  • Neither setting is synced to the server; they are device preferences.

mise run i18n compiles every target; generated files are deterministic and committed, and mise run i18n-check fails on drift. Apple and Android compile ICU plurals ahead of time to their native plural forms. The Rust runtime (capsule-i18n) has no platform ICU underneath, so it carries a small interpreter for literals, {name} interpolation and plural with CLDR integer rules; constructs outside that subset (select, number/date skeletons, offset:) are refused rather than mis-rendered.

  • The server attaches a stable code from the catalog’s error.* namespace (for example error.auth.invalid_credentials) to every structured rejection, alongside an English detail. Clients switch on the code, never the HTTP status or the detail (Rejection Mapping).
  • Clients localize the code through their generated catalog; the detail stays English.
  • The server names codes through the generated capsule_i18n::error_codes constants, so a typo is a compile error. Error codes are catalog keys; there is no second list.
  • The server never translates by Accept-Language: localization is client-side, which keeps it offline-capable and decouples the user’s language from their server.

The CLI and TUI are not localized and will stay English-only. They are the reference client for operators and power users, and every string they add would otherwise cost thirteen catalog entries for an audience that reads English. They render the shared error.* codes through the English catalog, and their own strings are written inline. The apps own localization.

README.md ships as README.<lang>.md for every non-source locale in config.json. xtask translate-readme segments the README by block, takes each block’s translation from xtask/translations/readme/<locale>.json (source/target pairs), and refuses a translation that alters a link target, inline code or a glossary term. translate-readme --check fails when a committed translation no longer matches the README’s structure.

Translators edit locales/<locale>.json and open a pull request; locales/README.md documents the format and how to add a language.

Each translation entry records who produced it, so a reviewed string is always distinguishable from one awaiting review.

originMeaning
machineA machine translation no fluent reader has reviewed
humanWritten, or reviewed and accepted, by a fluent reader
  • origin is required on every translation entry and refused on the source catalog, which is authored rather than translated. A reviewer who accepts or rewrites an entry sets human.
  • The contributor and the date are the git history of the entry, not catalog fields: a second copy in the file would drift from the commit that is the record.
  • The repository is the system of record. No hosted translation platform (Weblate, Crowdin) owns provenance; one adopted later edits these files through pull requests and keeps origin, so the record stays in git.
  • Codegen is a pure function of the catalogs; i18n-check compares committed output to a fresh render.
  • Every translation carries exactly its source message’s arguments: the same {name} set, and a whole-message plural exactly where the source has one, on the same selector. Order is free and plural arms are the target language’s own. A dropped, added or retyped argument, or a key the source lacks, fails generation and so i18n-check.
  • Negotiation, interpolation, fallback and missing keys are unit-tested against fixed vectors; the CLDR plural table is asserted at boundary counts.
  • Every plural key in every catalog is rendered at boundary counts and asserted to contain no ICU syntax, and to match the sentence the generated Android resource produces.