MusePi i18n Architecture
| English | 中文 |
Living document (established 2026-08-16) — covers the two independent i18n systems in this repo: the renderer side (desktop-web, GUI/tray/collab Web) and the TUI (coding-agent). The vocabulary was split into per-domain modules on 2026-08-16, with compile-level parity on the en side and a plugin registration seam. The implementation is authoritative.
The Two Systems
desktop-web (packages/desktop-web/src/i18n) |
coding-agent (packages/coding-agent/src/i18n) |
|
|---|---|---|
| Consumers | GUI main window/tray/bubble, collab Web UI | TUI, daemon (reuses the same module) |
| Placeholders | Named parameters {count} (validated via template literal type extraction) |
Positional parameters {0} (legacy convention, not migrated) |
| Key typing | Strict TranslationKey = keyof typeof zhCN (typos/missing params are compile errors) |
Loose (key: string, English source text is the key) |
| en mapping | Yes (en-US/ domain files, compile-level parity) |
None (English passthrough) |
| Vocabulary | 12 domains (zh-CN/ + en-US/) |
13 domains (zh-CN/) |
| Plugin seam | registerTranslations(locale, map) + tLoose(key, params) |
registerTranslations(locale, map) |
Shared conventions: the key is the English source text, and t() falls back to the key itself on a miss; t() runs only at render/call time, never at module load.
Vocabulary Split (desktop-web)
- Domain modules:
zh-CN/{shell,composer,sessions,context,collab,transcript,settings,agents,tools,pet,guest}.ts(each domain exportsXxxKey = keyof typeof x), with an isomorphicen-US/tree. - Barrels (
zh-CN/index.ts/en-US/index.ts) merge everything into flat maps and throw at module load on cross-domain duplicate keys (a replacement for silent spread overrides); the TUI’szh-CN/index.tshas the same guard. - en compile-level parity: each en domain file uses
export const x = { … } as const satisfies Record<ZhKey, string>— a missing or extra en key is a compile error (negative verification: injecting an extra key into shell.ts → TS2353). Stronger than tests; every new zh key must be accompanied by en. - Tests:
desktop-web/src/i18n/i18n.test.ts(key-set parity, cross-domain duplicate guard, registration override/isolation, new locales) +desktop-web/test/i18n.test.ts(lookup/replacement/fallback/passthrough/no leftover positional parameters). Tests callingsetLocalemust restore the initial value inafterAll(bun test runs files sequentially in one process).
Plugin Seam
registerTranslations(locale, map): registers/overrides copy for a locale at runtime. The desktop-web version triggersemit()so the UI re-renders immediately; the TUI version has no subscribers —t()reads the registry on every call, so it is naturally immediate. Neither persists (restarts revert to the core vocabulary).tLoose(key, params)(desktop-web only): for plugin-owned keys not in the core vocabulary (coretonly acceptsTranslationKey; the TUI’stnever restricted keys in the first place).- Brand-new locales are supported (e.g. a plugin registering
fr-FR); unregistered keys for that locale fall back to the raw key text.
Maintenance Guide
- Editing copy: find the domain file matching the feature area (settings is largest at 2137 keys, then tools/pet); do not merge back into a single file.
- Adding a key: after adding it to a zh domain, the en domain must be updated in sync (otherwise a compile error); keep keys in English source-text style; use named parameters like
{name}, not{0}. - No duplicate keys across domains: duplicates throw when the barrel loads (the error names both domains).
- GUI dynamic keys (schema-driven labels, runtime error strings,
tag.${…}concatenation): use explicitas TranslationKey; plugin/extension copy goes throughregisterTranslations+tLoose. - TUI: leave
{0}positional parameters untouched; when adding a new domain module, update the import/spread/guard parts ofzh-CN/index.tsaccordingly.