Skip to content

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.jsonpaths 配置。

导出内容

  • 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 });
  1. 在 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 初始化)。
  • 服务端调用时请显式传 lngi18n.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 },
}
  1. 运行时可通过 i18n.changeLanguage("ja") 切换。

进阶:多命名空间资源(Multiple Namespaces)

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

目录组织(示例)

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.tsdebug: true 会在控制台提示缺失键;发布前改为 false
  • Q:浏览器语言是 en-US 也能命中吗?

    • A:可以,会匹配到 en;若未命中,则根据 fallbackLng: "zh-CN" 回退。
  • Q:t() 返回键名本身,怎么办?

    • A:优先检查 zh-CN/translation.json 是否存在该键(类型基准); 其次检查其他语言是否补齐同名键; 再核对命名空间与拼写(默认 translation)。

最佳实践

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

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

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

  • 单玩家 vs. 多玩家

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

    • 客户端本地即可读取浏览器语言并初始化 i18n 实例,导出绑定好的 t
    • 服务端需要从客户端上报的事件中获知玩家语言,再按语言调用 i18n.t(key, { lng: 玩家语言 })
  • API 形态总结(单一共享模块 @root/i18n

    • 客户端:直接使用 i18n.t(key)(内部已根据浏览器语言初始化)。
    • 服务端:调用 i18n.t(key, { lng: 玩家语言 })

神岛实验室