# 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.md](pets.md)).

- `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.md](testing-and-validation.md) and [plugins.md](plugins.md)).

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.
</content>
