Skip to content

Commit 3540670

Browse files
GouBuliyaCotton-Candy123claude
authored
feat(i18n): add internationalization support (#78)
* feat(i18n): 添加国际化支持 引入i18n系统支持多语言翻译 初始化en-US和zh-CN语言包 修改组件使用t()函数替代硬编码文本 添加翻译键命名规范和文档指南 更新构建配置以支持JSON导入 * feat(i18n): 添加认证流程的多语言支持 refactor(oauth): 重构控制台认证流程使用国际化文本 feat(vscode): 添加文件变更差异对比功能 style(diff): 为diff超时常量添加注释 chore(gitignore): 添加use目录到忽略列表 * fix(i18n): 修复未翻译组件及 React Compiler 缓存导致的 locale 不更新问题 - 删除未使用的 I18nExample.tsx(误用 HTML <div>) - 为 HelpV2、SkillPermissionRequest、WebFetchPermissionRequest 等组件的 硬编码字符串添加翻译键 - 在 React Compiler 编译后的组件中注入 useLocale(),打破 memoization 对翻译文本的缓存,使切换语言后能正确重渲染 - 补充对应的中英文翻译内容 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(i18n): 修复 CodeRabbit 标出的剩余 Major 问题 - FilesystemPermissionRequest: 恢复标题中的 read/edit 语义,使用新的 file.readFile / file.editFile 翻译键 - Spinner.tsx / BriefSpinner: 将每帧 randomVerb sample 改为 useMemo, 避免 spinner 文字闪烁;并将 "Working" fallback 替换为 t('spinner.working') - diff.ts: 在 getUnifiedDiffString 中统一应用 &/$ escape,保持与 getPatchFromContents / getPatchForDisplay 行为一致 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(i18n): 修复 i18n core 的三个边界问题 - setLocale: 相同 locale 时跳过广播,避免无意义重渲染 - has(): 用 key in 判断替代 !!truthy,支持空字符串翻译值 - t(): 对插值参数做 RegExp escape,防止含特殊字符时 SyntaxError Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Li_Xufeng <a528895030@gmail.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent c9c14c8 commit 3540670

38 files changed

Lines changed: 2067 additions & 386 deletions

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,4 @@ coverage
77
.idea
88
.vscode
99
*.suo
10-
*.lock
10+
*.lock

docs/i18n-guide.md

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
# i18n 国际化指南
2+
3+
## 概述
4+
5+
Claude Code 的国际化 (i18n) 系统允许应用支持多种语言,目前支持:
6+
- **en-US** - 英语(默认)
7+
- **zh-CN** - 简体中文
8+
9+
## 快速开始
10+
11+
### 1. 导入翻译函数
12+
13+
```typescript
14+
import { t } from 'src/i18n';
15+
```
16+
17+
### 2. 使用翻译
18+
19+
```typescript
20+
// 简单翻译
21+
const text = t('common.loading'); // "加载中..." 或 "Loading..."
22+
23+
// 带参数的翻译
24+
const message = t('notifications.noImage', { shortcut: 'Ctrl+V' });
25+
// 输出:"剪贴板中没有图片。使用 Ctrl+V 粘贴图片。"
26+
```
27+
28+
## 翻译键命名规范
29+
30+
使用点分隔的层级命名:
31+
32+
```
33+
category.subcategory.item
34+
```
35+
36+
例如:
37+
- `common.loading` - 通用加载文本
38+
- `chat.input.placeholder` - 聊天输入框占位符
39+
- `permissions.title` - 权限对话框标题
40+
41+
## 可用的翻译键
42+
43+
### 通用 (common.*)
44+
|| 英文 | 中文 |
45+
|---|---|---|
46+
| `common.loading` | Loading... | 加载中... |
47+
| `common.confirm` | Confirm | 确认 |
48+
| `common.cancel` | Cancel | 取消 |
49+
| `common.yes` | Yes ||
50+
| `common.no` | No ||
51+
| `common.ok` | OK | 确定 |
52+
| `common.close` | Close | 关闭 |
53+
| `common.retry` | Retry | 重试 |
54+
55+
### 聊天 (chat.*)
56+
|| 英文 | 中文 |
57+
|---|---|---|
58+
| `chat.input.placeholder` | Type a message... | 输入消息或 '/' 查看命令... |
59+
| `chat.submit` | Send | 发送 |
60+
| `chat.cancelling` | Cancelling... | 取消中... |
61+
62+
### 权限 (permissions.*)
63+
|| 英文 | 中文 |
64+
|---|---|---|
65+
| `permissions.title` | Permission Request | 权限请求 |
66+
| `permissions.allow` | Allow | 允许 |
67+
| `permissions.deny` | Deny | 拒绝 |
68+
69+
### 设置 (settings.*)
70+
|| 英文 | 中文 |
71+
|---|---|---|
72+
| `settings.title` | Settings | 设置 |
73+
| `settings.general` | General | 通用 |
74+
| `settings.appearance` | Appearance | 外观 |
75+
76+
## 添加新的翻译
77+
78+
### 步骤 1: 在翻译文件中添加键值
79+
80+
编辑 `src/i18n/locales/en-US.json``src/i18n/locales/zh-CN.json`
81+
82+
```json
83+
{
84+
"my.new.key": "English text",
85+
"my.new.key": "中文文本"
86+
}
87+
```
88+
89+
### 步骤 2: 在代码中使用
90+
91+
```typescript
92+
const text = t('my.new.key');
93+
```
94+
95+
### 步骤 3: 带参数的翻译
96+
97+
如果文本包含动态内容,使用参数:
98+
99+
```json
100+
{
101+
"greeting": "Hello, {name}!",
102+
"greeting": "你好,{name}!"
103+
}
104+
```
105+
106+
```typescript
107+
const text = t('greeting', { name: 'World' });
108+
```
109+
110+
## 运行时切换语言
111+
112+
```typescript
113+
import { setLocale, getLocale } from 'src/i18n';
114+
115+
// 获取当前语言
116+
const current = getLocale(); // 'en-US' 或 'zh-CN'
117+
118+
// 切换语言
119+
setLocale('zh-CN');
120+
```
121+
122+
## 最佳实践
123+
124+
1. **始终使用翻译键**:不要在 UI 代码中使用硬编码的字符串
125+
2. **保持一致性**:相同的概念使用相同的翻译键
126+
3. **避免过度嵌套**:键名深度不超过 3-4 层
127+
4. **参数化动态内容**:使用 `{param}` 语法而不是字符串拼接
128+
5. **翻译所有用户可见文本**:包括错误消息、通知、按钮文本等
129+
130+
## 示例
131+
132+
### 组件中使用 i18n
133+
134+
```typescript
135+
import { t } from 'src/i18n';
136+
import { Text } from 'src/ink.js';
137+
138+
function MyComponent() {
139+
return (
140+
<Box>
141+
<Text>{t('common.loading')}</Text>
142+
<Text>{t('permissions.title')}</Text>
143+
</Box>
144+
);
145+
}
146+
```
147+
148+
### 错误处理
149+
150+
```typescript
151+
import { t } from 'src/i18n';
152+
153+
function handleError(error: Error) {
154+
console.error(t('errors.generic'), error);
155+
// 如果翻译键不存在,会返回键名本身,便于调试
156+
}
157+
```
158+
159+
## 文件结构
160+
161+
```
162+
src/i18n/
163+
├── index.ts # 核心翻译函数 t()
164+
├── init.ts # 初始化逻辑
165+
├── README.md # 快速参考
166+
└── locales/
167+
├── en-US.json # 英文翻译
168+
└── zh-CN.json # 中文翻译
169+
```
170+
171+
## 注意事项
172+
173+
1. **不要删除已有的翻译键**:可能导致旧代码返回键名
174+
2. **保持英文和中文键名一致**:便于维护
175+
3. **测试两种语言**:确保翻译正确显示
176+
4. **locale 自动检测**:启动时根据系统语言自动选择

src/components/ApproveApiKey.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { c as _c } from "react/compiler-runtime";
22
import React from 'react';
33
import { Text } from '../ink.js';
4+
import { t } from '../i18n/index.js';
45
import { saveGlobalConfig } from '../utils/config.js';
56
import { Select } from './CustomSelect/index.js';
67
import { Dialog } from './design-system/Dialog.js';
@@ -75,15 +76,15 @@ export function ApproveApiKey(t0) {
7576
}
7677
let t5;
7778
if ($[8] === Symbol.for("react.memo_cache_sentinel")) {
78-
t5 = <Text>Do you want to use this API key?</Text>;
79+
t5 = <Text>{t('auth.apiKeyPrompt')}</Text>;
7980
$[8] = t5;
8081
} else {
8182
t5 = $[8];
8283
}
8384
let t6;
8485
if ($[9] === Symbol.for("react.memo_cache_sentinel")) {
8586
t6 = {
86-
label: "Yes",
87+
label: t('auth.apiKeyYes'),
8788
value: "yes"
8889
};
8990
$[9] = t6;
@@ -93,7 +94,7 @@ export function ApproveApiKey(t0) {
9394
let t7;
9495
if ($[10] === Symbol.for("react.memo_cache_sentinel")) {
9596
t7 = [t6, {
96-
label: <Text>No (<Text bold={true}>recommended</Text>)</Text>,
97+
label: <Text>{t('auth.apiKeyNo')} (<Text bold={true}>{t('auth.apiKeyRecommended')}</Text>)</Text>,
9798
value: "no"
9899
}];
99100
$[10] = t7;
@@ -110,7 +111,7 @@ export function ApproveApiKey(t0) {
110111
}
111112
let t9;
112113
if ($[13] !== t2 || $[14] !== t4 || $[15] !== t8) {
113-
t9 = <Dialog title="Detected a custom API key in your environment" color="warning" onCancel={t2}>{t4}{t5}{t8}</Dialog>;
114+
t9 = <Dialog title={t('auth.apiKeyDetected')} color="warning" onCancel={t2}>{t4}{t5}{t8}</Dialog>;
114115
$[13] = t2;
115116
$[14] = t4;
116117
$[15] = t8;

src/components/CompactSummary.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import type { NormalizedUserMessage } from '../types/message.js';
77
import { getUserMessageText } from '../utils/messages.js';
88
import { ConfigurableShortcutHint } from './ConfigurableShortcutHint.js';
99
import { MessageResponse } from './MessageResponse.js';
10+
import { t } from '../i18n/index.js';
1011
type Props = {
1112
message: NormalizedUserMessage;
1213
screen: Screen;
@@ -38,7 +39,7 @@ export function CompactSummary(t0) {
3839
}
3940
let t3;
4041
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
41-
t3 = <Text bold={true}>Summarized conversation</Text>;
42+
t3 = <Text bold={true}>{t('ui.summarizedConversation')}</Text>;
4243
$[3] = t3;
4344
} else {
4445
t3 = $[3];

0 commit comments

Comments
 (0)