Skip to content

Feat/i18n integration#112

Open
xinggitxing wants to merge 9 commits into
lessweb:mainfrom
xinggitxing:feat/i18n-integration
Open

Feat/i18n integration#112
xinggitxing wants to merge 9 commits into
lessweb:mainfrom
xinggitxing:feat/i18n-integration

Conversation

@xinggitxing
Copy link
Copy Markdown

feat(i18n): 国际化(i18n)支持

概述

为 Deep Code CLI 添加完整的国际化(i18n)支持,覆盖四个维度:UI 界面文字系统提示词模板LLM 思考语言LLM 回复语言。支持中/英双语,并允许用户独立配置 UI 语言、思考语言和回复语言。


新增特性

1. 三语言独立配置

  • locale — UI 语言 + 系统提示词模板语言
  • thinkingLocale — LLM 推理内容(reasoning_content)语言
  • replyLocale — LLM 输出内容语言

三者默认一致,均可独立设置。支持 en(英语)和 zh-CN(中文)。

2. /config 命令

新增 /config 命令,提供两级下拉菜单:

  1. 选择配置类别:UI 语言 / 思考语言 / 回复语言
  2. 选择具体语言:English / 中文

设置会持久化写入到 .deepcode/settings.json

3. 系统提示词语种感知

  • getCurrentDateAndModelPrompt() 使用当前 locale 生成日期和模型信息
  • getSystemPrompt() 自动追加思考语言和回复语言指令到 LLM system prompt
  • 支持 locale 特定的 EJS 模板

4. 全 UI 组件翻译

所有 Ink 组件中的硬编码文本均已替换为 t() 调用,覆盖:

模块 组件/文件
消息视图 MessageView index.tsx + utils.ts
输入框 PromptInput
应用层 App.tsx
欢迎页面 WelcomeScreen
退出摘要 exitSummary
加载动画 loadingText
MCP 状态 McpStatusList
斜杠命令 slashCommands
会话列表 SessionList
撤销选择器 UndoSelector
更新提示 UpdatePrompt
问题询问 AskUserQuestionPrompt
进程输出 ProcessStdoutView
CLI 帮助 cli.tsx
会话运行时 session.ts

5. CJK 字符宽度安全

新增 src/common/display-width.ts,提供 displayWidth()(参考 wcwidth 语义,CJK 计 2 列,ASCII 计 1 列)和 truncateDisplay()(视觉宽度安全截断),修复了下拉菜单中 CJK 文本被截断为"推…"的问题。

6. 翻译文件完整性检查

新增 scripts/check-i18n.mjs,通过 npm run check:i18n 验证所有 locale 文件间 key 一致性。

7. i18n-development 技能(LLM 辅助开发)

新增 .agents/skills/i18n-development/SKILL.md — 这是一个特殊的 技能定义文件,由 Deep Code CLI 自动加载。当开发者使用 Deep Code CLI 在此项目上工作时,LLM 会自动加载该技能文档,获得关于 i18n 架构的完整上下文,包括:

  • 四维度 i18n 架构:UI / 系统提示词 / 思考语言 / 回复语言
  • 三语言独立配置规则:locale / thinkingLocale / replyLocale 的作用域
  • 翻译文件组织结构:按模块拆分 JSON 的目录规范
  • 核心 API 说明initI18n()t()useI18n() 的使用方式
  • 常见陷阱清单:7 个已验证的常见错误及修复方法(模块级 t() 调用、CJK 宽度、事件传播等)

这意味着其他开发者接手 i18n 相关工作时,无需翻读代码,直接在 CLI 中提问即可获得准确的架构指导。


如何对新组件进行 i18n 化

步骤 1:添加翻译键值

locales/en/locales/zh-CN/ 目录下找到对应模块的 JSON 文件,添加新的键值对。

例如,为一个新的 StatusBar 组件添加翻译:

locales/en/ui-status-bar.json

{
  "ui": {
    "statusBar": {
      "connected": "Connected",
      "disconnected": "Disconnected",
      "latency": "Latency: {ms}ms"
    }
  }
}

locales/zh-CN/ui-status-bar.json

{
  "ui": {
    "statusBar": {
      "connected": "已连接",
      "disconnected": "未连接",
      "latency": "延迟:{ms}ms"
    }
  }
}

如果新增的页面较大,也可以将翻译直接追加到 locales/{lang}/index.json(单文件模式)。翻译文件最终会被 loadLocaleDir() 自动合并展平。

步骤 2:在代码中使用 t()

React 组件(使用 useI18n() 钩子)

import { useI18n } from "../contexts/i18n";

export function StatusBar({ latency }: { latency: number }): React.ReactElement {
  const { t } = useI18n();
  return (
    <Box>
      <Text>{t("ui.statusBar.latency", { ms: String(latency) })}</Text>
    </Box>
  );
}

非 React 模块(直接导入 t

import { t } from "../common/i18n";

export function buildStatusText(latency: number): string {
  return t("ui.statusBar.latency", { ms: String(latency) });
}

步骤 3:添加参数插值

翻译字符串中的 {变量名} 会被 t() 的第二个参数替换:

翻译值 调用 输出
"Latency: {ms}ms" t("key", { ms: "42" }) Latency: 42ms
"{count} servers" t("key", { count: 3 }) 3 servers

步骤 4:更新测试

使用了 t() 的测试文件需要在 setup 中初始化 i18n:

import { initI18n, resetI18n } from "../common/i18n";

test("status bar displays latency", () => {
  initI18n("en");
  // ... 测试逻辑
  resetI18n(); // 可选:清理状态
});

步骤 5:运行检查

npm run check:i18n     # 验证翻译 key 一致性
npm run check          # typecheck + lint + format
npm test               # 运行所有测试

注意事项

⚠️ 避免模块级 t() 调用

t() 在模块顶部被调用时,initI18n() 尚未执行,会导致返回 key 原文字符串:

// ❌ 错误 — 模块加载时调用,i18n 还未初始化
const OPTIONS = [{ label: t("ui.config.language") }];

// ✅ 正确 — 延迟到运行时调用
function getOptions() {
  return [{ label: t("ui.config.language") }];
}

⚠️ CJK 文本宽度

在计算 UI 列宽或截断字符串时,使用 displayWidth() 而非 String.length,因为 CJK 字符视觉宽度为 2 列:

import { displayWidth } from "../common/display-width";

const w = displayWidth("推理语言"); // 返回 8,而非 4

其他注意事项

  • Ink <Static>:已渲染的消息不会在 locale 切换时自动重绘,需调用 reloadActiveSessionView()
  • LLM 语言指令为软约束:语言指令会引导模型,但无法完全保证输出语言一致
  • 工具文档templates/tools/*.md 保持英文不翻译(仅面向 LLM,非用户可见)

变更文件清单

.agents/skills/i18n-development/SKILL.md  # i18n 开发技能文档(LLM 自动加载)
locales/en/index.json                    # 英语翻译
locales/zh-CN/index.json                 # 中文翻译
scripts/check-i18n.mjs                   # 翻译 key 一致性检查
src/common/i18n.ts                       # 核心 i18n 模块
src/common/display-width.ts              # CJK 宽度安全工具
src/settings.ts                          # 设置中增加 locale 配置
src/prompt.ts                            # 系统提示词语种感知
src/session.ts                           # 会话运行时 i18n
src/cli.tsx                              # CLI 入口 i18n 初始化
src/ui/App.tsx                           # locale 切换回调
src/ui/AppContainer.tsx                   # I18nProvider 集成
src/ui/contexts/i18n.tsx                 # React i18n context
src/ui/components/ConfigDropdown/index.tsx # /config 命令组件
src/ui/WelcomeScreen.tsx                 # 欢迎页翻译
src/ui/exitSummary.ts                    # 退出摘要翻译
src/ui/loadingText.ts                    # 加载文本翻译
src/ui/McpStatusList.tsx                 # MCP 状态翻译
src/ui/ProcessStdoutView.tsx             # 进程输出翻译
src/ui/PromptInput.tsx                   # 输入框翻译
src/ui/SessionList.tsx                   # 会话列表翻译
src/ui/UndoSelector.tsx                  # 撤销选择器翻译
src/ui/UpdatePrompt.tsx                  # 更新提示翻译
src/ui/AskUserQuestionPrompt.tsx         # 问题询问翻译
src/ui/DropdownMenu.tsx                  # 下拉菜单 CJK 修复
src/ui/SlashCommandMenu.tsx              # 斜杠菜单 CJK 修复
src/ui/slashCommands.ts                  # 斜杠命令翻译
src/ui/components/MessageView/index.tsx  # 消息视图翻译
src/ui/components/MessageView/utils.ts   # 消息工具 CJK 修复
eslint.config.mjs                        # JSON 导入支持
package.json                             # locales/ 列入发布文件

检查清单

  • npm run check 通过
  • npm test 通过
  • npm run check:i18n 通过
  • enzh-CN 翻译文件 key 一致
  • 所有硬编码 UI 字符串已替换
  • 自动 locale 检测(LANG 环境变量)
  • /config 命令持久化设置

Phase 2 of the i18n implementation. Replaces hardcoded English
strings across all UI modules with i18n t() calls:

- MessageView (index.tsx + utils.ts): thinking/loadedSkill/
  conversationSummary/changes/plan/result/toolName/reasoningFallback/
  noContent/imageAttachment labels
- loadingText: Thinking... placeholder and elapsed format
- exitSummary: Goodbye header and table column labels
- WelcomeScreen: shortcut tips descriptions and title
- slashCommands: all built-in command descriptions
- cli.tsx: --help text via cli.help.* keys
- McpStatusList: status labels (ready/failed/connecting/reconnecting)
- i18n.ts: fix getExtensionRoot for tsx test environments (2 levels up);
  change TranslationKey to string for dot-notation support
- Tests: add initI18n('en') calls to tests that use t()

15 files changed, 135 insertions(+), 92 deletions(-)
Phase 3 of the i18n implementation. Makes system prompt generation
and session runtime messages locale-aware:

- prompt.ts: import t/getThinkingLocale/getReplyLocale;
  getCurrentDateAndModelPrompt() uses t('prompt.dateAndModel');
  getDefaultSkillPrompt() uses t('prompt.skillDocumentsHeader');
  getSystemPrompt() appends thinking and reply language instructions
  using the configured thinkingLocale and replyLocale
- session.ts: import t; replace 'compacting'/'Interrupted.'/
  'Killed processes:'/'Failed to kill processes:' with t() calls
- Tests: add initI18n('en') to prompt.test.ts and session.test.ts;
  update assertion patterns from Chinese to English text

4 files changed, 15 insertions(+), 11 deletions(-)
Phase 4 of the i18n implementation. Adds runtime locale configuration:

- src/ui/components/ConfigDropdown/index.tsx: two-step dropdown for
  selecting UI Language / Thinking Language / Reply Language
- src/ui/slashCommands.ts: register /config as a built-in command
- src/ui/PromptInput.tsx: integrate ConfigDropdown with /config routing
- src/ui/App.tsx: locale change handlers with settings.json persistence
  (readSettings + writeSettings)
- UseI18n context + global setThinkingLocale/setReplyLocale for
  three-locale state management

5 files changed, 217 insertions(+), 2 deletions(-)
Add 96 new translation keys (236 total) and replace hardcoded strings
with t() calls across 12 component files:

- ModelsDropdown, RawModelDropdown (incl. RAW_COMMAND_MODELS labels),
  SkillsDropdown, FileMentionMenu, DropdownMenu, SlashCommandMenu
- McpStatusList, SessionList, UndoSelector, ProcessStdoutView
  (complemented missing translations)
- Added initI18n to test files for formatSessionStatus/image count
Split locales/{lang}/index.json into 18 individual JSON files:
- One file per UI component (ui-message-view, ui-prompt-input, etc.)
- Grouped small dropdowns into ui-dropdowns.json
- Grouped undo-related into ui-undo.json
- Session, prompt, cli-help remain separate

Updated check-i18n.mjs to read all *.json files from locale dirs
instead of just index.json.

i18n.ts loadLocaleDir() already supports multi-file loading.
- Show current locale value next to each category name (e.g. 'Language (en)')
- Translate locale option labels ('English'/'中文') via t() lookup
- Replace hardcoded 'current' with t('ui.config.currentLabel')
- Remove unused useMemo import (fixes lint warning)
- Add 4 new translation keys: currentLabel, localeEn, localeZhCN, categoryWithValue
  (240 total keys)
- After selecting a locale, ConfigDropdown returns to category selection
  instead of closing (onClose was called immediately)
- Shows status message in footer (e.g. 'Language: English')
  via onStatusMessage prop wired to PromptInput's setStatusMessage
- Added 3 translation keys: languageUpdated, thinkingLanguageUpdated,
  replyLanguageUpdated (243 total)
- WelcomeScreen tips already fully translated, no changes needed
…odule-level t() calls

WelcomeScreen.SHORTCUT_TIPS was defined at module scope, so t() ran
before initI18n() (ESM import order), returning key strings like
"ui.welcome.pasteImage" instead of translated text.

Convert SHORTCUT_TIPS from a module-level const array to a lazy
getShortcutTips() function called at render time.

Also update the i18n-development SKILL.md to:
- Add the WelcomeScreen real-world case to Pitfall lessweb#1
- Document the "tests pass but UI shows raw keys" subtle trap in Pitfall lessweb#5
- Expand audit commands for detecting module-level t() calls
@xinggitxing xinggitxing force-pushed the feat/i18n-integration branch from f3fa694 to f0ea78c Compare May 23, 2026 10:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant