Internationalization
DraftEvery 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.
| Piece | Role |
|---|---|
locales/ | Canonical ICU MessageFormat catalogs and config.json, the supported-locale set |
xtask i18n | Compiles the catalogs into every platform’s files; --check is the drift gate in check-rust |
capsule-i18n | Rust runtime: locale negotiation, message formatting, error_codes constants |
| Generated files | Web JSON, Android strings.xml, Apple .xcstrings; committed, with a do-not-edit banner |
Catalogs
Section titled “Catalogs”locales/config.jsonis the closed set of supported locales:sourceLocale,supportedLocalesand per-localefallbacks. A locale ships exactly when its catalog lands and its tag enterssupportedLocales.locales/<locale>.jsonmaps a key to an ICUmessage.en.jsonis the source; every key is defined there first, with the translatorcontext. A translation entry carries itsorigininstead.- 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.
Language and Region
Section titled “Language and Region”Final for every app. The user’s language and their region are two separate settings:
| Setting | Controls | Example |
|---|---|---|
| Language | Which catalog renders UI text; text direction | zh-Hant-HK (Hong Kong Traditional Chinese) |
| Region | Formatting: dates, times, numbers, first weekday, measurement units, calendar | GB (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.
Codegen and Runtime
Section titled “Codegen and Runtime”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.
Server Error Codes
Section titled “Server Error Codes”- The server attaches a stable code from the catalog’s
error.*namespace (for exampleerror.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_codesconstants, 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 Is English-Only
Section titled “The CLI Is English-Only”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 Translation
Section titled “README Translation”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.
Contributing Translations
Section titled “Contributing Translations”Translators edit locales/<locale>.json and open a pull request; locales/README.md documents
the format and how to add a language.
Translation Provenance
Section titled “Translation Provenance”Each translation entry records who produced it, so a reviewed string is always distinguishable from one awaiting review.
origin | Meaning |
|---|---|
machine | A machine translation no fluent reader has reviewed |
human | Written, or reviewed and accepted, by a fluent reader |
originis 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 setshuman.- 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.
Validation
Section titled “Validation”- Codegen is a pure function of the catalogs;
i18n-checkcompares 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 soi18n-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.