# 国际化

插件通过清单声明语言资源，并使用 SDK 翻译接口显示文本。更新语言资源或翻译接口时，同步调整清单、词典和调用代码。

## 资源归属

桌面程序、Web Client 和每个插件分别拥有业务词典。共享运行库只实现语言匹配、ICU 消息格式、格式化和校验。协议包的 `diagnostics.en.ts` 仅包含协议自身校验诊断，不包含应用界面及插件业务文案；对外不提供通用的应用 `t()` 接口。桌面端和 Web Client 分别通过自身 `src/i18n/runtime.ts` 与 `src/i18n/locales` 翻译界面。插件 SDK 不打包宿主或其他插件的词典；插件缺失消息时不能从宿主查找翻译。

插件清单中的 `i18n` 没有独立版本号：

```json
{
  "name": "My clock",
  "nameKey": "plugin.name",
  "i18n": {
    "defaultLocale": "en-US",
    "resources": {
      "en-US": "locales/en-US.json",
      "zh-CN": "locales/zh-CN.json",
      "ja-JP": "locales/ja-JP.json"
    }
  }
}
```

也可以用 `messages` 内联声明同样的词典；`resources` 与 `messages` 必须二选一。`defaultLocale` 必须对应一个存在的词典。开发者可以选择任意合法的规范 BCP 47 语言标签作为默认语言，插件支持的语言不受宿主界面语言数量限制。完全没有可翻译文案的插件可省略 `i18n`。

语言文件为扁平 JSON 字符串字典，推荐使用语义键：

```json
{
  "plugin.name": "My clock",
  "items.count": "{count, plural, one {# item} other {# items}}"
}
```

查找顺序为：匹配语言中的消息 → 插件默认语言中的消息 → 调用方提供的显示回退 → 原始键。语言匹配优先精确匹配，再按语言与文字脚本匹配，不跨简繁体自动匹配。空字符串是有效翻译。ICU 参数不会递归展开；标签形式的内容作为普通文本处理，不作为 HTML 执行。

## 清单展示字段

`nameKey`、`titleKey`、`descriptionKey`、`labelKey`、`placeholderKey`、`helpKey` 显式引用当前插件词典。小组件的展示分类支持 `categoryKey`。未声明引用的原文字段原样保留。

本地化只遍历协议定义的展示容器。ID、动作类别 ID、参数键、`value`、`enum`、`default`、示例数据、用户保存内容和运行状态保持原值。默认语言中缺失清单引用会阻止打包及安装。

## 后台 SDK

```js
const { definePlugin, t, i18n } = require(process.env.FLYONDECK_SDK_PATH)
const label = t('items.count', { count: 3 })
const preferred = i18n.getRequestedLocale()
const resolved = i18n.getResolvedLocale()
```

`context.i18n` 提供相同实例接口，包括 `number`、`date`、`relativeTime` 和 `onLocaleChange`。宿主通过保留环境变量 `FLYONDECK_LOCALE` 提供期望语言，插件不能用配置覆盖。运行中的默认实例接收宿主语言变更。每个异步命令通过 AsyncLocalStorage 保留发起请求的语言快照，`t`、格式化函数和 `context.i18n` 在 `await` 之后仍使用同一语言；跨插件服务继续传递这一语言。宿主切换语言只影响后续默认请求。动作和实例动作通过执行选项传递语言，在排队前固定；工作流通过 `RunContext.locale` 继续传递，不把语言写入业务参数。派生标签应在请求或渲染时生成。

`getRequestedLocale()` 保留用户期望语言；`getResolvedLocale()`／`getLocale()` 返回当前包实际使用的语言。例如宿主界面只能回退到英文时，日文插件仍会收到 `ja-JP`。

## 页面 SDK

页面先加载 `/api/plugin-sdk/i18n-v1.js`，再加载对应的小组件或配置页 SDK。宿主在页面其他脚本执行前注入已验证的插件资源，页面无需注册或自行获取词典。

```html
<script src="/api/plugin-sdk/i18n-v1.js"></script>
<script src="/api/plugin-sdk/widget-page-v1.js"></script>
<span data-i18n="plugin.name">My clock</span>
<script>
const label = FlyonDeckWidget.t('items.count', { count: 3 })
FlyonDeckWidget.i18n.onLocaleChange(render)
</script>
```

静态文案支持 `data-i18n`，属性支持 `data-i18n-title`、`data-i18n-aria-label`、`data-i18n-placeholder`、`data-i18n-alt`。动态文本显式调用 `t()`。语言变化先更新静态标签，再通知动态渲染订阅者，避免初始标签覆盖实时状态。页面设置 `lang` 和 `dir`。

语言来自页面 URL 和通过父窗口及插件身份校验的消息，每个页面有独立实例。配置页接口对应 `FlyonDeckWidgetConfig`。Web Client 将期望语言与小组件数据请求一起签名，桌面 IPC 也单独传递语言。宿主校验语言后放入插件命令上下文，不把它混入插件业务输入或保存配置。天气插件的响应缓存按语言区分。用户输入、API 返回的文章和用户自定义标签不应作为翻译键。时区独立于语言配置。

## 资源校验与发布

资源随插件发布，安装和运行不需要翻译服务联网。文件路径仅允许包内 JSON 相对路径，拒绝路径穿越、符号链接、重复 JSON 键、非法 UTF-8、无效 ICU 消息、跨语言参数不一致和超限资源。单个资源文件最多 256 KiB，总文件资源最多 2 MiB，每个词典最多 5000 条消息、最多 100 种语言。

Node-only 读取适配器通过 `@flyondeck/plugin-contracts/node-i18n` 使用，纯协议入口不导入文件系统。宿主页面资源快照转义 `<` 和 Unicode 行分隔符，响应使用 `Cache-Control: no-store`。

仓库内置扩展、官方插件和教学示例均使用自身 `locales` 目录与语义键。正常构建不从宿主生成插件词典，也不提供旧清单迁移脚本。

## SDK 诊断与共享工具

SDK 自身诊断保存在 `plugin-sdk/diagnostics.json`，使用稳定的 `SDK_*` 错误码和独立中英文词典，缺失语言回退到英文。插件 `t()` 不会查询 SDK 诊断，SDK 也不查询插件文案。错误码在进程间保留；业务逻辑使用错误码或明确的通知动作标识，不根据已翻译的文案判断。

宿主的 `RequestLocale.ts` 使用 Node AsyncLocalStorage，让宿主文案和纯协议诊断在异步等待、动作排队及工作流中保留调用方语言。主程序、Web Client 的日期时间显示遵循当前期望语言，时区仍然独立。

共享外观和布局校验通过 `createAppearanceTools(t)`／`createDeviceGridTools(t)` 显式接受调用方翻译函数。Web Client 的生产构建检查实际模块图，拒绝导入桌面业务词典。共享显示字段和权限说明在访问时翻译，浏览器偏好在其他模块生成显示元数据之前初始化。

## 验证

```sh
npm test --prefix packages/plugin-contracts
npm --prefix packages run build
npm --prefix packages test
```

`build:i18n` 自动执行 `check:i18n`，检查 19 个仓库插件的资源归属、清单引用、代码／HTML 消息键、翻译完整度，以及页面在 SDK 前加载 i18n 运行库。应用构建和安全测试通过 `build:extensions` 使用这道检查。市场发布检查器也读取包内词典并验证展示引用。

| 验收范围 | 可核对的实现与验证 |
| --- | --- |
| 任意默认语言、匹配与回退 | contracts 测试验证日文默认、区域与文字脚本匹配、包内回退、ICU、格式化、实例隔离；共 33 项通过 |
| 宿主／插件／SDK 资源解耦 | `i18n-ownership.test.cjs` 与 Web Client 实际构建模块图验证无跨包业务词典依赖，SDK 不包含旧中央词典 |
| 安全资源读取 | 重复键、非法 UTF-8、路径穿越、符号链接、缺失文件、超限资源与参数不一致均有拒绝验证；内联和文件词典都按 UTF-8 字节限额校验 |
| 标识及用户数据不变 | 清单展示遍历测试、设置输入保持测试、原生代码／调试器测试；业务 ID、枚举值、默认配置、用户文字和程序不进入翻译 |
| 请求语言隔离 | `i18n-request-context.test.cjs` 实际启动消费方与服务提供方，验证交错请求、宿主语言变化、跨插件服务、动态选项、已认证设备请求、排队动作及工作流 |
| 插件生命周期 | `i18n-integration.test.cjs` 验证离线打包、安装、执行、运行中替换词典和卸载；目录、后台动作和页面使用新资源，卸载后页面返回 404 |
| HTML 注入 | 资源在页面脚本前注入并转义脚本边界；省略可选 head 标签时保留 DOCTYPE；响应禁止缓存 |
| 页面语言切换 | `i18n-pages.electron.cjs` 从清单枚举全部 29 个页面入口／配置页，实际加载包内资源与 SDK，验证中英文切换、设置值和用户内容保持、无页面脚本异常 |
| 长文本、RTL 与异步旧响应 | 页面测试注入长阿拉伯语词典，检查 lang／dir 和窄窗口横向溢出并生成截图；延迟英文响应后切换中文，确认控制组件与天气／网络示例不会显示旧语言结果 |
| 宿主真实界面 | `i18n.electron.cjs` 验证语言选择器、偏好保存与重载，以及中英文通知时间和基于稳定动作标识的通知入口 |
| 桌面端回归 | `test:security` 包含原生资源构建、8 个内置插件构建、SDK 类型检查及 507 项测试：502 项通过；4 项 Windows 专用测试和 1 项无可调亮度显示器的测试在当前 macOS 环境跳过 |
| Web Client | 类型检查、生产构建及 42 项测试通过 |
| 可分发插件与示例 | 官方插件 25 项测试及打包通过；3 个官方插件和 8 个教学示例的完整包均通过市场检查；内置插件随宿主构建分发 |
| 文档与市场 | 文档字段索引、示例、链接、完整包检查等 19 项测试通过；市场 36 项服务测试、5 项 Web 测试及完整构建通过；文档与实现快照同步到市场 |

所有国际化调用方使用本文定义的资源格式和翻译接口。当前资源包含中文和英文；插件支持哪些语言、使用哪种默认语言由插件自己决定，不要求第三方插件采用中文。

## 商店检查报告

商店的包检查器拥有独立诊断词库，持久化检查项目使用稳定 `id`，标题与详情保存 `{ code, values }`。API 在响应时由相应资源所有者生成 `message` 和各支持语言的 `messages`。插件清单、包内语言资源及用户资料保持原文，不并入商店词库。商店页面、API、Worker、检查器分别维护资源；仅复用纯格式化机制。检查报告的生成方和读取方使用相同的数据结构。
