Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@ coverage
.idea
.vscode
*.suo
*.lock
*.lock
176 changes: 176 additions & 0 deletions docs/i18n-guide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
# i18n 国际化指南

## 概述

Claude Code 的国际化 (i18n) 系统允许应用支持多种语言,目前支持:
- **en-US** - 英语(默认)
- **zh-CN** - 简体中文

## 快速开始

### 1. 导入翻译函数

```typescript
import { t } from 'src/i18n';
```

### 2. 使用翻译

```typescript
// 简单翻译
const text = t('common.loading'); // "加载中..." 或 "Loading..."

// 带参数的翻译
const message = t('notifications.noImage', { shortcut: 'Ctrl+V' });
// 输出:"剪贴板中没有图片。使用 Ctrl+V 粘贴图片。"
```

## 翻译键命名规范

使用点分隔的层级命名:

```
category.subcategory.item
```

例如:
- `common.loading` - 通用加载文本
- `chat.input.placeholder` - 聊天输入框占位符
- `permissions.title` - 权限对话框标题

## 可用的翻译键

### 通用 (common.*)
| 键 | 英文 | 中文 |
|---|---|---|
| `common.loading` | Loading... | 加载中... |
| `common.confirm` | Confirm | 确认 |
| `common.cancel` | Cancel | 取消 |
| `common.yes` | Yes | 是 |
| `common.no` | No | 否 |
| `common.ok` | OK | 确定 |
| `common.close` | Close | 关闭 |
| `common.retry` | Retry | 重试 |

### 聊天 (chat.*)
| 键 | 英文 | 中文 |
|---|---|---|
| `chat.input.placeholder` | Type a message... | 输入消息或 '/' 查看命令... |
| `chat.submit` | Send | 发送 |
| `chat.cancelling` | Cancelling... | 取消中... |

### 权限 (permissions.*)
| 键 | 英文 | 中文 |
|---|---|---|
| `permissions.title` | Permission Request | 权限请求 |
| `permissions.allow` | Allow | 允许 |
| `permissions.deny` | Deny | 拒绝 |

### 设置 (settings.*)
| 键 | 英文 | 中文 |
|---|---|---|
| `settings.title` | Settings | 设置 |
| `settings.general` | General | 通用 |
| `settings.appearance` | Appearance | 外观 |

## 添加新的翻译

### 步骤 1: 在翻译文件中添加键值

编辑 `src/i18n/locales/en-US.json` 和 `src/i18n/locales/zh-CN.json`:

```json
{
"my.new.key": "English text",
"my.new.key": "中文文本"
}
```
Comment on lines +82 to +87
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

The JSON examples are invalid as written.

Each example repeats the same key in a single object, so anyone copying it will silently overwrite the first value. Show en-US.json and zh-CN.json as separate examples instead.

Also applies to: 99-104

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/i18n-guide.md` around lines 82 - 87, The JSON examples in the i18n guide
are invalid because they use the same key twice in one object (which silently
overwrites); split the example into two separate JSON snippets named
`en-US.json` and `zh-CN.json`, each containing the same key once with its
respective translation (e.g., "my.new.key": "English text" in `en-US.json` and
"my.new.key": "中文文本" in `zh-CN.json`), and update the other duplicated example
block (lines referenced 99-104) the same way so each language file shows unique
keys per object.


### 步骤 2: 在代码中使用

```typescript
const text = t('my.new.key');
```

### 步骤 3: 带参数的翻译

如果文本包含动态内容,使用参数:

```json
{
"greeting": "Hello, {name}!",
"greeting": "你好,{name}!"
}
```

```typescript
const text = t('greeting', { name: 'World' });
```

## 运行时切换语言

```typescript
import { setLocale, getLocale } from 'src/i18n';

// 获取当前语言
const current = getLocale(); // 'en-US' 或 'zh-CN'

// 切换语言
setLocale('zh-CN');
```

## 最佳实践

1. **始终使用翻译键**:不要在 UI 代码中使用硬编码的字符串
2. **保持一致性**:相同的概念使用相同的翻译键
3. **避免过度嵌套**:键名深度不超过 3-4 层
4. **参数化动态内容**:使用 `{param}` 语法而不是字符串拼接
5. **翻译所有用户可见文本**:包括错误消息、通知、按钮文本等

## 示例

### 组件中使用 i18n

```typescript
import { t } from 'src/i18n';
import { Text } from 'src/ink.js';

function MyComponent() {
return (
<Box>
<Text>{t('common.loading')}</Text>
<Text>{t('permissions.title')}</Text>
</Box>
);
}
```

### 错误处理

```typescript
import { t } from 'src/i18n';

function handleError(error: Error) {
console.error(t('errors.generic'), error);
// 如果翻译键不存在,会返回键名本身,便于调试
}
```

## 文件结构

```
src/i18n/
├── index.ts # 核心翻译函数 t()
├── init.ts # 初始化逻辑
├── README.md # 快速参考
└── locales/
├── en-US.json # 英文翻译
└── zh-CN.json # 中文翻译
```

## 注意事项

1. **不要删除已有的翻译键**:可能导致旧代码返回键名
2. **保持英文和中文键名一致**:便于维护
3. **测试两种语言**:确保翻译正确显示
4. **locale 自动检测**:启动时根据系统语言自动选择
9 changes: 5 additions & 4 deletions src/components/ApproveApiKey.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { c as _c } from "react/compiler-runtime";
import React from 'react';
import { Text } from '../ink.js';
import { t } from '../i18n/index.js';
import { saveGlobalConfig } from '../utils/config.js';
import { Select } from './CustomSelect/index.js';
import { Dialog } from './design-system/Dialog.js';
Expand Down Expand Up @@ -75,15 +76,15 @@ export function ApproveApiKey(t0) {
}
let t5;
if ($[8] === Symbol.for("react.memo_cache_sentinel")) {
t5 = <Text>Do you want to use this API key?</Text>;
t5 = <Text>{t('auth.apiKeyPrompt')}</Text>;
$[8] = t5;
} else {
t5 = $[8];
}
let t6;
if ($[9] === Symbol.for("react.memo_cache_sentinel")) {
t6 = {
label: "Yes",
label: t('auth.apiKeyYes'),
value: "yes"
};
$[9] = t6;
Expand All @@ -93,7 +94,7 @@ export function ApproveApiKey(t0) {
let t7;
if ($[10] === Symbol.for("react.memo_cache_sentinel")) {
t7 = [t6, {
label: <Text>No (<Text bold={true}>recommended</Text>)</Text>,
label: <Text>{t('auth.apiKeyNo')} (<Text bold={true}>{t('auth.apiKeyRecommended')}</Text>)</Text>,
value: "no"
}];
$[10] = t7;
Expand All @@ -110,7 +111,7 @@ export function ApproveApiKey(t0) {
}
let t9;
if ($[13] !== t2 || $[14] !== t4 || $[15] !== t8) {
t9 = <Dialog title="Detected a custom API key in your environment" color="warning" onCancel={t2}>{t4}{t5}{t8}</Dialog>;
t9 = <Dialog title={t('auth.apiKeyDetected')} color="warning" onCancel={t2}>{t4}{t5}{t8}</Dialog>;
$[13] = t2;
$[14] = t4;
$[15] = t8;
Expand Down
3 changes: 2 additions & 1 deletion src/components/CompactSummary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type { NormalizedUserMessage } from '../types/message.js';
import { getUserMessageText } from '../utils/messages.js';
import { ConfigurableShortcutHint } from './ConfigurableShortcutHint.js';
import { MessageResponse } from './MessageResponse.js';
import { t } from '../i18n/index.js';
type Props = {
message: NormalizedUserMessage;
screen: Screen;
Expand Down Expand Up @@ -38,7 +39,7 @@ export function CompactSummary(t0) {
}
let t3;
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
t3 = <Text bold={true}>Summarized conversation</Text>;
t3 = <Text bold={true}>{t('ui.summarizedConversation')}</Text>;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Avoid mixed-language output in the summarized metadata section.

Line 42 localizes only the heading, but the same UI block still has hardcoded English copy (e.g., message count sentence and “Compact summary”), which can produce mixed locale output.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/CompactSummary.tsx` at line 42, The summarized metadata block
in CompactSummary (variable t3 and adjacent UI copy) mixes localized text with
hardcoded English; update the component to replace the remaining hardcoded
strings (e.g., "Compact summary" and the message count sentence) with i18n
lookups using the same translator function (t) and new keys (for example
'ui.compactSummary' and 'ui.messageCount' or similar), pass any dynamic values
(like message count) as interpolation params to t, and use those keys in place
of the literal strings so the entire block is consistently localized.

$[3] = t3;
} else {
t3 = $[3];
Expand Down
Loading