i18n 使用指南
引言
本指南覆盖 ArenaPro 的国际化(i18n)方案,包含客户端与服务端:初始化位置、类型安全(t-keys 自动补全)、如何在代码中使用 t()
、如何新增翻译键/语言,以及运行时切换语言等。
传统工作流的痛点
程序小明:先写个文案吧,先用简体中文写死,繁体中文之后再说。
产品同学:英文版什么时候好?日语也要支持一下。
临时硬编码的文案、分散在各处的字符串、手动复制的键名与路径,都会在多人协作中迅速变成维护“噩梦”。
ArenaPro 的 i18n 实践能把文案“像资源一样”统一管理,并让 VS Code 在编码时提供键名自动补全与错误检查:
- 类型安全的键名提示,避免拼写错误与缺键。
- 开发期即暴露缺失翻译,避免上线后才发现问题。
- 统一的文件组织,新增键/语言有章可循。
目录与文件
共享初始化与导出(客户端/服务端通用):
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
,再同步到其他语言文件,才能保证类型安全与一致性。
如何使用
- 直接使用导出的
i18n
:
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 });
- 在 UI 代码中使用(示例):
import i18n from "@root/i18n";
const text = UiText.create();
text.parent = ui;
text.textContent = i18n.t("welcome_game");
服务端指引
同样使用共享的 @root/i18n
,服务端由于没有 navigator.language
,请在调用时显式传入玩家语言:
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
下面列出项目中最常用的几种写法:
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 })
。
运行时切换语言
你可以在运行时切换语言,例如:
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
为“基准语言”:
- 在
i18n/res/zh-CN/translation.json
中新增/修改键值。 - 在
i18n/res/en/translation.json
(以及其他语言文件)中补齐相同键。 - 在代码中使用
i18n.t("你的新键")
。
示例:
- 在
i18n/res/zh-CN/translation.json
增加:
{
"common": {
"ok": "确定",
"cancel": "取消"
}
}
- 在
i18n/res/en/translation.json
增加对应键:
{
"common": {
"ok": "OK",
"cancel": "Cancel"
}
}
- 代码中使用:
import { t } from "@root/i18n";
const ok = t("common.ok");
const cancel = t("common.cancel");
新增一种语言
- 在项目根
i18n/res/
下新增目录与translation.json
,如:i18n/res/ja/translation.json
。 - 在
i18n/index.ts
中:import ja_Translation from "./res/ja/translation.json";
- 在
resources
中添加:
resources: {
en: { translation: en_Translation },
"zh-CN": { translation: zhCN_Translation },
ja: { translation: ja_Translation },
}
- 运行时可通过
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
示例内容:
// zh-CN/ui.json
{
"title": "开始游戏",
"button": { "start": "开始", "quit": "退出" }
}
i18n 初始化配置(i18n/index.ts
)
将单一的 translation
拆分为多个命名空间,并在 resources
中分别挂载:
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
参数指定命名空间。
import i18n from "@root/i18n";
const title = i18n.t("title", { ns: "ui" }); // 取 ui 命名空间下的 title
const start = i18n.t("button.start", { ns: "ui" });
方式二:获取固定命名空间的 t
(推荐在同一模块大量使用同一 ns 时)。
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
调整为多命名空间结构。示例:
// 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
。
- A:
Q:浏览器语言是
en-US
也能命中吗?- A:可以,会匹配到
en
;若未命中,则根据fallbackLng: "zh-CN"
回退。
- A:可以,会匹配到
Q:
t()
返回键名本身,怎么办?- A:优先检查
zh-CN/translation.json
是否存在该键(类型基准); 其次检查其他语言是否补齐同名键; 再核对命名空间与拼写(默认translation
)。
- A:优先检查
最佳实践
- 统一在
zh-CN
先维护键,其他语言同步跟进,保证类型安全与一致性。 - 按模块分组键名,例如:
home.title
、home.button.start
,便于管理。 - 保持键稳定,避免频繁重命名,减少翻译与代码维护成本。
- 发布前将
debug
设为false
,以避免无用日志输出。
为什么客户端和服务端用法不一样
客户端与服务端在运行时语境不同,因此 API 设计不同:
单玩家 vs. 多玩家
- 客户端是“单玩家会话”,一个页面只有一个当前语言(来自
navigator.language
)。 - 服务端同时服务多个玩家/会话,每个玩家语言都可能不同,不能用一个全局语言。
- 客户端是“单玩家会话”,一个页面只有一个当前语言(来自
语言来源不同
- 客户端本地即可读取浏览器语言并初始化 i18n 实例,导出绑定好的
t
。 - 服务端需要从客户端上报的事件中获知玩家语言,再按语言调用
i18n.t(key, { lng: 玩家语言 })
。
- 客户端本地即可读取浏览器语言并初始化 i18n 实例,导出绑定好的
API 形态总结(单一共享模块
@root/i18n
)- 客户端:直接使用
i18n.t(key)
(内部已根据浏览器语言初始化)。 - 服务端:调用
i18n.t(key, { lng: 玩家语言 })
。
- 客户端:直接使用