feat(i18n): add @fusion/i18n package, catalogs, and i18next-cli tooling (U2)
Create the @fusion/i18n package as the authored source-of-truth: shared i18next config (namespace split, script-aware zh-CN/zh-TW fallback, plural setup), en base catalogs, and a generated CLI static-import map so the terminal surface is drop-in for new locales. Add the i18next-cli workflow (extract/sync/types/status/lint) wired as root i18n:* scripts, install the i18next stack into dashboard + CLI, strip @fusion/i18n from the published CLI manifest, gitignore the generated dashboard catalog tree, and add a changeset. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
9
.changeset/i18n-localization-foundation.md
Normal file
9
.changeset/i18n-localization-foundation.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Add a localization (i18n) foundation across the UI. Introduces react-i18next-backed translation for both the dashboard and the terminal UI, with English as the source language and Simplified Chinese, Traditional Chinese, French, and Spanish as target locales.
|
||||||
|
|
||||||
|
- New `@fusion/i18n` package holding the authored catalogs and shared i18next configuration (namespace split, script-aware zh-CN/zh-TW fallback, plural setup).
|
||||||
|
- A `language` preference (`fusion settings`) and a Settings language switcher; the CLI resolves locale from `--lang`, settings, then environment.
|
||||||
|
- An `i18next-cli` workflow (`extract`/`sync`/`types`/`status`/`lint`) so adding a future language is a translate-only, near-zero-code operation.
|
||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -11,6 +11,10 @@ dist/
|
|||||||
dist-electron/
|
dist-electron/
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Generated i18n catalogs synced into the dashboard tree from @fusion/i18n
|
||||||
|
# (authored source-of-truth lives in packages/i18n/locales/)
|
||||||
|
packages/dashboard/app/locales/
|
||||||
|
|
||||||
# Desktop packaging artifacts
|
# Desktop packaging artifacts
|
||||||
*.dmg
|
*.dmg
|
||||||
*.dmg.blockmap
|
*.dmg.blockmap
|
||||||
|
|||||||
46
i18next.config.ts
Normal file
46
i18next.config.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import {
|
||||||
|
defineConfig,
|
||||||
|
recommendedAcceptedAttributes,
|
||||||
|
recommendedAcceptedTags,
|
||||||
|
} from "i18next-cli";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* i18next-cli workflow config for the whole monorepo.
|
||||||
|
*
|
||||||
|
* - `extract` pulls t()/<Trans> keys from the dashboard and CLI source into the
|
||||||
|
* authored `en` catalogs under @fusion/i18n.
|
||||||
|
* - `sync` propagates the `en` key structure to the four other locales.
|
||||||
|
* - `types` regenerates key types from the `en` catalogs.
|
||||||
|
* - `status` reports per-locale completion (CI gate).
|
||||||
|
* - `lint` flags hardcoded user-facing strings (primary guardrail).
|
||||||
|
*
|
||||||
|
* Namespaces are routed by the `ns:` prefix in keys / `useTranslation(ns)` in
|
||||||
|
* source, not by file path. `common` is the default namespace.
|
||||||
|
*/
|
||||||
|
export default defineConfig({
|
||||||
|
locales: ["en", "zh-CN", "zh-TW", "fr", "es"],
|
||||||
|
extract: {
|
||||||
|
input: [
|
||||||
|
"packages/dashboard/app/**/*.{ts,tsx}",
|
||||||
|
"packages/cli/src/**/*.{ts,tsx}",
|
||||||
|
"!**/__tests__/**",
|
||||||
|
"!**/*.test.*",
|
||||||
|
],
|
||||||
|
output: "packages/i18n/locales/{{language}}/{{namespace}}.json",
|
||||||
|
primaryLanguage: "en",
|
||||||
|
defaultNS: "common",
|
||||||
|
keySeparator: ".",
|
||||||
|
nsSeparator: ":",
|
||||||
|
// Untranslated secondary-locale keys stay empty so `status` can measure
|
||||||
|
// real completion; the active locale falls back to `en` at runtime.
|
||||||
|
defaultValue: "",
|
||||||
|
},
|
||||||
|
types: {
|
||||||
|
input: ["packages/i18n/locales/en/*.json"],
|
||||||
|
output: "packages/i18n/src/i18next-resources.d.ts",
|
||||||
|
},
|
||||||
|
lint: {
|
||||||
|
acceptedTags: recommendedAcceptedTags,
|
||||||
|
acceptedAttributes: recommendedAcceptedAttributes,
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -46,6 +46,12 @@
|
|||||||
"test:coverage:dashboard": "pnpm --filter @fusion/dashboard exec vitest run --silent=passed-only --reporter=dot --coverage",
|
"test:coverage:dashboard": "pnpm --filter @fusion/dashboard exec vitest run --silent=passed-only --reporter=dot --coverage",
|
||||||
"test:slow-cli": "pnpm --filter @runfusion/fusion test:slow-cli",
|
"test:slow-cli": "pnpm --filter @runfusion/fusion test:slow-cli",
|
||||||
"typecheck": "pnpm -r --filter=!@fusion/desktop --filter=!@fusion/mobile typecheck",
|
"typecheck": "pnpm -r --filter=!@fusion/desktop --filter=!@fusion/mobile typecheck",
|
||||||
|
"i18n:extract": "i18next-cli extract",
|
||||||
|
"i18n:sync": "i18next-cli sync",
|
||||||
|
"i18n:types": "i18next-cli types",
|
||||||
|
"i18n:status": "i18next-cli status",
|
||||||
|
"i18n:lint": "i18next-cli lint",
|
||||||
|
"i18n:gen-cli": "pnpm --filter @fusion/i18n gen:cli-catalogs",
|
||||||
"changeset": "changeset",
|
"changeset": "changeset",
|
||||||
"version": "changeset version",
|
"version": "changeset version",
|
||||||
"release": "node scripts/release.mjs",
|
"release": "node scripts/release.mjs",
|
||||||
@@ -76,6 +82,7 @@
|
|||||||
"@eslint/js": "^9.0.0",
|
"@eslint/js": "^9.0.0",
|
||||||
"eslint": "^9.0.0",
|
"eslint": "^9.0.0",
|
||||||
"fast-glob": "^3.3.3",
|
"fast-glob": "^3.3.3",
|
||||||
|
"i18next-cli": "^1.59.1",
|
||||||
"react-devtools-core": "^7.0.1",
|
"react-devtools-core": "^7.0.1",
|
||||||
"tsx": "^4.19.0",
|
"tsx": "^4.19.0",
|
||||||
"typescript": "^5.7.0",
|
"typescript": "^5.7.0",
|
||||||
|
|||||||
@@ -59,13 +59,15 @@
|
|||||||
"@earendil-works/pi-coding-agent": "^0.78.0",
|
"@earendil-works/pi-coding-agent": "^0.78.0",
|
||||||
"dockerode": "^4.0.12",
|
"dockerode": "^4.0.12",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
|
"i18next": "^26.3.1",
|
||||||
"ink": "^6.8.0",
|
"ink": "^6.8.0",
|
||||||
"ink-spinner": "^5.0.0",
|
"ink-spinner": "^5.0.0",
|
||||||
"ink-text-input": "^6.0.0",
|
"ink-text-input": "^6.0.0",
|
||||||
"ioredis": "^5.6.0",
|
"ioredis": "^5.6.0",
|
||||||
"multer": "^2.1.1",
|
"multer": "^2.1.1",
|
||||||
"node-pty": "npm:@homebridge/node-pty-prebuilt-multiarch@^0.13.1",
|
"node-pty": "npm:@homebridge/node-pty-prebuilt-multiarch@^0.13.1",
|
||||||
"react": "^19.0.0"
|
"react": "^19.0.0",
|
||||||
|
"react-i18next": "^17.0.8"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@earendil-works/pi-ai": "*",
|
"@earendil-works/pi-ai": "*",
|
||||||
@@ -87,6 +89,7 @@
|
|||||||
"@fusion/core": "workspace:*",
|
"@fusion/core": "workspace:*",
|
||||||
"@fusion/dashboard": "workspace:*",
|
"@fusion/dashboard": "workspace:*",
|
||||||
"@fusion/engine": "workspace:*",
|
"@fusion/engine": "workspace:*",
|
||||||
|
"@fusion/i18n": "workspace:*",
|
||||||
"@fusion/pi-claude-cli": "workspace:*",
|
"@fusion/pi-claude-cli": "workspace:*",
|
||||||
"@fusion/pi-llama-cpp": "workspace:*",
|
"@fusion/pi-llama-cpp": "workspace:*",
|
||||||
"@types/node": "^22.0.0",
|
"@types/node": "^22.0.0",
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export function applyPrepackTransform(pkg) {
|
|||||||
delete devDependencies["@fusion/core"];
|
delete devDependencies["@fusion/core"];
|
||||||
delete devDependencies["@fusion/dashboard"];
|
delete devDependencies["@fusion/dashboard"];
|
||||||
delete devDependencies["@fusion/engine"];
|
delete devDependencies["@fusion/engine"];
|
||||||
|
delete devDependencies["@fusion/i18n"];
|
||||||
delete devDependencies["@fusion/pi-claude-cli"];
|
delete devDependencies["@fusion/pi-claude-cli"];
|
||||||
delete devDependencies["@fusion/pi-llama-cpp"];
|
delete devDependencies["@fusion/pi-llama-cpp"];
|
||||||
delete devDependencies["@fusion-plugin-examples/roadmap"];
|
delete devDependencies["@fusion-plugin-examples/roadmap"];
|
||||||
|
|||||||
@@ -90,17 +90,18 @@
|
|||||||
"@codemirror/state": "^6.5.2",
|
"@codemirror/state": "^6.5.2",
|
||||||
"@codemirror/theme-one-dark": "^6.1.2",
|
"@codemirror/theme-one-dark": "^6.1.2",
|
||||||
"@codemirror/view": "^6.36.4",
|
"@codemirror/view": "^6.36.4",
|
||||||
|
"@earendil-works/pi-coding-agent": "^0.78.0",
|
||||||
|
"@fusion-plugin-examples/cli-printing-press": "workspace:*",
|
||||||
|
"@fusion-plugin-examples/cursor-runtime": "workspace:*",
|
||||||
"@fusion-plugin-examples/dependency-graph": "workspace:*",
|
"@fusion-plugin-examples/dependency-graph": "workspace:*",
|
||||||
"@fusion-plugin-examples/roadmap": "workspace:*",
|
"@fusion-plugin-examples/droid-runtime": "workspace:*",
|
||||||
"@fusion-plugin-examples/hermes-runtime": "workspace:*",
|
"@fusion-plugin-examples/hermes-runtime": "workspace:*",
|
||||||
"@fusion-plugin-examples/openclaw-runtime": "workspace:*",
|
"@fusion-plugin-examples/openclaw-runtime": "workspace:*",
|
||||||
"@fusion-plugin-examples/droid-runtime": "workspace:*",
|
|
||||||
"@fusion-plugin-examples/cursor-runtime": "workspace:*",
|
|
||||||
"@fusion-plugin-examples/cli-printing-press": "workspace:*",
|
|
||||||
"@fusion-plugin-examples/paperclip-runtime": "workspace:*",
|
"@fusion-plugin-examples/paperclip-runtime": "workspace:*",
|
||||||
|
"@fusion-plugin-examples/roadmap": "workspace:*",
|
||||||
"@fusion/core": "workspace:*",
|
"@fusion/core": "workspace:*",
|
||||||
"@fusion/engine": "workspace:*",
|
"@fusion/engine": "workspace:*",
|
||||||
"@earendil-works/pi-coding-agent": "^0.78.0",
|
"@fusion/i18n": "workspace:*",
|
||||||
"@types/multer": "^2.1.0",
|
"@types/multer": "^2.1.0",
|
||||||
"@xterm/addon-fit": "^0.10.0",
|
"@xterm/addon-fit": "^0.10.0",
|
||||||
"@xterm/addon-search": "^0.15.0",
|
"@xterm/addon-search": "^0.15.0",
|
||||||
@@ -109,6 +110,9 @@
|
|||||||
"@xterm/xterm": "^5.5.0",
|
"@xterm/xterm": "^5.5.0",
|
||||||
"archiver": "^7.0.1",
|
"archiver": "^7.0.1",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
|
"i18next": "^26.3.1",
|
||||||
|
"i18next-browser-languagedetector": "^8.2.1",
|
||||||
|
"i18next-resources-to-backend": "^1.2.1",
|
||||||
"ioredis": "^5.6.0",
|
"ioredis": "^5.6.0",
|
||||||
"lucide-react": "^1.7.0",
|
"lucide-react": "^1.7.0",
|
||||||
"multer": "^2.1.1",
|
"multer": "^2.1.1",
|
||||||
@@ -116,6 +120,7 @@
|
|||||||
"qrcode": "^1.5.4",
|
"qrcode": "^1.5.4",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
|
"react-i18next": "^17.0.8",
|
||||||
"react-markdown": "^10.1.0",
|
"react-markdown": "^10.1.0",
|
||||||
"remark-gfm": "^4.0.1",
|
"remark-gfm": "^4.0.1",
|
||||||
"ws": "^8.18.0",
|
"ws": "^8.18.0",
|
||||||
|
|||||||
9
packages/i18n/locales/en/app.json
Normal file
9
packages/i18n/locales/en/app.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"settings": {
|
||||||
|
"appearance": {
|
||||||
|
"title": "Appearance",
|
||||||
|
"language": "Language",
|
||||||
|
"languageHint": "Choose the language for the {{brand}} interface."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
6
packages/i18n/locales/en/cli.json
Normal file
6
packages/i18n/locales/en/cli.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"tui": {
|
||||||
|
"loading": "Loading…",
|
||||||
|
"quit": "Quit"
|
||||||
|
}
|
||||||
|
}
|
||||||
15
packages/i18n/locales/en/common.json
Normal file
15
packages/i18n/locales/en/common.json
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"columns": {
|
||||||
|
"triage": "Planning",
|
||||||
|
"todo": "Todo",
|
||||||
|
"in-progress": "In Progress",
|
||||||
|
"in-review": "In Review",
|
||||||
|
"done": "Done",
|
||||||
|
"archived": "Archived"
|
||||||
|
},
|
||||||
|
"actions": {
|
||||||
|
"save": "Save",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"close": "Close"
|
||||||
|
}
|
||||||
|
}
|
||||||
4
packages/i18n/locales/en/errors.json
Normal file
4
packages/i18n/locales/en/errors.json
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"fetchProjectsFailed": "Failed to fetch projects",
|
||||||
|
"openTaskLogsFailed": "Failed to open task logs: {{detail}}"
|
||||||
|
}
|
||||||
9
packages/i18n/locales/es/app.json
Normal file
9
packages/i18n/locales/es/app.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"settings": {
|
||||||
|
"appearance": {
|
||||||
|
"title": "",
|
||||||
|
"language": "",
|
||||||
|
"languageHint": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
6
packages/i18n/locales/es/cli.json
Normal file
6
packages/i18n/locales/es/cli.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"tui": {
|
||||||
|
"loading": "",
|
||||||
|
"quit": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
15
packages/i18n/locales/es/common.json
Normal file
15
packages/i18n/locales/es/common.json
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"columns": {
|
||||||
|
"triage": "",
|
||||||
|
"todo": "",
|
||||||
|
"in-progress": "",
|
||||||
|
"in-review": "",
|
||||||
|
"done": "",
|
||||||
|
"archived": ""
|
||||||
|
},
|
||||||
|
"actions": {
|
||||||
|
"save": "",
|
||||||
|
"cancel": "",
|
||||||
|
"close": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
4
packages/i18n/locales/es/errors.json
Normal file
4
packages/i18n/locales/es/errors.json
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"fetchProjectsFailed": "",
|
||||||
|
"openTaskLogsFailed": ""
|
||||||
|
}
|
||||||
9
packages/i18n/locales/fr/app.json
Normal file
9
packages/i18n/locales/fr/app.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"settings": {
|
||||||
|
"appearance": {
|
||||||
|
"title": "",
|
||||||
|
"language": "",
|
||||||
|
"languageHint": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
6
packages/i18n/locales/fr/cli.json
Normal file
6
packages/i18n/locales/fr/cli.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"tui": {
|
||||||
|
"loading": "",
|
||||||
|
"quit": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
15
packages/i18n/locales/fr/common.json
Normal file
15
packages/i18n/locales/fr/common.json
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"columns": {
|
||||||
|
"triage": "",
|
||||||
|
"todo": "",
|
||||||
|
"in-progress": "",
|
||||||
|
"in-review": "",
|
||||||
|
"done": "",
|
||||||
|
"archived": ""
|
||||||
|
},
|
||||||
|
"actions": {
|
||||||
|
"save": "",
|
||||||
|
"cancel": "",
|
||||||
|
"close": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
4
packages/i18n/locales/fr/errors.json
Normal file
4
packages/i18n/locales/fr/errors.json
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"fetchProjectsFailed": "",
|
||||||
|
"openTaskLogsFailed": ""
|
||||||
|
}
|
||||||
9
packages/i18n/locales/zh-CN/app.json
Normal file
9
packages/i18n/locales/zh-CN/app.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"settings": {
|
||||||
|
"appearance": {
|
||||||
|
"title": "",
|
||||||
|
"language": "",
|
||||||
|
"languageHint": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
6
packages/i18n/locales/zh-CN/cli.json
Normal file
6
packages/i18n/locales/zh-CN/cli.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"tui": {
|
||||||
|
"loading": "",
|
||||||
|
"quit": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
15
packages/i18n/locales/zh-CN/common.json
Normal file
15
packages/i18n/locales/zh-CN/common.json
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"columns": {
|
||||||
|
"triage": "",
|
||||||
|
"todo": "",
|
||||||
|
"in-progress": "",
|
||||||
|
"in-review": "",
|
||||||
|
"done": "",
|
||||||
|
"archived": ""
|
||||||
|
},
|
||||||
|
"actions": {
|
||||||
|
"save": "",
|
||||||
|
"cancel": "",
|
||||||
|
"close": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
4
packages/i18n/locales/zh-CN/errors.json
Normal file
4
packages/i18n/locales/zh-CN/errors.json
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"fetchProjectsFailed": "",
|
||||||
|
"openTaskLogsFailed": ""
|
||||||
|
}
|
||||||
9
packages/i18n/locales/zh-TW/app.json
Normal file
9
packages/i18n/locales/zh-TW/app.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"settings": {
|
||||||
|
"appearance": {
|
||||||
|
"title": "",
|
||||||
|
"language": "",
|
||||||
|
"languageHint": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
6
packages/i18n/locales/zh-TW/cli.json
Normal file
6
packages/i18n/locales/zh-TW/cli.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"tui": {
|
||||||
|
"loading": "",
|
||||||
|
"quit": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
15
packages/i18n/locales/zh-TW/common.json
Normal file
15
packages/i18n/locales/zh-TW/common.json
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"columns": {
|
||||||
|
"triage": "",
|
||||||
|
"todo": "",
|
||||||
|
"in-progress": "",
|
||||||
|
"in-review": "",
|
||||||
|
"done": "",
|
||||||
|
"archived": ""
|
||||||
|
},
|
||||||
|
"actions": {
|
||||||
|
"save": "",
|
||||||
|
"cancel": "",
|
||||||
|
"close": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
4
packages/i18n/locales/zh-TW/errors.json
Normal file
4
packages/i18n/locales/zh-TW/errors.json
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"fetchProjectsFailed": "",
|
||||||
|
"openTaskLogsFailed": ""
|
||||||
|
}
|
||||||
37
packages/i18n/package.json
Normal file
37
packages/i18n/package.json
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"name": "@fusion/i18n",
|
||||||
|
"version": "0.39.0",
|
||||||
|
"license": "MIT",
|
||||||
|
"description": "Fusion i18n: authored translation catalogs and shared i18next configuration for the Fusion dashboard and terminal UI.",
|
||||||
|
"type": "module",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./src/index.ts",
|
||||||
|
"source": "./src/index.ts",
|
||||||
|
"import": "./src/index.ts"
|
||||||
|
},
|
||||||
|
"./config": {
|
||||||
|
"types": "./src/config.ts",
|
||||||
|
"source": "./src/config.ts",
|
||||||
|
"import": "./src/config.ts"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"typecheck": "tsc --noEmit",
|
||||||
|
"gen:cli-catalogs": "node scripts/gen-cli-catalogs.mjs",
|
||||||
|
"test": "vitest run --silent=passed-only --reporter=dot"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@fusion/core": "workspace:*",
|
||||||
|
"i18next": "^26.3.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^25.5.0",
|
||||||
|
"typescript": "^5.7.0",
|
||||||
|
"vitest": "^3.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=22.5.0"
|
||||||
|
},
|
||||||
|
"private": true
|
||||||
|
}
|
||||||
49
packages/i18n/scripts/gen-cli-catalogs.mjs
Normal file
49
packages/i18n/scripts/gen-cli-catalogs.mjs
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
// Generates src/cli-catalogs.ts: a static import map of the CLI-relevant
|
||||||
|
// catalogs for every locale present under locales/. The terminal UI bundles
|
||||||
|
// catalogs statically (tsup, no lazy loading), so it needs an explicit import
|
||||||
|
// map rather than a dynamic loader. Driving this off the locales/ directory
|
||||||
|
// listing keeps "add a language" a no-code operation on the CLI side: add the
|
||||||
|
// locale (via `i18next-cli sync`), regenerate, done.
|
||||||
|
import { readdirSync, writeFileSync } from "node:fs";
|
||||||
|
import { dirname, join } from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
|
const here = dirname(fileURLToPath(import.meta.url));
|
||||||
|
const root = join(here, "..");
|
||||||
|
const localesDir = join(root, "locales");
|
||||||
|
|
||||||
|
// Keep in sync with CLI_NAMESPACES in src/config.ts.
|
||||||
|
const CLI_NAMESPACES = ["common", "cli", "errors"];
|
||||||
|
|
||||||
|
const locales = readdirSync(localesDir, { withFileTypes: true })
|
||||||
|
.filter((d) => d.isDirectory())
|
||||||
|
.map((d) => d.name)
|
||||||
|
.sort();
|
||||||
|
|
||||||
|
const ident = (s) => s.replace(/[^a-zA-Z0-9]/g, "_");
|
||||||
|
|
||||||
|
const imports = [];
|
||||||
|
const entries = [];
|
||||||
|
for (const lng of locales) {
|
||||||
|
const nsLines = [];
|
||||||
|
for (const ns of CLI_NAMESPACES) {
|
||||||
|
const id = `${ident(lng)}_${ns}`;
|
||||||
|
imports.push(`import ${id} from "../locales/${lng}/${ns}.json";`);
|
||||||
|
nsLines.push(` ${ns}: ${id},`);
|
||||||
|
}
|
||||||
|
entries.push(` "${lng}": {\n${nsLines.join("\n")}\n },`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const out = [
|
||||||
|
"// GENERATED by scripts/gen-cli-catalogs.mjs — do not edit by hand.",
|
||||||
|
"// Run `pnpm --filter @fusion/i18n gen:cli-catalogs` to regenerate.",
|
||||||
|
...imports,
|
||||||
|
"",
|
||||||
|
"export const cliResources = {",
|
||||||
|
...entries,
|
||||||
|
"} as const;",
|
||||||
|
"",
|
||||||
|
].join("\n");
|
||||||
|
|
||||||
|
writeFileSync(join(root, "src", "cli-catalogs.ts"), out);
|
||||||
|
console.log(`Generated cli-catalogs.ts for ${locales.length} locale(s): ${locales.join(", ")}`);
|
||||||
51
packages/i18n/src/__tests__/config.test.ts
Normal file
51
packages/i18n/src/__tests__/config.test.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import { SUPPORTED_LOCALES } from "@fusion/core";
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { cliResources } from "../cli-catalogs.js";
|
||||||
|
import {
|
||||||
|
baseInitOptions,
|
||||||
|
CLI_NAMESPACES,
|
||||||
|
DASHBOARD_NAMESPACES,
|
||||||
|
DEFAULT_NAMESPACE,
|
||||||
|
FALLBACK_LNG,
|
||||||
|
NAMESPACES,
|
||||||
|
} from "../config.js";
|
||||||
|
|
||||||
|
describe("@fusion/i18n config", () => {
|
||||||
|
it("dashboard and cli namespaces are subsets of NAMESPACES", () => {
|
||||||
|
for (const ns of [...DASHBOARD_NAMESPACES, ...CLI_NAMESPACES]) {
|
||||||
|
expect(NAMESPACES).toContain(ns);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("defaults to the common namespace", () => {
|
||||||
|
expect(DEFAULT_NAMESPACE).toBe("common");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps zh-CN and zh-TW separate (load: currentOnly)", () => {
|
||||||
|
const opts = baseInitOptions();
|
||||||
|
expect(opts.load).toBe("currentOnly");
|
||||||
|
expect(opts.supportedLngs).toEqual([...SUPPORTED_LOCALES]);
|
||||||
|
expect(opts.interpolation?.escapeValue).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("routes Chinese scripts and defaults everything else to en", () => {
|
||||||
|
expect(FALLBACK_LNG.zh).toEqual(["zh-CN"]);
|
||||||
|
expect(FALLBACK_LNG["zh-Hans"]).toEqual(["zh-CN"]);
|
||||||
|
expect(FALLBACK_LNG["zh-Hant"]).toEqual(["zh-TW"]);
|
||||||
|
expect(FALLBACK_LNG.default).toEqual(["en"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("ships a CLI catalog map for every supported locale and namespace", () => {
|
||||||
|
for (const lng of SUPPORTED_LOCALES) {
|
||||||
|
expect(cliResources).toHaveProperty(lng);
|
||||||
|
for (const ns of CLI_NAMESPACES) {
|
||||||
|
expect(cliResources[lng]).toHaveProperty(ns);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("has real en content (catalogs wired, not empty)", () => {
|
||||||
|
expect(cliResources.en.cli).toMatchObject({ tui: { loading: expect.any(String) } });
|
||||||
|
expect(cliResources.en.common).toMatchObject({ columns: { done: "Done" } });
|
||||||
|
});
|
||||||
|
});
|
||||||
45
packages/i18n/src/cli-catalogs.ts
Normal file
45
packages/i18n/src/cli-catalogs.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
// GENERATED by scripts/gen-cli-catalogs.mjs — do not edit by hand.
|
||||||
|
// Run `pnpm --filter @fusion/i18n gen:cli-catalogs` to regenerate.
|
||||||
|
import en_common from "../locales/en/common.json";
|
||||||
|
import en_cli from "../locales/en/cli.json";
|
||||||
|
import en_errors from "../locales/en/errors.json";
|
||||||
|
import es_common from "../locales/es/common.json";
|
||||||
|
import es_cli from "../locales/es/cli.json";
|
||||||
|
import es_errors from "../locales/es/errors.json";
|
||||||
|
import fr_common from "../locales/fr/common.json";
|
||||||
|
import fr_cli from "../locales/fr/cli.json";
|
||||||
|
import fr_errors from "../locales/fr/errors.json";
|
||||||
|
import zh_CN_common from "../locales/zh-CN/common.json";
|
||||||
|
import zh_CN_cli from "../locales/zh-CN/cli.json";
|
||||||
|
import zh_CN_errors from "../locales/zh-CN/errors.json";
|
||||||
|
import zh_TW_common from "../locales/zh-TW/common.json";
|
||||||
|
import zh_TW_cli from "../locales/zh-TW/cli.json";
|
||||||
|
import zh_TW_errors from "../locales/zh-TW/errors.json";
|
||||||
|
|
||||||
|
export const cliResources = {
|
||||||
|
"en": {
|
||||||
|
common: en_common,
|
||||||
|
cli: en_cli,
|
||||||
|
errors: en_errors,
|
||||||
|
},
|
||||||
|
"es": {
|
||||||
|
common: es_common,
|
||||||
|
cli: es_cli,
|
||||||
|
errors: es_errors,
|
||||||
|
},
|
||||||
|
"fr": {
|
||||||
|
common: fr_common,
|
||||||
|
cli: fr_cli,
|
||||||
|
errors: fr_errors,
|
||||||
|
},
|
||||||
|
"zh-CN": {
|
||||||
|
common: zh_CN_common,
|
||||||
|
cli: zh_CN_cli,
|
||||||
|
errors: zh_CN_errors,
|
||||||
|
},
|
||||||
|
"zh-TW": {
|
||||||
|
common: zh_TW_common,
|
||||||
|
cli: zh_TW_cli,
|
||||||
|
errors: zh_TW_errors,
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
54
packages/i18n/src/config.ts
Normal file
54
packages/i18n/src/config.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import { DEFAULT_LOCALE, SUPPORTED_LOCALES } from "@fusion/core";
|
||||||
|
import type { FallbackLngObjList, InitOptions } from "i18next";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shared, framework-agnostic i18next configuration for both Fusion UI surfaces.
|
||||||
|
*
|
||||||
|
* The dashboard (browser) and the terminal UI (Node) each build their own
|
||||||
|
* i18next instance, but they share the locale list, namespace split, fallback
|
||||||
|
* chain, and base options defined here so the two surfaces stay consistent.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** All translation namespaces. Split so each surface loads only what it needs. */
|
||||||
|
export const NAMESPACES = ["common", "app", "errors", "cli"] as const;
|
||||||
|
export type Namespace = (typeof NAMESPACES)[number];
|
||||||
|
|
||||||
|
/** Default namespace keys resolve against when none is specified. */
|
||||||
|
export const DEFAULT_NAMESPACE: Namespace = "common";
|
||||||
|
|
||||||
|
/** Namespaces the browser dashboard loads (skips the terminal-only `cli`). */
|
||||||
|
export const DASHBOARD_NAMESPACES: readonly Namespace[] = ["common", "app", "errors"];
|
||||||
|
|
||||||
|
/** Namespaces the terminal UI loads (skips the dashboard-only `app`). */
|
||||||
|
export const CLI_NAMESPACES: readonly Namespace[] = ["common", "cli", "errors"];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Script-aware fallback chain. A generic `zh` resolves to Simplified, the
|
||||||
|
* Han-script tags resolve to their region catalog, and everything else falls
|
||||||
|
* back to the source language. Combined with `load: "currentOnly"` this keeps
|
||||||
|
* `zh-CN` and `zh-TW` from ever collapsing into a single generic `zh`.
|
||||||
|
*/
|
||||||
|
export const FALLBACK_LNG: FallbackLngObjList = {
|
||||||
|
"zh-Hans": ["zh-CN"],
|
||||||
|
"zh-Hant": ["zh-TW"],
|
||||||
|
zh: ["zh-CN"],
|
||||||
|
default: [DEFAULT_LOCALE],
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base init options shared by every surface. Each surface spreads these and
|
||||||
|
* adds its own resource-loading strategy (lazy backend for the dashboard,
|
||||||
|
* static `resources` for the CLI) plus framework plugins.
|
||||||
|
*/
|
||||||
|
export function baseInitOptions(): InitOptions {
|
||||||
|
return {
|
||||||
|
supportedLngs: [...SUPPORTED_LOCALES],
|
||||||
|
fallbackLng: FALLBACK_LNG,
|
||||||
|
// Never collapse zh-CN/zh-TW into a generic `zh`.
|
||||||
|
load: "currentOnly",
|
||||||
|
nonExplicitSupportedLngs: false,
|
||||||
|
// React (and Ink) escape on render; double-escaping mangles output.
|
||||||
|
interpolation: { escapeValue: false },
|
||||||
|
returnNull: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
3
packages/i18n/src/index.ts
Normal file
3
packages/i18n/src/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export * from "./config.js";
|
||||||
|
export { cliResources } from "./cli-catalogs.js";
|
||||||
|
export type { Locale } from "@fusion/core";
|
||||||
11
packages/i18n/tsconfig.json
Normal file
11
packages/i18n/tsconfig.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"noEmit": true,
|
||||||
|
"rootDir": ".",
|
||||||
|
"types": ["node", "vitest/globals"],
|
||||||
|
"resolveJsonModule": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*", "locales/**/*.json"],
|
||||||
|
"exclude": ["src/**/*.test.ts"]
|
||||||
|
}
|
||||||
14
packages/i18n/vitest.config.ts
Normal file
14
packages/i18n/vitest.config.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { resolve } from "node:path";
|
||||||
|
import { defineConfig } from "vitest/config";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
// Use @fusion/core's TypeScript source so tests don't require a dist build.
|
||||||
|
"@fusion/core": resolve(__dirname, "../core/src/index.ts"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
test: {
|
||||||
|
include: ["src/**/*.test.ts"],
|
||||||
|
},
|
||||||
|
});
|
||||||
929
pnpm-lock.yaml
generated
929
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user