# 中英文界面与 i18n 约定

本文件是本次 i18n 改造的**唯一契约**。多个模块并行改造时，一律遵循此处约定，
避免同一个仓库出现两套翻译机制。

## 1. 目标

- 界面语言**跟随用户在 DSH 里的选择**（设置 → Language），插件不覆盖用户偏好。
- 保持中文界面**零回归**：中文仍是 DSH 默认语言，既有中文文案逐字不变。
- 用户在 DSH 选英文、或配置 `locale: 'en'` 时，Host 启动后使用英文提示与管理工具描述。
- 面板顶部工具栏直接展示紧凑的「中文 / EN」按钮，不再单独占行；完整名称「简体中文 / English」保留在悬停提示及读屏标签中。当前语言高亮，支持键盘及读屏。编辑弹窗遮挡工具栏；切换处理器仍保留未保存编辑确认。仅重载当前面板，不修改 Host 配置或重启 DSH。主动选择保存在当前浏览器同源 localStorage（不同浏览器/设备不共享，同源面板共享）；存储不可用时仍可通过当前页参数切换。面板语言与 Host 文案可独立；需要统一时同时设置 DSH/插件语言并手工重启 Host。
- 服务商目录说明、品牌名称、用户内容、示例任务中的企业名及 v1 模板协议常量保留原文，不宣称第三方内容全部英文化。
- 缺译文**绝不**显示空白或 key：回退中文原文。

## 1.1 语言偏好从哪里读（**易错**）

语言偏好的真值在 **Host 的 profile 补丁**里：`@deepseek-ai/dsh-client-locale`
把用户的显式选择写成该条目的 `config.preference`（例如 `preference: en`）。

- `locale` 是**仅浏览器端**的服务，Host 侧没有（Host 的 98 个服务里不含 `locale`）。
  因此**不能**用 `ctx.inject(['locale'])` 在 Host 等待它——永远等不到，会静默退回中文。
- 也**不能**写 `ctx.locale?.getLocale?.()`：Cordis 的上下文代理在访问未注入的属性时
  直接抛 `cannot get property "locale" without inject`，可选链拦不住，插件装载失败。
- 正确做法：`readHostLocalePreference(ctx)` 从 `ctx.loader.entries()` 找到
  `@deepseek-ai/dsh-client-locale` 条目并读 `options.config.preference`；
  读不到时返回 `undefined`。插件启动传入空的环境变量映射，回退中文，不因英文 shell 环境改变现有用户体验。

`locale` 默认值为 `'auto'`，即走上述链路；`'zh-CN'` / `'en'` 为强制指定。

面板优先级：用户当前页 `uiLocale` 选择 → 浏览器保存的主动选择 → Host 注入语言 → 客户端 `locale` 参数 → HTML 默认中文。新增懒加载分享界面的词典位于 `ui/locale.js`，只翻译显示与提示，不改写用户输入或序列化模板常量。

验收：`npm run check` 为代码门禁；`node scripts/check-i18n-ui.mjs` 为独立 Chromium + Mock API UI 验收，需配置 Playwright。Windows CI 和真实 Windows Desktop 仍是不同证据，不可互相替代。

## 2. 核心模块

`lib/i18n/index.js` 提供纯函数：

| 导出 | 用途 |
|---|---|
| `DEFAULT_LOCALE` | `'zh-CN'` |
| `SUPPORTED_LOCALES` | `['zh-CN', 'en']` |
| `normalizeLocale(value)` | `zh`/`zh_CN`/`en-US` → `'zh-CN'`/`'en'`；其它返回 `undefined` |
| `resolveLocale(value, { hostLocale, env })` | 解析配置值：`'auto'` 时看 Host/环境变量，否则明确选择 |
| `interpolate(template, params)` | `{name}` 占位符插值 |
| `createTranslator(locale, dictionaries)` | 返回 `t(msgid, params)`，附带 `.locale` / `.missing()` |

`lib/i18n/locales/en.js` 合并 `lib/i18n/locales/en/*.js` 分片；同一 msgid 译文冲突会抛错。

## 3. 文案书写规则（**必须**）

1. **msgid 就是中文原文**，逐字复制既有中文，不要改写、不要精简、不要改标点。
   ```js
   // 改造前
   return { ok: false, message: '连接不存在，请刷新后重试' };
   // 改造后
   return { ok: false, message: t('连接不存在，请刷新后重试') };
   ```
2. **带变量的句子整体作为一个 msgid**，变量在模板里保留，值通过参数传：
   ```js
   t('已启用 "{name}"', { name: committed.name })
   // ❌ 不要拼接：t('已启用') + ' "' + name + '"'
   ```
3. **不要翻译**：连接器名称/厂商/分类/标签（来自 catalog 与远程 registry）、
   工具原始名、`serverName`、URL、错误码、字段名、JSON 示例、命令行。
4. **不要动**代码注释、日志文案（`logger.*`）、内部断言。日志是给维护者看的，保持中文。
5. **不要动** `throw new Error()` 里的**内部**错误；但**面向用户**的 `message` 字段必须翻译。
   判断标准：该字符串会出现在 `{ ok, message }` 返回、界面、或工具描述/参数描述里 → 要翻译；
   只在插件内部被 catch 且不会展示 → 不翻译。
6. 变量插值后仍是中文语序的句子，英文译文自行调整语序：
   `t('共 {count} 条', { count })` → `"{count} total"`。

## 4. 函数签名改造

需要翻译的模块，把 `t` 作为参数传入，**不要**读全局变量，也不要自己读环境变量：

```js
// 改造前
export function registerTools(ctx, api, { compact = false } = {}) { ... }
// 改造后
export function registerTools(ctx, api, { compact = false, t = createTranslator(DEFAULT_LOCALE) } = {}) { ... }
```

默认值是中文 translator，因此**既有调用点与既有测试不改也能继续工作**。

## 5. 词典分片（各自只改自己那一个文件）

| 分片文件 | 负责的源文件 |
|---|---|
| `lib/i18n/locales/en/host.js` | `lib/index.js` |
| `lib/i18n/locales/en/tools.js` | `lib/tools.js` |
| `lib/i18n/locales/en/diagnostics.js` | `lib/diagnostics.js`、`lib/connection-scopes.js`、`lib/governance.js` |
| `lib/i18n/locales/en/validation.js` | `lib/mcp-validation.js`、`lib/schema.js`、`lib/catalog.js`、`lib/connection-editor.js`、`lib/oauth.js`、`lib/connectors/*.js`、`lib/prompts.js` |
| `lib/i18n/locales/en/web.js` | `lib/web.js` |
| `lib/i18n/locales/en/client.js` | `lib/client.js` |

分片格式：

```js
export default {
  '中文原文': 'English text',
  '已启用 "{name}"': 'Enabled "{name}"',
};
```

## 6. 英文质量要求

- 面向最终用户，用**简洁、专业**的英文；技术名词保留标准写法（OAuth、PKCE、Server、
  Workspace、Token、API Key、MCP）。
- 同一概念用词统一：`连接`→ connection，`连接器`→ connector，`凭据`→ credential，
  `授权`→ authorization，`重新授权`→ reauthorize，`作用域`→ scope，`快照`→ snapshot，
  `工具`→ tool，`目录`→ catalog，`已安装`→ Installed，`托管`→ managed by the Host。
- 句首大写，句末按英文习惯加句点（短按钮文案不加）。
- **不要**逐字硬译；中文的四字并列结构在英文里改成自然表达。
- 占位符 `{name}` 必须原样保留，不得改名或丢字。

## 7. 测试

- 中文测试**断言既有中文**，不要为了英文改断言。
- 新增英文覆盖，断言 `t.locale === 'en'` 且关键文案是英文。
- 改 `ui/index.html` 的测试用 `new Function` 截取代码片段运行，改造后片段里可能出现
  `t(...)`，测试需要向 `new Function` 传入 `t`（可传 `(s) => s` 或词典 translator）。

## 8. 禁止事项

- 禁止引入第三方 i18n 依赖（仓库无运行时依赖扩张）。
- 禁止改 `package.json` 版本号。
- 禁止改 `catalog/catalog.json` 里的连接器数据。
- 禁止修改 `test/catalog-proxy.test.mjs`（该文件在本机 Node 26 上因 undici
  global-dispatcher 行为失败，与本次改动无关，属既有环境问题）。
