Internationalization (i18n)
Localize OpenPets desktop UI, pet speech, and plugins with the shared fallback model and locale contribution workflow.
Internationalization (i18n)
OpenPets localizes three distinct surfaces, each with its own mechanism but a shared principle: resolve through fallback-aware catalogs, default to English, never crash on a missing key. This doc explains all three and how to add a locale.
Source maps: apps/desktop/src/i18n/codemap.md (+ locales/, reactions/),
apps/desktop/src/codemap.md (plugin-i18n.ts).
Supported locales
The desktop host ships 8 locales (English plus seven):
| Locale | Language |
|---|---|
en |
English (base / fallback) |
ja |
Japanese |
ko |
Korean |
zh-Hans |
Chinese (Simplified) |
zh-Hant |
Chinese (Traditional) |
es-419 |
Spanish (Latin America) |
pt-BR |
Portuguese (Brazil) |
fr |
French |
The original target set is Asian + Latin American markets; French was
added later. The repo README is translated into those original six
(README.<locale>.md). The public
web site currently localizes a narrower set (English unprefixed, Chinese under
/zh; site docs are English-only) - see web/AGENTS.md; that is out of scope
for the app docs here.
Surface 1 - desktop host UI
Tray entries, Control Center labels, pet status text, and other main-process strings.
i18n/catalog.tsis the pure, dependency-free core: locale dictionaries, BCP-47 → supported-locale mapping,translate(), and brace interpolation ({name}). It’s pure so it can be unit-tested without Electron.i18n/index.tsis the Electron-aware layer:setLocaleFromPreference()maps"system"toapp.getLocale()(or honors the user’s explicit choice), andgetActiveMessages()returns the resolved dictionary.locales/<locale>.tsholds each dictionary;en.tsis the source of truth and the fallback for any missing key.- The renderer hydrates via the
openpets:get-i18nIPC hook inwindows.ts, so the React UI renders in the active locale.
Locale preference is stored in app state and coordinated through app-state.ts.
French covers every English host catalog key and all pet reaction types.
Surface 2 - pet reaction speech
What the pet says per reaction (see Pets).
i18n/reactions/<locale>.tsprovides localized message pools per reaction type;reactions/index.tsaggregates them.reaction-messages.tspicks a message from the pool for the active locale.
This keeps speech in the user’s language without coupling it to the host UI dictionary.
Surface 3 - plugins
Plugins localize their own manifest strings and runtime text.
- Manifests reference
$t:keys forname,description, and config labels/descriptions; runtime code callsctx.t(key, vars). plugin-i18n.tsloads each plugin’slocales/<locale>.json(flat, dotted keys) and resolves$t:andctx.t()with English fallback.- Every plugin must ship
locales/en.json; the release validator fails on a missing one or an unresolved$t:in a catalog card (see Testing and validation and Plugin platform).
The locale-key audit for plugins is pnpm plugins:locales
(scripts/check-plugin-locales.mjs), run as part of pnpm plugins:test.
All official plugins ship French (locales/fr.json), including manifest and
runtime text. The audit requires that file for every official plugin and checks
its keys against English, just like other shipped locales. Community plugins
are not required to ship French; their supplied locales still undergo key parity
checks.
Fallback model (all surfaces)
- Map the requested BCP-47 locale to a supported locale.
- Look up the key in that locale.
- On any miss (unknown locale, missing key), fall back to English.
- Interpolate
{var}tokens.
Nothing throws on a missing translation - it degrades to English. That is the contract; preserve it.
Adding a locale (desktop)
- Add
locales/<locale>.tsmirroring all keys inen.ts. - Add
reactions/<locale>.tswith the reaction message pools and register it inreactions/index.ts. - Ensure the BCP-47 mapping in
i18n/catalog.tsresolves the new locale. - Translate
README.<locale>.mdif you want parity with the others. - Add
locales/<locale>.jsonto plugins you want covered (English remains the fallback). Every official plugin must retain complete French coverage.
Notes & known state
- Earlier project notes treated plugin i18n as “deferred”; it now exists and is
enforced (
plugin-i18n.ts,$t:,ctx.t(),plugins:locales). Treat plugin localization as a first-class requirement, not a future. - Keep
en.ts/en.jsonauthoritative: add keys there first, then translate.
