Skip to content
OpenPets paw badgeOpenPets Docs
Esc
navigateopen⌘Jpreview
On this page

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 7 locales (English plus six):

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)

The target set is deliberately Asian + Latin American markets. The repo README is also translated into the same 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.ts is 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.ts is the Electron-aware layer: setLocaleFromPreference() maps "system" to app.getLocale() (or honors the user’s explicit choice), and getActiveMessages() returns the resolved dictionary.
  • locales/<locale>.ts holds each dictionary; en.ts is the source of truth and the fallback for any missing key.
  • The renderer hydrates via the openpets:get-i18n IPC hook in windows.ts, so the React UI renders in the active locale.

Locale preference is stored in app state and coordinated through app-state.ts.

Surface 2 - pet reaction speech

What the pet says per reaction (see Pets).

  • i18n/reactions/<locale>.ts provides localized message pools per reaction type; reactions/index.ts aggregates them.
  • reaction-messages.ts picks 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 for name, description, and config labels/descriptions; runtime code calls ctx.t(key, vars).
  • plugin-i18n.ts loads each plugin’s locales/<locale>.json (flat, dotted keys) and resolves $t: and ctx.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.

Fallback model (all surfaces)

  1. Map the requested BCP-47 locale to a supported locale.
  2. Look up the key in that locale.
  3. On any miss (unknown locale, missing key), fall back to English.
  4. Interpolate {var} tokens.

Nothing throws on a missing translation - it degrades to English. That is the contract; preserve it.

Adding a locale (desktop)

  1. Add locales/<locale>.ts mirroring all keys in en.ts.
  2. Add reactions/<locale>.ts with the reaction message pools and register it in reactions/index.ts.
  3. Ensure the BCP-47 mapping in i18n/catalog.ts resolves the new locale.
  4. Translate README.<locale>.md if you want parity with the others.
  5. For official plugins, add locales/<locale>.json to each plugin you want covered (English remains the fallback).

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.json authoritative: add keys there first, then translate.

Was this page helpful?