# Internationalization The UI ships English and Simplified Chinese (`zh-CN`). The locale is a per-device choice, set under **Preferences → Language** on desktop and **You → Language** on mobile, and applied immediately — every component that reads through `useT()` subscribes to the locale store, so there is no reload and no "restart to apply". On first run, with no stored choice, the browser's languages decide: any `zh*` tag lands on Simplified Chinese, anything else on English. ## How it works Three files, no dependencies: | file | role | |---|---| | `src/lib/i18n.ts` | locale store (zustand + `localStorage`, same shape as `stores/sound.ts`), lookup, `{name}` interpolation | | `src/locales/en.ts` | the English catalogue — **source of truth** | | `src/locales/zh-CN.ts` | the Chinese catalogue, typed against `en` | `en` types every other locale, so a translation that misspells a key or invents one fails `tsc` rather than rendering a blank. A key a locale hasn't translated yet falls back to English, which means a partial translation renders a mixed UI instead of an empty one — partial is the normal state of a locale, not a bug. ## Using it in a component ```tsx import { useT } from '@/lib/i18n' function Greeting({ name }: { name: string }) { const t = useT() return
.ts`, keep the
`Partial>` type, translate the values.
2. Register it in `src/lib/i18n.ts`: add the code to the `Locale` union,
the dictionary to `DICTS`, and an entry to `LOCALES` (whose `label` is
the language's own name — someone looking for Chinese scans for
简体中文, not for "Chinese (Simplified)").
3. If the language should be auto-detected, extend `detectLocale()`.
The picker itself needs no change; it renders from `LOCALES`.
## Current coverage
The catalogue is fully mirrored: `zh-CN` carries every key `en` does, and
every surface reads through `useT()` / `t()` — the conversation, board,
calendar, document, agent and shipping views, the mobile screens, and the
admin console included. Components that don't render text of their own
(`Select`, `ContextMenu`, the peek panes) receive already-translated
strings as props, so there is no hidden English inside them.
That doesn't retire the English fallback: a key added to `en` renders in
English until its `zh-CN` value lands, which is why new keys should ship
with their `zh-CN` translation in the same PR to keep the mirror complete.
Server-generated text (agent replies, digests, emails) is a separate
problem: it comes from model prompts, not from this catalogue.