Skip to content

Commit 75ed2b9

Browse files
committed
feat(i18n): complete i18n for all command secondary pages
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
1 parent dfaee9f commit 75ed2b9

30 files changed

Lines changed: 1207 additions & 343 deletions

File tree

.agents/skills/i18n-development/SKILL.md

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,3 +175,102 @@ All i18n changes have negligible performance impact:
175175
3. **LLM output is a soft constraint**: Language instructions guide the LLM but cannot guarantee compliance. Most models follow reliably.
176176
4. **`TranslationKey` type**: Must match keys in all `en/*.json` files. Auto-derived via `import type` + `keyof typeof`.
177177
5. **Tool docs**: `templates/tools/*.md` stay in English (sent to LLM, not user-facing).
178+
179+
## Common Pitfalls
180+
181+
### 1. 🚫 Module-Level `t()` Calls (i18n Not Yet Initialized)
182+
183+
**Problem**: `t()` called at module scope evaluates BEFORE `initI18n()` runs (ESM import resolution order). The translation cache is empty, so `t()` returns the key string itself.
184+
185+
```typescript
186+
// ❌ WRONG — evaluates at module load time
187+
const OPTIONS = [{ label: t("ui.config.language") }]; // → "ui.config.language"
188+
```
189+
190+
**Fix**: Move `t()` into functions called at render time:
191+
192+
```typescript
193+
// ✅ CORRECT — lazy evaluation after initI18n()
194+
function getOptions() {
195+
return [{ label: t("ui.config.language") }];
196+
}
197+
// Or use map inside a render-time function:
198+
export function buildCommands() {
199+
return DEFS.map(d => ({ ...d, desc: t(d.key) }));
200+
}
201+
```
202+
203+
**Audit**: `rg -n '^\w.*t\("' src/ --include='*.ts' --include='*.tsx' | grep -v test` — no matches expected.
204+
205+
### 2. 🚫 Missing `t` Import
206+
207+
**Problem**: File uses `t("...")` without importing it.
208+
209+
```typescript
210+
// ❌ WRONG — missing import
211+
export function buildExitSummaryText() { return t("ui.exitSummary.goodbye"); }
212+
```
213+
214+
**Fix**: Always add `import { t } from "../common/i18n"` at the top.
215+
216+
**Audit**: `rg -l 't\("' src/ --include='*.ts' --include='*.tsx' | xargs grep -L 'import.*i18n' | grep -v tests/`
217+
218+
### 3. 🚫 Duplicate `t` Import
219+
220+
**Rule**: React components → `const { t } = useI18n()`. Non-React modules → `import { t } from "../common/i18n"`. Never both in the same file.
221+
222+
### 4. 🚫 Ink `useInput` Event Propagation Without Guards
223+
224+
**Problem**: Ink delivers keyboard events to ALL active `useInput` hooks. When a dropdown is open, Enter triggers both the dropdown's action AND the parent's submit.
225+
226+
```typescript
227+
// ❌ WRONG — showConfigDropdown missing
228+
if (openRawModelDropdown || showSkillsDropdown || showModelDropdown) { return; }
229+
submitCurrentBuffer(); // fires while ConfigDropdown is open!
230+
```
231+
232+
**Fix**: Include ALL dropdown states in the guard:
233+
234+
```typescript
235+
// ✅ CORRECT
236+
if (openRawModelDropdown || showSkillsDropdown || showModelDropdown || showConfigDropdown) { return; }
237+
```
238+
239+
### 5. 🚫 Test Fixtures Without `initI18n`
240+
241+
Tests calling functions using `t()` must call `initI18n("en")` first, otherwise `t()` returns key strings.
242+
243+
### 6. 🚫 Translation Key Naming Mismatch
244+
245+
Run `npm run check:i18n` before PR. Also audit key usage:
246+
```bash
247+
node -e "see i18n-todo.md for full audit script"
248+
```
249+
250+
### 7. 🚫 CJK 字符视觉宽度被 `String.length` 低估
251+
252+
**Problem**: CJK 字符(中文、日文、韩文)每个占 2 列视觉宽度,但 `String.length` 计为 1。使用 `.length` 计算 UI 列宽/截断位置会导致:
253+
- 列宽低估 → Dropdown 选项被 `wrap="truncate-end"` 截断(如 "推理语言" → "推…")
254+
- 表格 padding 不足 → 内容偏移
255+
256+
```typescript
257+
// ❌ WRONG — "推理语言".length = 4, 但视觉宽 = 8
258+
width += item.label.length;
259+
```
260+
261+
**Fix**: 使用 `displayWidth()` 替代 `String.length``src/common/display-width.ts`):
262+
263+
```typescript
264+
import { displayWidth } from "../common/display-width";
265+
width += displayWidth(item.label); // "推理语言" → 8 ✅
266+
```
267+
268+
`displayWidth()` 对 CJK/全角/emoji 计 2 列,ASCII 计 1 列。
269+
270+
**受影响的组件及状态**
271+
272+
| 文件 | 原始代码 | 修复方式 | 状态 |
273+
|------|---------|---------|------|
274+
| `DropdownMenu.tsx:89` | `item.label.length` | `displayWidth(item.label)` | ✅ 已修复 |
275+
| `SlashCommandMenu.tsx:29` | `s.label.length` | `displayWidth(s.label)` | ✅ 已修复 |
276+
| `exitSummary.ts:13` | `visibleLength()` 仅去 ANSI | `displayWidth()` | 📌 待定(仅视觉偏移) |

.deepcode/i18n-plan.md

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,40 @@
1-
# i18n 支持方案 v7(经过第6轮 Review — 最终版)
2-
1+
# i18n 支持方案 v7(经过第8轮 Review — 最终版)
2+
3+
> **Review 8 发现与修正(CJK 视觉宽度与布局偏移 — 2026-05-23)**:
4+
> 1. **🟡 DropdownMenu 列宽低估 CJK 字符**`item.label.length` 将中文 "推理语言" 计为 4 列,但视觉占 8 列,导致 `labelColumnWidth` 低估 → `wrap="truncate-end"` 截断为 "推…"
5+
> 2. **🟡 SlashCommandMenu 同样问题**`s.label.length` 低估中文技能名的视觉宽度,同上
6+
> 3. **🟡 exitSummary 表格偏移**`visibleLength()` 仅去 ANSI 码未处理 CJK 宽度,zh-CN 下表头/数据右偏数列(不影响功能)
7+
> 4. **🟢 新增解决方案**:创建 `src/common/display-width.ts``displayWidth()` 函数,CJK/全角/emoji 计 2 列,ASCII 计 1 列
8+
> 5. **🟢 修复 DropdownMenu**`item.label.length``displayWidth(item.label)`
9+
> 6. **🟢 修复 SlashCommandMenu**`s.label.length``displayWidth(s.label)`
10+
> 7. **📌 待定**`exitSummary.ts:visibleLength()` 可选改为 `displayWidth()` 修复表格对齐
11+
>
12+
> **Review 7 发现与修正(代码审查 + Key 使用审计 — 2026-05-22)**:
13+
> 1. **🟡 死代码**`i18n.ts:getExtensionRoot()` 第 38 行不可达(多余的 `return` 语句),应删除
14+
> 2. **🟡 McpStatusList 视图比较**`viewMode === t("ui.mcp.serverDetail")` 用翻译字符串做状态比较,locale 切换时会失效;应使用固定字符串
15+
> 3. **🟡 遗漏 `t()` 调用**`session.ts:activateSession()` 中 4 处运行时消息仍为硬编码英文(failReason "OpenAI API key not found"、apiKeyNotFound 消息、sessionAgentSteps 提示、requestFailed 拼接),虽然 JSON 中已定义对应 key
16+
> 4. **🟡 遗漏 `t()` 调用**`App.tsx:handleModelConfigChange()` 中 modelUnchanged/modelUpdated/noActiveSession/codeRestoreFailed/conversationRestoreFailed 等仍为硬编码
17+
> 5. **🟡 遗漏 `t()` 调用**`cli.tsx` 第 84 行非 TTY 错误消息未翻译
18+
> 6. **🟡 大量翻译 Key 未在源代码中调用**:扫查发现 `en/index.json` 中有约 35 个 key 未在代码中被 `t()` 引用(详见下方 Key 使用审计)
19+
> 7. **🟡 Key 使用审计结果**
20+
> - 已定义且使用的 key:105 个(75%)
21+
> - 已定义但未使用的 key:35 个(25%)— 详见 `i18n-todo.md` 的 Key 使用审计表格
22+
> - 代码中调用但未定义的 key:0 个(所有 `t()` 调用均指向有效 key)
23+
> 8. **🟡 未使用的 Key 详细清单**
24+
> - `ui.app.error/statusStatus/statusTokens/statusFail` — App.tsx 中硬编码的状态行/错误行
25+
> - `ui.app.modelUnchanged/modelUpdated/noActiveSession/codeRestoreFailed/conversationRestoreFailed` — App.tsx handleModelConfigChange/handleUndoRestore 硬编码
26+
> - `ui.app.sessionDefaultSummary/sessionAgentSteps/apiKeyNotFound/requestFailed` — session.ts 硬编码
27+
> - `ui.config.languageUpdated/thinkingLanguageUpdated/replyLanguageUpdated` — 未在任何 t() 中调用
28+
> - `ui.welcome.deepCodeTitle` — WelcomeScreen 未使用该 key
29+
> - `ui.mcp.serverList/statusConnecting` — McpStatusList 硬编码
30+
> - `ui.slashCommands.continueDesc` — slashCommands.ts 第 62 行硬编码英文
31+
> - `ui.sessionList.title/empty` — SessionList 硬编码
32+
> - `ui.askUserQuestion.submit/cancel/selectOption` — AskUserQuestionPrompt 硬编码
33+
> - `ui.processStdout.title/running/adjustTimeout/noOutput` — ProcessStdoutView 硬编码
34+
> - `ui.updatePrompt.planHeader` — UpdatePrompt 硬编码
35+
> - `session.skillPromptHeader` — session.ts 第 987 行硬编码 "Use the skill document below..."
36+
> - `ui.promptInput.footerBusy/ctrlOViewOutput/ctrlOExpand/ctrlOCollapse/imageCount` — PromptInput 中动态拼接
37+
>
338
> **Review 6 发现与修正(综合审计)**:
439
> 1. **回滚方案**:每个 Phase 需明确回滚步骤;Phase 1 最安全,Phase 2 风险最高(13+ 文件)
540
> 2. **验证策略细化**:Phase 1 应验证 `t("ui.loading.thinking")` 返回正确字符串而非 key 自身

0 commit comments

Comments
 (0)