---
title: 前端 i18n 细则
description: 国际化键规范、插值与格式化、错误信息策略及前端落地路径。
---

> **最后更新**: 2026-05-16
> **下次复查触发条件**: 切换 i18n 库（next-intl → 其他）/ 新增 locale（zh-TW / ja 等）/ 引入 AI 类模块的 system prompt / 季度复查

# 核心原则
- 所有用户可见文案必须国际化，前端 `next-intl`，后端 `nestjs-i18n`。
- 已有键优先复用，避免重复；键名语义稳定，不塞具体文案。

# 键名规范
- 模块命名空间：`auth.login.title`、`errors.validation.required` 等。

# 插值与格式化
- 使用 ICU/占位符插值，禁止字符串拼接。
- 日期/数字必须本地化格式化。

# 后端错误信息策略
- 返回稳定 `code`，可选 `key` + 参数。
- 非必要不返回原始中英文文本。

# 禁止事项
- 组件内硬编码中英文文案。
- 业务逻辑中混入翻译内容。
- 拼接用户可见字符串（如 `"Hello " + name`）。

# 快速检查清单
- [ ] 复用或新增正确命名空间的键  
- [ ] 用插值替代拼接  
- [ ] 日期/数字已本地化  
- [ ] 后端返回 code + key  

# 前端落地
- 文案文件：`frontend/src/locales/`
- 翻译调用：`frontend/src/hooks/useTranslation.ts`
- 禁止在组件中硬编码中文/英文

---

# 实战陷阱（2026-05 沉淀）

> 项目反复踩的 i18n 坑。CLAUDE.md L2 段明确"每次 PR 必须切 zh-CN ↔ en-US 双语回归"，但很多坑不在新加的 UI 区域、而在**周边历史区域**。本节集中沉淀写新页面 / review PR 时的扫描清单。

## 1. 周边历史区域的硬编码中文 / 全角冒号

### 现象

切换到英文环境，审批中心 (`/approval-center`) 多处仍渲染中文：

1. **`Submitted：` 全角冒号** —— 列表卡 label，应该是 `Submitted: ` 半角冒号 + 空格
2. **时间线 action 字符串** —— `IT Administrator 提交申请2026/5/1 ...`
   - "提交申请" / "审批通过" / "审批拒绝" / "撤销审批" / "转交给" / "委托给" / "重新分配给" / "代...审批通过/拒绝" / "强制终止流程" / "升级至" / "加签" / "系统自动通过/拒绝" / "系统执行任务" 等 14 处硬编码
3. **`Last edited:L2 全字段...`** —— 表单管理首页"最近编辑"label 与值之间缺空格

### 根因

`git blame` 确认这些硬编码都是**远早于本次分支的历史 commit**引入（如 `formatOperatorString` 在 `133e4502` 引入），属于"既存技术债"。本次 PR 只动了某局部按钮，**review 时通常只看新加区域**，错过周边历史硬编码。

### 教训：PR 合并前的 i18n 双语回归不能只看新加的 UI 区域

**周边历史区域的硬编码在英文环境下会露馅**——用户切英文一眼看到中英混杂。

### MCP 切语言后用 `body.innerText` 扫硬编码 zh / 全角冒号

低成本快速筛查手段：

```js
// Playwright MCP evaluate：切到 en-US 后扫全页 zh 字符
mcp__plugin_playwright_playwright__browser_evaluate(`
  () => {
    const text = document.body.innerText;
    const zhMatches = text.match(/[\\u4e00-\\u9fa5]+/g) || [];
    const fullWidthColon = (text.match(/：/g) || []).length;
    const fullWidthComma = (text.match(/，/g) || []).length;
    return {
      zhWordCount: zhMatches.length,
      zhSamples: zhMatches.slice(0, 10),
      fullWidthColon,
      fullWidthComma
    };
  }
`)
```

**预期**（en-US locale）：

- `zhWordCount: 0`（如果有 → 硬编码或 missing key fallback）
- `fullWidthColon: 0`（如果有 → 中文环境的 `：` 残留到英文）

**任一不为 0** → 周边历史区域有硬编码，必须修。

### 修法

修周边历史硬编码：

```ts
// ❌ 旧
function formatOperatorString(action: string, ...): string {
  switch (action) {
    case 'SUBMIT': return `${name} 提交申请`;
    case 'APPROVE': return `${name} 审批通过`;
    // ...
  }
}

// ✅ 新（用 t 字典）
function formatOperatorString(action: string, t: TranslateFn, ...): string {
  const locale = t?.locale || 'zh';
  return t(`approvals.actions.${action}`, { name });
  // approvals/zh.ts: "approvals.actions.SUBMIT": "{name} 提交申请"
  // approvals/en.ts: "approvals.actions.SUBMIT": "{name} submitted the request"
}
```

参考 learning: [`.learnings/2026-05-03-i18n-pre-existing-hardcoded-zh-in-approval.md`](../../../.learnings/2026-05-03-i18n-pre-existing-hardcoded-zh-in-approval.md)

## 2. Template literal 不是机械替换

### 现象

logging-system 模块 7/8 page 全没用 locale 文件已有的 i18n key，硬编码中文。AI 第一次评估时跟"颜色 token 化"放在同一类，认为是"机械化替换"，告诉用户"170 处替换 ~30 分钟"。**实际花了 ~1 小时**——i18n 修复**不是** 1:1 字符串替换。

### 跟"颜色 token 化"的本质区别

| 维度 | 颜色 token | i18n 修复 |
|---|---|---|
| 替换关系 | hex → token，1:1 全局映射表 | 文案 → key，**每处需上下文判断** |
| 工具 | `sed -i` 批量 | 每处 `Edit`（手工） |
| 缺失映射 | 保留原色（标记给设计师） | 必须补 key，**zh/en 双向同步** |
| Template literal | 不涉及 | 频繁出现（`` `${count} 请求, 错误率 ${rate}%` ``）|
| 风险 | 视觉微调 | 句法不通 / missing key / 中英混杂 |
| Review 焦点 | 色号映射对不对 | 每个 key 的**双语翻译 + 上下文** |

**把 i18n 修复跟颜色 token 替换放在同一心智模型下评估，必然低估工作量 ~3-4×**。

### Template literal 的正确写法

```tsx
// ❌ 硬编码 + 拼接
<div>{`${count} 请求, 错误率 ${rate}%`}</div>

// ❌ 字符串拼接
<div>{count + ' 请求, 错误率 ' + rate + '%'}</div>

// ✅ ICU 占位符
<div>{t('logging.summary', { count, rate })}</div>
// locales/zh.ts: "logging.summary": "{count} 请求, 错误率 {rate}%"
// locales/en.ts: "logging.summary": "{count} requests, error rate {rate}%"
```

### 规则

写 i18n 修复 PR 时：

1. **PR 工作量按文件数 × 1.5h** 估算（不是字符串数 × 几秒）
2. 不要在 `sed -i` 类批量工具里做 i18n 替换——容易切坏 template literal
3. 缺失 key 必须 **zh + en 双向同步补**，不能只补一边
4. Review 时**逐 key 检查双语翻译是否对**（语义、语序、复数形式）

参考 learning: [`.learnings/2026-05-10-i18n-fix-is-not-mechanical.md`](../../../.learnings/2026-05-10-i18n-fix-is-not-mechanical.md)

## 3. AI 类模块的 system prompt 必须随 locale 切换

### 现象

AI 类模块（典型如 agent / ai-assistant）的 doc-review 历史 finding：

> system prompt 单语，en-US 用户输出语言混搭

LLM 收到的 system prompt 永远是中文（例："你是 X 助手，请根据用户描述生成 ..."）。当用户 locale = `en-US`、输入英文 prompt，LLM 倾向**输出中文混着英文**——因为 system prompt 把它锚到中文输出风格。

### 规则

**调 LLM / AI service 的代码必须根据用户 locale 切 system prompt**：

```ts
// ❌ 硬编码
const systemPrompt = '你是流程图设计专家...';

// ✅ 跟随 locale
const systemPrompt = getSystemPrompt(user.locale);
// prompts/zh.ts: "你是流程图设计专家..."
// prompts/en.ts: "You are a flow diagram design expert..."

// 默认 fallback 是哪种语言？文档必须明示。
```

**doc-review Lens F**（AI quality 维度）已加入"system prompt 的 i18n 切换"作为必查项——AI 类模块的 PRD 必须明示。

### 与 i18n 文案的差别

普通 UI 文案：`zh.ts` + `en.ts` 各一份 key，用户切 locale 自动取。

LLM system prompt：**不是 t() 取键**，是**整段不同语言的 prompt 文本**，因为 prompt 工程的措辞、few-shot example 都要原生语言写。两份 prompt 不是机器翻译关系，是**两份独立设计**。

参考 learning: [`.learnings/2026-05-01-doc-review-needs-ai-and-observability-lens.md`](../../../.learnings/2026-05-01-doc-review-needs-ai-and-observability-lens.md)（AI 类模块 doc-review 第 5 lens 缺口）

## 4. 部门预设配色 / 标签 key 必须双语映射

### 反例

某模块定义部门预设：

```ts
const DEPT_PRESETS = {
  '研发中心': '#3370FF',
  '产品部': '#00B42A',
  '财务部': '#FF7D00',
};
```

切英文 locale 时，UI 显示部门 dropdown：
- 中文环境：研发中心 / 产品部 / 财务部 ✓
- 英文环境：研发中心 / 产品部 / 财务部 ❌（仍然是中文 key）

### 规则

任何 **"中文 key → 业务属性"** 的预设映射都不能直接用中文当 key——用**稳定 enum / code**：

```ts
const DEPT_PRESETS = {
  RND: { color: '#3370FF', i18nKey: 'org.dept.rnd' },
  PRODUCT: { color: '#00B42A', i18nKey: 'org.dept.product' },
  FINANCE: { color: '#FF7D00', i18nKey: 'org.dept.finance' },
};
// locales/zh.ts: "org.dept.rnd": "研发中心" / "org.dept.product": "产品部" / ...
// locales/en.ts: "org.dept.rnd": "R&D Center" / ...
```

## 5. PR 合并前 i18n 回归扩展 checklist

CLAUDE.md L2 段已明确"每次必须切 zh-CN ↔ en-US 两套 locale"。本节是**扩展 checklist**：

- [ ] 新加 UI 区域**所有**文案都用 `t(...)`，不硬编码
- [ ] Template literal 用 ICU 占位符（`{count} 请求, ...`），不是 `${x} 拼接`
- [ ] 缺失 key 在 `zh.ts` + `en.ts` **双向同步**补
- [ ] 切英文 locale 后跑 §1 的 `body.innerText` 扫描，`zhWordCount: 0` + `fullWidthColon: 0`
- [ ] **扫周边历史区域**（不是只看新加 UI）—— PR 主题之外的页面在英文下也得正常
- [ ] 控制台无 `missing key warning`
- [ ] 日期 / 数字 / 货币按 locale 格式化（`Intl.DateTimeFormat` / `Intl.NumberFormat`，不靠 `toLocaleDateString('zh-CN')` 硬编码）
- [ ] AI 类模块（调 LLM）的 system prompt 跟随 locale 切（§3）
- [ ] 中文 key 预设映射改成稳定 enum + i18nKey（§4）

## 6. 相关 learning

- [`.learnings/2026-05-03-i18n-pre-existing-hardcoded-zh-in-approval.md`](../../../.learnings/2026-05-03-i18n-pre-existing-hardcoded-zh-in-approval.md)（周边历史硬编码）
- [`.learnings/2026-05-10-i18n-fix-is-not-mechanical.md`](../../../.learnings/2026-05-10-i18n-fix-is-not-mechanical.md)（不是机械替换）
- [`.learnings/2026-05-01-doc-review-needs-ai-and-observability-lens.md`](../../../.learnings/2026-05-01-doc-review-needs-ai-and-observability-lens.md)（AI 类 system prompt i18n）
- 配套阅读：[`doc-review/SKILL.md`](../../doc-review/SKILL.md) §Lens F（AI quality + 可观测性）
