feat(i18n): add machine-drafted translations and contributor docs (U8)
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) <noreply@anthropic.com>
This commit is contained in:
@@ -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 <code>` (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`). |
|
||||
|
||||
@@ -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:
|
||||
|
||||
73
docs/i18n-contributing.md
Normal file
73
docs/i18n-contributing.md
Normal file
@@ -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()/<Trans> 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 <code>` → 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/
|
||||
26
packages/i18n/locales/TRANSLATION_STATUS.md
Normal file
26
packages/i18n/locales/TRANSLATION_STATUS.md
Normal file
@@ -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.
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"settings": {
|
||||
"appearance": {
|
||||
"title": "",
|
||||
"language": "",
|
||||
"languageHint": ""
|
||||
"title": "Apariencia",
|
||||
"language": "Idioma",
|
||||
"languageHint": "Elige el idioma de la interfaz de {{brand}}."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"fetchProjectsFailed": "",
|
||||
"openTaskLogsFailed": ""
|
||||
"fetchProjectsFailed": "No se pudieron obtener los proyectos",
|
||||
"openTaskLogsFailed": "No se pudieron abrir los registros de la tarea: {{detail}}"
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"settings": {
|
||||
"appearance": {
|
||||
"title": "",
|
||||
"language": "",
|
||||
"languageHint": ""
|
||||
"title": "Apparence",
|
||||
"language": "Langue",
|
||||
"languageHint": "Choisissez la langue de l'interface {{brand}}."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}}"
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"settings": {
|
||||
"appearance": {
|
||||
"title": "",
|
||||
"language": "",
|
||||
"languageHint": ""
|
||||
"title": "外观",
|
||||
"language": "语言",
|
||||
"languageHint": "选择 {{brand}} 界面的语言。"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"tui": {
|
||||
"loading": "",
|
||||
"quit": "",
|
||||
"loadingTasks": "",
|
||||
"noTasks": "",
|
||||
"switchProjectsHint": ""
|
||||
"loading": "加载中…",
|
||||
"loadingTasks": "正在加载任务…",
|
||||
"noTasks": "此项目中没有任务。",
|
||||
"switchProjectsHint": "按 [{{key}}] 切换项目。",
|
||||
"quit": "退出"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": "关闭"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"fetchProjectsFailed": "",
|
||||
"openTaskLogsFailed": ""
|
||||
"fetchProjectsFailed": "获取项目失败",
|
||||
"openTaskLogsFailed": "打开任务日志失败:{{detail}}"
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"settings": {
|
||||
"appearance": {
|
||||
"title": "",
|
||||
"language": "",
|
||||
"languageHint": ""
|
||||
"title": "外觀",
|
||||
"language": "語言",
|
||||
"languageHint": "選擇 {{brand}} 介面的語言。"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"tui": {
|
||||
"loading": "",
|
||||
"quit": "",
|
||||
"loadingTasks": "",
|
||||
"noTasks": "",
|
||||
"switchProjectsHint": ""
|
||||
"loading": "載入中…",
|
||||
"loadingTasks": "正在載入工作…",
|
||||
"noTasks": "此專案中沒有工作。",
|
||||
"switchProjectsHint": "按 [{{key}}] 切換專案。",
|
||||
"quit": "結束"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": "關閉"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"fetchProjectsFailed": "",
|
||||
"openTaskLogsFailed": ""
|
||||
"fetchProjectsFailed": "擷取專案失敗",
|
||||
"openTaskLogsFailed": "開啟工作記錄失敗:{{detail}}"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user