# i18n 使用指南

## 引言

本指南覆盖 ArenaPro 的国际化（i18n）方案，包含客户端与服务端：初始化位置、类型安全（t-keys 自动补全）、如何在代码中使用 `t()`、如何新增翻译键/语言，以及运行时切换语言等。

### 传统工作流的痛点

> 程序小明：先写个文案吧，先用简体中文写死，繁体中文之后再说。
>
> 产品同学：英文版什么时候好？日语也要支持一下。

临时硬编码的文案、分散在各处的字符串、手动复制的键名与路径，都会在多人协作中迅速变成维护“噩梦”。

ArenaPro 的 i18n 实践能把文案“像资源一样”统一管理，并让 VS Code 在编码时提供键名自动补全与错误检查：

1. 类型安全的键名提示，避免拼写错误与缺键。
2. 开发期即暴露缺失翻译，避免上线后才发现问题。
3. 统一的文件组织，新增键/语言有章可循。

## 目录与文件

- 共享初始化与导出（客户端/服务端通用）：`i18n/index.ts`
- i18n 类型增强（确保 t-keys 类型安全）：`i18n/types/i18n.d.ts`
- 翻译资源（JSON）：

  - 简体中文：`i18n/res/zh-CN/translation.json`
  - 英文：`i18n/res/en/translation.json`

  > 注：当前我们以 `res/zh-CN/translation.json` 作为“键名的单一真相来源”，保证类型安全与自动补全。

## 初始化与工作原理

参见 `i18n/index.ts`：

- 使用 `i18next` 初始化。
- `lng` 使用浏览器语言 `navigator.language` 自动检测（在服务端不存在时回退到 `zh-CN`）。
- `fallbackLng` 设置为 `zh-CN`，当目标语言缺失时回退到中文。
- 默认命名空间（`defaultNS`）为 `translation`。
- 资源（`resources`）内联引入：
  - `en` → `@root/i18n/res/en/translation.json`
  - `zh-CN` → `@root/i18n/res/zh-CN/translation.json`
- 当前配置 `debug: false`，如需调试缺失键可在开发期临时改为 `true`。

> 注：`@root/*` 别名来自 `client/tsconfig.json` 的 `paths` 配置。

### 导出内容

- `default export i18n`: 直接使用 `i18n.t('key', options?)`。

### 类型安全与自动补全（共享声明）

通过 `i18n/types/i18n.d.ts` 使用 TypeScript 模块增强为 `i18next` 添加自定义类型：

- 以 `zh-CN/translation.json` 的结构作为“单一真相来源（source of truth）”，所有翻译键以其为基准。
- 你在调用 `i18n.t('...')` 时，将获得键名的自动补全和编译期检查。

因此，新增或修改翻译键时，请先更新 `i18n/res/zh-CN/translation.json`，再同步到其他语言文件，才能保证类型安全与一致性。

### 如何使用

1. 直接使用导出的 `i18n`：

```ts
import i18n from "@root/i18n";

// 最简单的取文案
const text = i18n.t("welcome_game");

// 插值示例（对应 translation.json 中的："navigator.language": "你的浏览器使用语言是：{{language}}"）
const langText = i18n.t("navigator.language", { language: navigator.language });
```

2. 在 UI 代码中使用（示例）：

```ts
import i18n from "@root/i18n";

const text = UiText.create();
text.parent = ui;
text.textContent = i18n.t("welcome_game");
```

## 服务端指引

同样使用共享的 `@root/i18n`，服务端由于没有 `navigator.language`，请在调用时显式传入玩家语言：

```ts
import i18n from "@root/i18n";

// 这里会显示中文的欢迎文案，因为显示设定了语言为中文
console.log("(server):", i18n.t("welcome_game", { lng: "zh-CN" }));

// 这里会显示英文的欢迎文案，因为显示设定了语言为英文
console.log("(server):", i18n.t("welcome_ap", { lng: "en" }));
```

## `t()` 的 options 常用用法（速查）

`i18n.t(key, options?)` 的完整说明见官方文档：

https://www.i18next.com/translation-function/essentials

下面列出项目中最常用的几种写法：

```ts
import i18n from "@root/i18n";

// 1) 指定语言（常用于服务端或强制指定场景）
i18n.t("welcome_game", { lng: "en" });

// 2) 指定命名空间（当采用多命名空间时）
i18n.t("title", { ns: "ui" });

// 3) 插值（占位变量）
// translation.json: { "hello": "你好，{{name}}" }
i18n.t("hello", { name: "小明" });

// 4) 复数 plural（需在资源中提供 one/other 等形式）
// translation.json:
// { "item": "{{count}} 个物品", "item_one": "{{count}} 个物品", "item_other": "{{count}} 个物品" }
i18n.t("item", { count: 1 }); // 单复数由 i18next 按语言规则选择
i18n.t("item", { count: 5 });

// 5) 语境 context（需要在资源中提供 *_male / *_female 等变体）
// translation.json: { "actor": "演员", "actor_male": "男演员", "actor_female": "女演员" }
i18n.t("actor", { context: "male" });
i18n.t("actor", { context: "female" });

// 6) 默认值（当 key 缺失时使用）
i18n.t("not.exist.key", { defaultValue: "占位默认文案" });

// 7) 返回对象/数组（当 key 指向一个对象时）
// i18n.t('group', { returnObjects: true }) // 返回结构化对象

// 8) 数组连接（当 key 对应数组时）
// i18n.t('list', { joinArrays: ', ' })
```

> 小贴士：
>
> - 客户端无需手动传 `lng`（已基于 `navigator.language` 初始化）。
> - 服务端调用时请显式传 `lng`：`i18n.t(key, { lng: playerLang })`。

## 运行时切换语言

你可以在运行时切换语言，例如：

```ts
import i18n from "@root/i18n";

async function switchToEnglish() {
  await i18n.changeLanguage("en");
}

async function switchToChinese() {
  await i18n.changeLanguage("zh-CN");
}
```

切换后，使用 `i18n.t()` 获取的文案会立刻基于新语言返回结果。若你的 UI 框架不具备响应式绑定，请在切换后手动触发界面刷新。

## 维护与扩展

### 新增或维护翻译键

以 `zh-CN` 为“基准语言”：

1. 在 `i18n/res/zh-CN/translation.json` 中新增/修改键值。
2. 在 `i18n/res/en/translation.json`（以及其他语言文件）中补齐相同键。
3. 在代码中使用 `i18n.t("你的新键")`。

示例：

- 在 `i18n/res/zh-CN/translation.json` 增加：

```json
{
  "common": {
    "ok": "确定",
    "cancel": "取消"
  }
}
```

- 在 `i18n/res/en/translation.json` 增加对应键：

```json
{
  "common": {
    "ok": "OK",
    "cancel": "Cancel"
  }
}
```

- 代码中使用：

```ts
import { t } from "@root/i18n";

const ok = t("common.ok");
const cancel = t("common.cancel");
```

### 新增一种语言

1. 在项目根 `i18n/res/` 下新增目录与 `translation.json`，如：`i18n/res/ja/translation.json`。
2. 在 `i18n/index.ts` 中：
   - `import ja_Translation from "./res/ja/translation.json";`
   - 在 `resources` 中添加：

```ts
resources: {
  en: { translation: en_Translation },
  "zh-CN": { translation: zhCN_Translation },
  ja: { translation: ja_Translation },
}
```

3. 运行时可通过 `i18n.changeLanguage("ja")` 切换。

## 进阶：多命名空间资源（Multiple Namespaces）

当项目逐渐变大，把所有文案都塞进一个 `translation.json` 会变得难以维护。推荐按“功能域”拆分为多个命名空间（如 `common`、`ui`、`gameplay`），既便于团队分工，也便于按模块定位。

#### 目录组织（示例）

```
i18n/res/
  en/
    common.json
    ui.json
    gameplay.json
  zh-CN/
    common.json
    ui.json
    gameplay.json
```

示例内容：

```json
// zh-CN/ui.json
{
  "title": "开始游戏",
  "button": { "start": "开始", "quit": "退出" }
}
```

#### i18n 初始化配置（`i18n/index.ts`）

将单一的 `translation` 拆分为多个命名空间，并在 `resources` 中分别挂载：

```ts
import i18next from "i18next";
import en_common from "@root/i18n/res/en/common.json";
import en_ui from "@root/i18n/res/en/ui.json";
import en_gameplay from "@root/i18n/res/en/gameplay.json";
import zh_common from "@root/i18n/res/zh-CN/common.json";
import zh_ui from "@root/i18n/res/zh-CN/ui.json";
import zh_gameplay from "@root/i18n/res/zh-CN/gameplay.json";

await i18next.init({
  lng: navigator.language,
  fallbackLng: "zh-CN",
  ns: ["common", "ui", "gameplay"],
  defaultNS: "common",
  resources: {
    en: {
      common: en_common,
      ui: en_ui,
      gameplay: en_gameplay,
    },
    "zh-CN": {
      common: zh_common,
      ui: zh_ui,
      gameplay: zh_gameplay,
    },
  },
  debug: true,
});

export default i18next;
```

#### 在代码中使用多命名空间

方式一：调用时通过 `ns` 参数指定命名空间。

```ts
import i18n from "@root/i18n";

const title = i18n.t("title", { ns: "ui" }); // 取 ui 命名空间下的 title
const start = i18n.t("button.start", { ns: "ui" });
```

方式二：获取固定命名空间的 `t`（推荐在同一模块大量使用同一 ns 时）。

```ts
import i18n from "@root/i18n";

const tUI = i18n.getFixedT(null, "ui");

const title = tUI("title");
const start = tUI("button.start");
```

#### 类型声明：为多命名空间提供类型安全

如果你在 `i18n/types/i18n.d.ts` 中通过模块增强提供了键名提示，需要把 `resources` 的类型从单一 `translation` 调整为多命名空间结构。示例：

```ts
// i18n/types/i18n.d.ts（多命名空间时可扩展为如下结构）
import "i18next";

declare module "i18next" {
  interface CustomTypeOptions {
    defaultNS: "common";
    // 注意：以 zh-CN 为基准语言的结构作为类型来源
    resources: {
      common: typeof import("@root/i18n/res/zh-CN/common.json");
      ui: typeof import("@root/i18n/res/zh-CN/ui.json");
      gameplay: typeof import("@root/i18n/res/zh-CN/gameplay.json");
    };
  }
}
```

完成上述调整后，`i18n.t()` 与 `getFixedT()` 都将获得命名空间内键名的自动补全与编译期检查。

#### 维护建议

- 新键优先写入 `zh-CN` 对应命名空间文件，再补齐其他语言。
- 模块内统一使用固定 `t`，例如 `const tUI = getFixedT(..., "ui")`，避免漏传 `ns`。
- 命名空间不宜过细，建议按业务域（如 UI、common、gameplay）划分。

## 常见问答（FAQ）

- Q：如何在开发时更容易发现缺失键？

  - A：`i18n/index.ts` 中 `debug: true` 会在控制台提示缺失键；发布前改为 `false`。

- Q：浏览器语言是 `en-US` 也能命中吗？

  - A：可以，会匹配到 `en`；若未命中，则根据 `fallbackLng: "zh-CN"` 回退。

- Q：`t()` 返回键名本身，怎么办？
  - A：优先检查 `zh-CN/translation.json` 是否存在该键（类型基准）；
    其次检查其他语言是否补齐同名键；
    再核对命名空间与拼写（默认 `translation`）。

## 最佳实践

- 统一在 `zh-CN` 先维护键，其他语言同步跟进，保证类型安全与一致性。
- 按模块分组键名，例如：`home.title`、`home.button.start`，便于管理。
- 保持键稳定，避免频繁重命名，减少翻译与代码维护成本。
- 发布前将 `debug` 设为 `false`，以避免无用日志输出。

## 为什么客户端和服务端用法不一样

客户端与服务端在运行时语境不同，因此 API 设计不同：

- 单玩家 vs. 多玩家

  - 客户端是“单玩家会话”，一个页面只有一个当前语言（来自 `navigator.language`）。
  - 服务端同时服务多个玩家/会话，每个玩家语言都可能不同，不能用一个全局语言。

- 语言来源不同

  - 客户端本地即可读取浏览器语言并初始化 i18n 实例，导出绑定好的 `t`。
  - 服务端需要从客户端上报的事件中获知玩家语言，再按语言调用 `i18n.t(key, { lng: 玩家语言 })`。

- API 形态总结（单一共享模块 `@root/i18n`）
  - 客户端：直接使用 `i18n.t(key)`（内部已根据浏览器语言初始化）。
  - 服务端：调用 `i18n.t(key, { lng: 玩家语言 })`。
