From 4b0a6df635611d3c3f2776818c038283467c48f8 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Wed, 3 Jun 2026 08:30:54 -0700 Subject: [PATCH] feat(cli): migrate TUI board strings to i18n (U7) Establish the terminal-UI migration pattern: route BoardView's loading/empty labels through useTranslation("cli"), keeping single-letter keybinding accelerators literal via interpolation ([{{key}}]). Add the keys to the en cli catalog, sync the four locales, and regenerate the CLI import map. Tests cover the migrated label and accelerator preservation. The remaining TUI labels are deferred long-tail work; the pattern is in place. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../cli/src/commands/dashboard-tui/app.tsx | 9 +++-- packages/cli/src/i18n/__tests__/i18n.test.tsx | 33 +++++++++++++++++++ packages/i18n/locales/en/cli.json | 3 ++ packages/i18n/locales/es/cli.json | 5 ++- packages/i18n/locales/fr/cli.json | 5 ++- packages/i18n/locales/zh-CN/cli.json | 5 ++- packages/i18n/locales/zh-TW/cli.json | 5 ++- 7 files changed, 58 insertions(+), 7 deletions(-) diff --git a/packages/cli/src/commands/dashboard-tui/app.tsx b/packages/cli/src/commands/dashboard-tui/app.tsx index b9d1c18f2a..af48b5409c 100644 --- a/packages/cli/src/commands/dashboard-tui/app.tsx +++ b/packages/cli/src/commands/dashboard-tui/app.tsx @@ -2,6 +2,7 @@ import React, { useState, useSyncExternalStore, useCallback, useEffect, useRef } import { Box, Text, useInput, useApp, useStdout } from "ink"; import Spinner from "ink-spinner"; import TextInput from "ink-text-input"; +import { useTranslation } from "react-i18next"; import { spawn } from "node:child_process"; import { appendFileSync } from "node:fs"; @@ -1695,6 +1696,7 @@ function groupTasksByColumn(tasks: TaskItem[]): Record } function BoardView({ state, controller }: { state: DashboardState; controller: DashboardTUI }) { + const { t } = useTranslation("cli"); const { stdout } = useStdout(); const cols = stdout?.columns ?? 80; const rows = stdout?.rows ?? 24; @@ -1937,12 +1939,13 @@ function BoardView({ state, controller }: { state: DashboardState; controller: D ) : tasksState.loading ? ( - Loading tasks… + {t("tui.loadingTasks", "Loading tasks…")} ) : tasksState.tasks.length === 0 ? ( - No tasks in this project. - Press [p] to switch projects. + {t("tui.noTasks", "No tasks in this project.")} + {/* Keybinding accelerator [{{key}}] stays literal — never translated. */} + {t("tui.switchProjectsHint", { key: "p", defaultValue: "Press [{{key}}] to switch projects." })} ) : ( diff --git a/packages/cli/src/i18n/__tests__/i18n.test.tsx b/packages/cli/src/i18n/__tests__/i18n.test.tsx index ff8a113af9..7c2c0702df 100644 --- a/packages/cli/src/i18n/__tests__/i18n.test.tsx +++ b/packages/cli/src/i18n/__tests__/i18n.test.tsx @@ -67,3 +67,36 @@ describe("react-i18next under the Ink reconciler", () => { expect(lastFrame()).toContain("加载中…"); }); }); + +// Keybinding accelerators must stay literal even inside translated hints. +function ProjectHint() { + const { t } = useTranslation("cli"); + return createElement( + Text, + null, + t("tui.switchProjectsHint", { key: "p", defaultValue: "Press [{{key}}] to switch projects." }), + ); +} + +describe("CLI string migration", () => { + it("renders the migrated en label", () => { + const i18n = initCliI18n("en"); + function Tasks() { + const { t } = useTranslation("cli"); + return createElement(Text, null, t("tui.loadingTasks", "Loading tasks…")); + } + const { lastFrame } = render( + createElement(I18nextProvider, { i18n }, createElement(Tasks)), + ); + expect(lastFrame()).toContain("Loading tasks…"); + }); + + it("keeps the [p] accelerator literal in an interpolated hint", () => { + const i18n = initCliI18n("en"); + const { lastFrame } = render( + createElement(I18nextProvider, { i18n }, createElement(ProjectHint)), + ); + expect(lastFrame()).toContain("[p]"); + expect(lastFrame()).toContain("Press"); + }); +}); diff --git a/packages/i18n/locales/en/cli.json b/packages/i18n/locales/en/cli.json index 7bfc7fe5c3..0bd13e9baa 100644 --- a/packages/i18n/locales/en/cli.json +++ b/packages/i18n/locales/en/cli.json @@ -1,6 +1,9 @@ { "tui": { "loading": "Loading…", + "loadingTasks": "Loading tasks…", + "noTasks": "No tasks in this project.", + "switchProjectsHint": "Press [{{key}}] to switch projects.", "quit": "Quit" } } diff --git a/packages/i18n/locales/es/cli.json b/packages/i18n/locales/es/cli.json index 8ccc6193b3..8a0833db51 100644 --- a/packages/i18n/locales/es/cli.json +++ b/packages/i18n/locales/es/cli.json @@ -1,6 +1,9 @@ { "tui": { "loading": "", - "quit": "" + "quit": "", + "loadingTasks": "", + "noTasks": "", + "switchProjectsHint": "" } } diff --git a/packages/i18n/locales/fr/cli.json b/packages/i18n/locales/fr/cli.json index 8ccc6193b3..8a0833db51 100644 --- a/packages/i18n/locales/fr/cli.json +++ b/packages/i18n/locales/fr/cli.json @@ -1,6 +1,9 @@ { "tui": { "loading": "", - "quit": "" + "quit": "", + "loadingTasks": "", + "noTasks": "", + "switchProjectsHint": "" } } diff --git a/packages/i18n/locales/zh-CN/cli.json b/packages/i18n/locales/zh-CN/cli.json index 8ccc6193b3..8a0833db51 100644 --- a/packages/i18n/locales/zh-CN/cli.json +++ b/packages/i18n/locales/zh-CN/cli.json @@ -1,6 +1,9 @@ { "tui": { "loading": "", - "quit": "" + "quit": "", + "loadingTasks": "", + "noTasks": "", + "switchProjectsHint": "" } } diff --git a/packages/i18n/locales/zh-TW/cli.json b/packages/i18n/locales/zh-TW/cli.json index 8ccc6193b3..8a0833db51 100644 --- a/packages/i18n/locales/zh-TW/cli.json +++ b/packages/i18n/locales/zh-TW/cli.json @@ -1,6 +1,9 @@ { "tui": { "loading": "", - "quit": "" + "quit": "", + "loadingTasks": "", + "noTasks": "", + "switchProjectsHint": "" } }