From ece6cc6635117dc1e960c54e2c1c779efa774eb5 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Wed, 3 Jun 2026 08:33:34 -0700 Subject: [PATCH] feat(i18n): add machine-drafted translations and contributor docs (U8) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fill zh-CN, zh-TW, fr, and es catalogs for the migrated keys. zh-CN and zh-TW are independently localized (Simplified vs Traditional script + Taiwan vocabulary, e.g. 项目/任务 vs 專案/工作), not script-converted. Translations are machine-drafted and flagged for human review in locales/TRANSLATION_STATUS.md. Add docs/i18n-contributing.md (translate / add-a-language / --lang usage) and cross-link it from contributing.md and cli-reference.md. i18n:status reports 100% for all four locales across the migrated key set. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/cli-reference.md | 6 ++ docs/contributing.md | 9 +++ docs/i18n-contributing.md | 73 +++++++++++++++++++++ packages/i18n/locales/TRANSLATION_STATUS.md | 26 ++++++++ packages/i18n/locales/es/app.json | 6 +- packages/i18n/locales/es/cli.json | 10 +-- packages/i18n/locales/es/common.json | 18 ++--- packages/i18n/locales/es/errors.json | 4 +- packages/i18n/locales/fr/app.json | 6 +- packages/i18n/locales/fr/cli.json | 10 +-- packages/i18n/locales/fr/common.json | 18 ++--- packages/i18n/locales/fr/errors.json | 4 +- packages/i18n/locales/zh-CN/app.json | 6 +- packages/i18n/locales/zh-CN/cli.json | 10 +-- packages/i18n/locales/zh-CN/common.json | 18 ++--- packages/i18n/locales/zh-CN/errors.json | 4 +- packages/i18n/locales/zh-TW/app.json | 6 +- packages/i18n/locales/zh-TW/cli.json | 10 +-- packages/i18n/locales/zh-TW/common.json | 18 ++--- packages/i18n/locales/zh-TW/errors.json | 4 +- 20 files changed, 190 insertions(+), 76 deletions(-) create mode 100644 docs/i18n-contributing.md create mode 100644 packages/i18n/locales/TRANSLATION_STATUS.md diff --git a/docs/cli-reference.md b/docs/cli-reference.md index bf693aa305..73ac1ece03 100644 --- a/docs/cli-reference.md +++ b/docs/cli-reference.md @@ -207,8 +207,14 @@ fn dashboard --no-auth # disable bearer auth (local only) fn dashboard --interactive fn dashboard --paused fn dashboard --dev +fn dashboard --lang zh-TW # force a UI locale for this run ``` +The terminal UI is localized. `--lang ` (one of `en`, `zh-CN`, `zh-TW`, +`fr`, `es`) takes precedence over the saved dashboard language setting and the +`LC_ALL`/`LC_MESSAGES`/`LANG`/`LANGUAGE` environment. See +[Localization contributor guide](./i18n-contributing.md). + | Option | Description | |---|---| | `--port`, `-p` | Dashboard HTTP port (default `4040`). | diff --git a/docs/contributing.md b/docs/contributing.md index 9b4c27d5c5..af3f470973 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -223,6 +223,15 @@ Fusion can automatically extract insights from memory and prune transient conten See [Settings Reference](./settings-reference.md#background-memory-summarization--audit) for configuration details. +## Localization (i18n) + +User-facing strings are localized with react-i18next across both the dashboard +and the terminal UI. English is the source-of-truth; catalogs and shared config +live in the `@fusion/i18n` package. To translate, add a language, or wire new +strings, see the [Localization contributor guide](./i18n-contributing.md). When +adding user-facing copy, prefer `t("namespace:key", "English default")` over a +hardcoded string and run `pnpm i18n:extract` to register the key. + ## Dashboard CSS Organization The dashboard's CSS has been modularized: diff --git a/docs/i18n-contributing.md b/docs/i18n-contributing.md new file mode 100644 index 0000000000..14d0b28396 --- /dev/null +++ b/docs/i18n-contributing.md @@ -0,0 +1,73 @@ +# Localization (i18n) contributor guide + +Fusion's UI is localized with [react-i18next]. English (`en`) is the +source-of-truth language; everything else is a translation of it. Both UI +surfaces — the React dashboard and the Ink terminal UI — share one set of +catalogs and config in the `@fusion/i18n` package. + +## Where things live + +| Path | What it is | +| ---- | ---------- | +| `packages/i18n/locales/{locale}/{namespace}.json` | Authored catalogs (translators edit here). `en` is the source. | +| `packages/i18n/src/config.ts` | Shared i18next config: namespaces, fallback chain, plural setup. | +| `packages/core` (`SUPPORTED_LOCALES`, `Locale`) | The single list of supported locale codes. | +| `i18next.config.ts` (repo root) | `i18next-cli` workflow config. | + +Namespaces: `common` (shared), `app` (dashboard-only), `errors`, and `cli` +(terminal-only). The dashboard loads `common`/`app`/`errors`; the CLI loads +`common`/`cli`/`errors`. + +## The workflow + +All commands run from the repo root: + +```bash +pnpm i18n:extract # pull t()/ keys from source into the en catalogs +pnpm i18n:sync # propagate the en key structure to every other locale +pnpm i18n:types # regenerate key types from the en catalogs +pnpm i18n:status # per-locale completion report +pnpm i18n:lint # flag hardcoded user-facing strings +pnpm i18n:gen-cli # regenerate the CLI static catalog import map +``` + +## Translating an existing language + +1. Run `pnpm i18n:sync` so every catalog has the current `en` keys (untranslated + entries are empty strings). +2. Fill the empty strings in `packages/i18n/locales/{locale}/*.json`. +3. Keep interpolation placeholders verbatim: `{{brand}}`, `{{detail}}`, + `{{key}}`. Never translate a `[{{key}}]` keybinding accelerator — only the + words around it. +4. `pnpm i18n:status` to confirm the locale is complete. + +`zh-CN` and `zh-TW` are independent — different script **and** vocabulary. Do +not machine-convert one into the other. + +## Adding a new language (near-zero code) + +1. Add the locale code to `SUPPORTED_LOCALES` in `packages/core/src/types.ts` + and to `locales` in `i18next.config.ts`. +2. `pnpm i18n:sync` — scaffolds a full set of catalog files for the new locale + with the correct plural categories. +3. `pnpm i18n:gen-cli` — adds the locale to the CLI's static import map. +4. Translate the new catalogs, then `pnpm i18n:status` to verify. + +No feature code changes are required: the dashboard discovers the locale through +the generated `app/locales/` tree, and the CLI through the regenerated import +map. Add the language's endonym to `ENDONYMS` in +`packages/dashboard/app/components/LanguageSelector.tsx` so it appears in the +Settings switcher. + +## Using a non-English locale + +- **Dashboard / mobile:** Settings → Appearance → Language. The choice persists + to `localStorage` and to server settings. +- **Terminal UI:** resolved from `--lang ` → the saved dashboard language + setting → the `LC_ALL`/`LC_MESSAGES`/`LANG`/`LANGUAGE` environment → `en`. + + ```bash + fusion dashboard --lang zh-TW + ``` + +[react-i18next]: https://react.i18next.com/ diff --git a/packages/i18n/locales/TRANSLATION_STATUS.md b/packages/i18n/locales/TRANSLATION_STATUS.md new file mode 100644 index 0000000000..1c28c3d7fa --- /dev/null +++ b/packages/i18n/locales/TRANSLATION_STATUS.md @@ -0,0 +1,26 @@ +# Translation status + +`en` is the authored source-of-truth. The four non-English locales below were +**machine-drafted and have NOT been reviewed by a human translator.** They are +shipped so the feature is end-to-end exercisable, but each string should be +reviewed (and corrected where needed) by a fluent speaker before being treated +as production-quality. + +| Locale | Script / region | Status | +| ------ | ------------------------ | ------------------- | +| `en` | English (source) | Authored | +| `zh-CN`| Simplified Chinese | Machine-drafted ⚠️ | +| `zh-TW`| Traditional Chinese (TW) | Machine-drafted ⚠️ | +| `fr` | French | Machine-drafted ⚠️ | +| `es` | Spanish | Machine-drafted ⚠️ | + +Notes for reviewers: + +- `zh-CN` and `zh-TW` are **independent** catalogs — different script *and* + vocabulary (e.g. zh-CN 项目/任务/加载 vs zh-TW 專案/工作/載入). Do not machine-convert + one into the other. +- Preserve interpolation placeholders verbatim: `{{brand}}`, `{{detail}}`, + `{{key}}`. The `{{brand}}` token is filled at runtime so the kb→fn rename can + sweep the product name without touching catalogs. +- Keybinding accelerators inside hints (e.g. `[{{key}}]`) must stay as the + literal key — translate only the surrounding words. diff --git a/packages/i18n/locales/es/app.json b/packages/i18n/locales/es/app.json index 0b4940a283..02f8b142a8 100644 --- a/packages/i18n/locales/es/app.json +++ b/packages/i18n/locales/es/app.json @@ -1,9 +1,9 @@ { "settings": { "appearance": { - "title": "", - "language": "", - "languageHint": "" + "title": "Apariencia", + "language": "Idioma", + "languageHint": "Elige el idioma de la interfaz de {{brand}}." } } } diff --git a/packages/i18n/locales/es/cli.json b/packages/i18n/locales/es/cli.json index 8a0833db51..25440982d2 100644 --- a/packages/i18n/locales/es/cli.json +++ b/packages/i18n/locales/es/cli.json @@ -1,9 +1,9 @@ { "tui": { - "loading": "", - "quit": "", - "loadingTasks": "", - "noTasks": "", - "switchProjectsHint": "" + "loading": "Cargando…", + "loadingTasks": "Cargando tareas…", + "noTasks": "No hay tareas en este proyecto.", + "switchProjectsHint": "Pulsa [{{key}}] para cambiar de proyecto.", + "quit": "Salir" } } diff --git a/packages/i18n/locales/es/common.json b/packages/i18n/locales/es/common.json index 4593684237..d02279b2c7 100644 --- a/packages/i18n/locales/es/common.json +++ b/packages/i18n/locales/es/common.json @@ -1,15 +1,15 @@ { "columns": { - "triage": "", - "todo": "", - "in-progress": "", - "in-review": "", - "done": "", - "archived": "" + "triage": "Planificación", + "todo": "Por hacer", + "in-progress": "En progreso", + "in-review": "En revisión", + "done": "Hecho", + "archived": "Archivado" }, "actions": { - "save": "", - "cancel": "", - "close": "" + "save": "Guardar", + "cancel": "Cancelar", + "close": "Cerrar" } } diff --git a/packages/i18n/locales/es/errors.json b/packages/i18n/locales/es/errors.json index 0af69d4b5d..1c05b55ea4 100644 --- a/packages/i18n/locales/es/errors.json +++ b/packages/i18n/locales/es/errors.json @@ -1,4 +1,4 @@ { - "fetchProjectsFailed": "", - "openTaskLogsFailed": "" + "fetchProjectsFailed": "No se pudieron obtener los proyectos", + "openTaskLogsFailed": "No se pudieron abrir los registros de la tarea: {{detail}}" } diff --git a/packages/i18n/locales/fr/app.json b/packages/i18n/locales/fr/app.json index 0b4940a283..daae4e691f 100644 --- a/packages/i18n/locales/fr/app.json +++ b/packages/i18n/locales/fr/app.json @@ -1,9 +1,9 @@ { "settings": { "appearance": { - "title": "", - "language": "", - "languageHint": "" + "title": "Apparence", + "language": "Langue", + "languageHint": "Choisissez la langue de l'interface {{brand}}." } } } diff --git a/packages/i18n/locales/fr/cli.json b/packages/i18n/locales/fr/cli.json index 8a0833db51..3ee8ddda41 100644 --- a/packages/i18n/locales/fr/cli.json +++ b/packages/i18n/locales/fr/cli.json @@ -1,9 +1,9 @@ { "tui": { - "loading": "", - "quit": "", - "loadingTasks": "", - "noTasks": "", - "switchProjectsHint": "" + "loading": "Chargement…", + "loadingTasks": "Chargement des tâches…", + "noTasks": "Aucune tâche dans ce projet.", + "switchProjectsHint": "Appuyez sur [{{key}}] pour changer de projet.", + "quit": "Quitter" } } diff --git a/packages/i18n/locales/fr/common.json b/packages/i18n/locales/fr/common.json index 4593684237..20d825b18b 100644 --- a/packages/i18n/locales/fr/common.json +++ b/packages/i18n/locales/fr/common.json @@ -1,15 +1,15 @@ { "columns": { - "triage": "", - "todo": "", - "in-progress": "", - "in-review": "", - "done": "", - "archived": "" + "triage": "Planification", + "todo": "À faire", + "in-progress": "En cours", + "in-review": "En revue", + "done": "Terminé", + "archived": "Archivé" }, "actions": { - "save": "", - "cancel": "", - "close": "" + "save": "Enregistrer", + "cancel": "Annuler", + "close": "Fermer" } } diff --git a/packages/i18n/locales/fr/errors.json b/packages/i18n/locales/fr/errors.json index 0af69d4b5d..608b4e6466 100644 --- a/packages/i18n/locales/fr/errors.json +++ b/packages/i18n/locales/fr/errors.json @@ -1,4 +1,4 @@ { - "fetchProjectsFailed": "", - "openTaskLogsFailed": "" + "fetchProjectsFailed": "Échec de la récupération des projets", + "openTaskLogsFailed": "Échec de l'ouverture des journaux de tâche : {{detail}}" } diff --git a/packages/i18n/locales/zh-CN/app.json b/packages/i18n/locales/zh-CN/app.json index 0b4940a283..83ce6b3ad0 100644 --- a/packages/i18n/locales/zh-CN/app.json +++ b/packages/i18n/locales/zh-CN/app.json @@ -1,9 +1,9 @@ { "settings": { "appearance": { - "title": "", - "language": "", - "languageHint": "" + "title": "外观", + "language": "语言", + "languageHint": "选择 {{brand}} 界面的语言。" } } } diff --git a/packages/i18n/locales/zh-CN/cli.json b/packages/i18n/locales/zh-CN/cli.json index 8a0833db51..2218e2644d 100644 --- a/packages/i18n/locales/zh-CN/cli.json +++ b/packages/i18n/locales/zh-CN/cli.json @@ -1,9 +1,9 @@ { "tui": { - "loading": "", - "quit": "", - "loadingTasks": "", - "noTasks": "", - "switchProjectsHint": "" + "loading": "加载中…", + "loadingTasks": "正在加载任务…", + "noTasks": "此项目中没有任务。", + "switchProjectsHint": "按 [{{key}}] 切换项目。", + "quit": "退出" } } diff --git a/packages/i18n/locales/zh-CN/common.json b/packages/i18n/locales/zh-CN/common.json index 4593684237..5c4c3d8b6c 100644 --- a/packages/i18n/locales/zh-CN/common.json +++ b/packages/i18n/locales/zh-CN/common.json @@ -1,15 +1,15 @@ { "columns": { - "triage": "", - "todo": "", - "in-progress": "", - "in-review": "", - "done": "", - "archived": "" + "triage": "规划", + "todo": "待办", + "in-progress": "进行中", + "in-review": "审核中", + "done": "已完成", + "archived": "已归档" }, "actions": { - "save": "", - "cancel": "", - "close": "" + "save": "保存", + "cancel": "取消", + "close": "关闭" } } diff --git a/packages/i18n/locales/zh-CN/errors.json b/packages/i18n/locales/zh-CN/errors.json index 0af69d4b5d..f1c41d95fb 100644 --- a/packages/i18n/locales/zh-CN/errors.json +++ b/packages/i18n/locales/zh-CN/errors.json @@ -1,4 +1,4 @@ { - "fetchProjectsFailed": "", - "openTaskLogsFailed": "" + "fetchProjectsFailed": "获取项目失败", + "openTaskLogsFailed": "打开任务日志失败:{{detail}}" } diff --git a/packages/i18n/locales/zh-TW/app.json b/packages/i18n/locales/zh-TW/app.json index 0b4940a283..1533de30b4 100644 --- a/packages/i18n/locales/zh-TW/app.json +++ b/packages/i18n/locales/zh-TW/app.json @@ -1,9 +1,9 @@ { "settings": { "appearance": { - "title": "", - "language": "", - "languageHint": "" + "title": "外觀", + "language": "語言", + "languageHint": "選擇 {{brand}} 介面的語言。" } } } diff --git a/packages/i18n/locales/zh-TW/cli.json b/packages/i18n/locales/zh-TW/cli.json index 8a0833db51..bcfb8f4125 100644 --- a/packages/i18n/locales/zh-TW/cli.json +++ b/packages/i18n/locales/zh-TW/cli.json @@ -1,9 +1,9 @@ { "tui": { - "loading": "", - "quit": "", - "loadingTasks": "", - "noTasks": "", - "switchProjectsHint": "" + "loading": "載入中…", + "loadingTasks": "正在載入工作…", + "noTasks": "此專案中沒有工作。", + "switchProjectsHint": "按 [{{key}}] 切換專案。", + "quit": "結束" } } diff --git a/packages/i18n/locales/zh-TW/common.json b/packages/i18n/locales/zh-TW/common.json index 4593684237..ff4bfd2e39 100644 --- a/packages/i18n/locales/zh-TW/common.json +++ b/packages/i18n/locales/zh-TW/common.json @@ -1,15 +1,15 @@ { "columns": { - "triage": "", - "todo": "", - "in-progress": "", - "in-review": "", - "done": "", - "archived": "" + "triage": "規劃", + "todo": "待辦", + "in-progress": "進行中", + "in-review": "審查中", + "done": "已完成", + "archived": "已封存" }, "actions": { - "save": "", - "cancel": "", - "close": "" + "save": "儲存", + "cancel": "取消", + "close": "關閉" } } diff --git a/packages/i18n/locales/zh-TW/errors.json b/packages/i18n/locales/zh-TW/errors.json index 0af69d4b5d..963851f5a2 100644 --- a/packages/i18n/locales/zh-TW/errors.json +++ b/packages/i18n/locales/zh-TW/errors.json @@ -1,4 +1,4 @@ { - "fetchProjectsFailed": "", - "openTaskLogsFailed": "" + "fetchProjectsFailed": "擷取專案失敗", + "openTaskLogsFailed": "開啟工作記錄失敗:{{detail}}" }