# 国际化

## 唯一入口

`src/lib/i18n.ts` 是全部文案的唯一来源，同时导出中文资源供测试扫描：

```ts
export const zhResources = {
  confirm: '确定',
  cancel: '取消',
  total: '共 {{count}} 条',
  // …
}

// en 的类型是 typeof zhResources：少一个 key 或多一个 key 都会编译失败
const en: typeof zhResources = {
  confirm: 'Confirm',
  cancel: 'Cancel',
  total: '{{count}} items',
}
```

`en: typeof zhResources` 这条类型约束是刻意的：中英文**结构必须一致**，翻译漏项会在 `pnpm typecheck` 阶段暴露，
而不是等到运行时显示 key。

## 在组件里使用

```tsx
import { useTranslation } from 'react-i18next'

export function Toolbar() {
  const { t } = useTranslation()
  return <Button>{t('confirm')}</Button>
}
```

带变量：

```tsx
t('total', { count: rows.length }) // 共 12 条
t('icons.copied', { name: 'Search' }) // 已复制 Search
```

## 哪些文案可以不走语言包

| 场景                           | 做法                                                               |
| ------------------------------ | ------------------------------------------------------------------ |
| 菜单项                         | `label` 可以写 i18n key，也可以先写中文（未登记的 key 会原样显示） |
| 组件内部固定文案               | 必须进语言包（例如表格的"密度""恢复默认"）                         |
| **组件总览与文档里的示例文案** | 直接写中文，属于演示内容                                           |
| 业务页面文案                   | 进语言包，或按业务域拆模块再合并进 `resources`                     |

## 语言切换

- 存储键：`storageKey('locale')`，取值 `zh-CN` / `en`。
- 切换时同步更新 `<html lang>`。
- 入口在顶栏「语言」快捷操作，以及侧栏底部用户菜单里的设置弹窗。

## 新增一种语言

1. 在 `src/lib/i18n.ts` 里加语言包对象，类型写成 `typeof zhResources`，先把缺项补成中文占位。
2. 在 `resources` 里登记语言包。
3. 在语言切换菜单的选项里加上该语言（`PreferencesMenu`）。
4. `resolveLocale` 逻辑里接受新的存储值（当前只区分 `en` 与其它）。
5. 跑 `pnpm test`：`i18n-keys.test.ts` 会扫描代码里的字面量 key，确认没有"用了但没登记"的文案。

## 为什么要扫描 key

漏登记 key 的表现是页面上直接显示 `sample.saved` 这样的原始字符串，肉眼评审很难覆盖全站。
`src/test/unit/i18n-keys.test.ts` 用正则收集源码里所有 `t('a.b')` 形式的字面量，断言它们都存在于 `zhResources`，
新增文案时忘记登记会直接让单测失败。

```bash
pnpm test -- i18n-keys
```

动态拼接的 key（例如 `t('sample.metric' + key)`）不在这条扫描范围内，需要人工确认；能写死就写死。
