Internationalization
Introduction
This guide covers ArenaPro's internationalization (i18n) setup for both client and server: where to initialize, type safety (t-keys autocompletion), how to use t()
in code, how to add new keys/languages, and how to switch language at runtime.
Pain points of the traditional workflow
- Hard-coded strings scattered across the codebase are hard to maintain.
- Key names get copied manually and easily go out of sync across languages.
- Non-developers cannot safely contribute to localization.
ArenaPro's i18n practice treats text resources as first-class assets and gives you IDE autocompletion and type checks:
- Type-safe key suggestions to avoid typos and missing keys.
- Missing translations are revealed during development, not after release.
- Unified file organization so adding keys/languages follows a clear process.
Directory & Files
- Shared initialization and exports (client/server shared):
i18n/index.ts
- i18n type augmentation (ensure t-keys type safety):
i18n/types/i18n.d.ts
- Translation resources (JSON):
- Simplified Chinese:
i18n/res/zh-CN/translation.json
- English:
i18n/res/en/translation.json
- Simplified Chinese:
We use
res/zh-CN/translation.json
as the single source of truth for key structure to guarantee type safety and editor autocompletion.
Initialization & How it Works
In i18n/index.ts
:
- Initialize
i18next
. lng
is auto-detected fromnavigator.language
on the client (fallback tozh-CN
on server).fallbackLng
iszh-CN
.- Default namespace (
defaultNS
) istranslation
. - Resources are imported inline:
en
→@root/i18n/res/en/translation.json
zh-CN
→@root/i18n/res/zh-CN/translation.json
debug: false
by default. Set totrue
during development to surface missing keys.
Note: The
@root/*
alias comes fromclient/tsconfig.json
paths
.
Exports
default export i18n
: usei18n.t('key', options?)
directly.
Type Safety & Autocompletion (module augmentation)
Through i18n/types/i18n.d.ts
, we augment i18next
types using the structure of zh-CN/translation.json
as the type source. When you call i18n.t('...')
, you get autocompletion and compile-time checks.
Therefore, when adding/editing keys, first update i18n/res/zh-CN/translation.json
, then sync to other language files to keep type safety and consistency.
How to Use
- Basic usage:
import i18n from "@root/i18n";
// simplest usage
const text = i18n.t("welcome_game");
// interpolation example (translation.json: "navigator.language": "Your browser language is: {{language}}")
const langText = i18n.t("navigator.language", { language: navigator.language });
- In UI code:
import i18n from "@root/i18n";
const text = UiText.create();
text.parent = ui;
text.textContent = i18n.t("welcome_game");
Server Guide
Use the shared @root/i18n
as well. Since there is no navigator.language
on the server, pass the player's language explicitly when calling:
import i18n from "@root/i18n";
// Shows the Chinese welcome text
console.log("(server):", i18n.t("welcome_game", { lng: "zh-CN" }));
// Shows the English welcome text
console.log("(server):", i18n.t("welcome_ap", { lng: "en" }));
Quick Reference: options for t()
Full docs: https://www.i18next.com/translation-function/essentials
import i18n from "@root/i18n";
// 1) force language (server or strict cases)
i18n.t("welcome_game", { lng: "en" });
// 2) namespace
i18n.t("title", { ns: "ui" });
// 3) interpolation
// translation.json: { "hello": "Hello, {{name}}" }
i18n.t("hello", { name: "Alice" });
// 4) plural
// translation.json: { "item": "{{count}} item", "item_one": "{{count}} item", "item_other": "{{count}} items" }
i18n.t("item", { count: 1 });
i18n.t("item", { count: 5 });
// 5) context
// translation.json: { "actor": "Actor", "actor_male": "Actor (male)", "actor_female": "Actor (female)" }
i18n.t("actor", { context: "male" });
i18n.t("actor", { context: "female" });
// 6) default value
i18n.t("not.exist.key", { defaultValue: "Fallback text" });
// 7) return objects
// i18n.t('group', { returnObjects: true })
// 8) join arrays
// i18n.t('list', { joinArrays: ', ' })
Tips:
- Client usually does not pass
lng
(initialized fromnavigator.language
). - Server should pass
lng
:i18n.t(key, { lng: playerLang })
.
Runtime Language Switching
import i18n from "@root/i18n";
async function switchToEnglish() {
await i18n.changeLanguage("en");
}
async function switchToChinese() {
await i18n.changeLanguage("zh-CN");
}
After switching, calls to i18n.t()
will immediately reflect the new language. If your UI framework is not reactive, manually trigger a refresh.
Maintenance & Expansion
Add or maintain translation keys
Use zh-CN
as the baseline language:
- Update
i18n/res/zh-CN/translation.json
. - Sync the same keys to
i18n/res/en/translation.json
(and other languages). - Use
i18n.t("your.new.key")
in code.
Example:
// zh-CN/translation.json
{
"common": {
"ok": "确定",
"cancel": "取消"
}
}
// en/translation.json
{
"common": {
"ok": "OK",
"cancel": "Cancel"
}
}
Usage:
import { t } from "@root/i18n";
const ok = t("common.ok");
const cancel = t("common.cancel");
Add a new language
- Create a directory and
translation.json
underi18n/res/
, e.g.i18n/res/ja/translation.json
. - In
i18n/index.ts
:
import ja_Translation from "./res/ja/translation.json";
await i18n.init({
resources: {
en: { translation: en_Translation },
"zh-CN": { translation: zhCN_Translation },
ja: { translation: ja_Translation },
}
});
- At runtime, switch with
i18n.changeLanguage("ja")
.
Advanced: Multiple Namespaces
When your project grows, stuffing everything into one translation.json
becomes hard to maintain. Split by domain (e.g., common
, ui
, gameplay
).
Directory organization (example)
i18n/res/
en/
common.json
ui.json
gameplay.json
zh-CN/
common.json
ui.json
gameplay.json
i18n initialization (i18n/index.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;
Use multiple namespaces in code
Way 1: Pass ns
when calling.
import i18n from "@root/i18n";
const title = i18n.t("title", { ns: "ui" });
const start = i18n.t("button.start", { ns: "ui" });
Way 2: Get a fixed t
for a specific namespace (recommended when used heavily within one module).
import i18n from "@root/i18n";
const tUI = i18n.getFixedT(null, "ui");
const title = tUI("title");
const start = tUI("button.start");
Types: keep type safety with multiple namespaces
If you provide key autocompletion via module augmentation in i18n/types/i18n.d.ts
, reshape the resources
type accordingly:
// i18n/types/i18n.d.ts
import "i18next";
declare module "i18next" {
interface CustomTypeOptions {
defaultNS: "common";
// Note: use zh-CN files as the type source of truth
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");
};
}
}
Tips:
- Add new keys to
zh-CN
first, then mirror to others. - Within a module, prefer a fixed
t
(e.g.,const tUI = getFixedT(..., "ui")
). - Don't over-split namespaces; keep them domain-oriented.
FAQ
How to surface missing keys during development?
- Set
debug: true
ini18n/index.ts
(turn it off before release).
- Set
Will
en-US
matchen
?- Yes. If not matched, it falls back according to
fallbackLng: "zh-CN"
.
- Yes. If not matched, it falls back according to
t()
returns the key itself—what now?- First check that
zh-CN/translation.json
includes the key (the type baseline). - Then verify other languages mirror the same key.
- Finally confirm namespace and spelling (default is
translation
).
- First check that
Best Practices
- Maintain keys in
zh-CN
first, then sync others to keep types and content consistent. - Group keys by module, e.g.,
home.title
,home.button.start
. - Keep keys stable—avoid frequent renaming to reduce translation and maintenance cost.
- Set
debug
tofalse
before release to avoid noisy logs.
Why client and server usage differs
They operate in different runtime contexts:
Single-player vs multi-player
- Client is a single user session with one current language (from
navigator.language
). - Server serves many players/sessions; each may have a different language—no global language.
- Client is a single user session with one current language (from
Language source differs
- Client initializes based on browser language and exports a bound
t
. - Server must read/report the player's language, then call
i18n.t(key, { lng: playerLang })
.
- Client initializes based on browser language and exports a bound
API summary (single shared module
@root/i18n
)- Client:
i18n.t(key)
directly. - Server:
i18n.t(key, { lng })
with explicit language.
- Client:
"Our game's comment section is blowing up!" came the urgent message in the creative team's QQ group.
You open the player feedback panel to see a sea of red alerts:
- Japanese player Yamamoto: "中国語が読めない!" (Can't read Chinese!)
- British player Tom: "Why is there no English version? I'm crying!"
- French player Luc: "Où est la version française?" (Where is the French version?)
- German player Hans: "Kann man die Sprache ändern?" (Can the language be changed?)
As the creator, you know you must immediately solve this language barrier problem! This is where i18next comes to the rescue! It's a powerful internationalization framework that can help you easily implement multilingual support.
Installation
npm install i18next
Info
It is recommended to implement localized configurations in client-side scripts rather than directly integrating them into server-side scripts. This architecture design can better meet the personalized needs of different users, while following the best practices for front-end processing of user specific configurations.
Basic Configuration
// i18n.ts
import i18n from "i18next";
// Initialization configuration
i18n.init({
lng: "en", // Default language
fallbackLng: "en", // Fallback language
debug: false, // Should be false in production
resources: {
en: {
translation: {
// English translations
welcome: "Welcome!",
greeting: "Hello, {{name}}!",
buttons: {
submit: "Submit",
cancel: "Cancel",
},
},
},
zh: {
translation: {
// Chinese translations
welcome: "欢迎!",
greeting: "你好,{{name}}!",
buttons: {
submit: "提交",
cancel: "取消",
},
},
},
},
});
export default i18n;
Core API Usage
1. Basic Translation
import i18n from "./i18n";
// Simple translation
console.log(i18n.t("welcome")); // Output: "Welcome!" or "欢迎!"
// With interpolation
console.log(i18n.t("greeting", { name: "Alice" })); // "Hello, Alice!" or "你好,Alice!"
// Nested keys
console.log(i18n.t("buttons.submit")); // "Submit" or "提交"
2. Language Switching
// Get current language
console.log(i18n.language); // Output current language code
// Change language
i18n.changeLanguage("zh").then(() => {
console.log("Language switched to Chinese");
});
// Listen for language changes
i18n.on("languageChanged", (lng) => {
console.log(`Language changed to: ${lng}`);
});
3. Pluralization
// Configure plural rules
i18n.init({
// ...other configurations
resources: {
en: {
translation: {
itemCount: "{{count}} item",
itemCount_plural: "{{count}} items",
},
},
zh: {
translation: {
itemCount: "{{count}} 个项目",
},
},
},
});
// Usage
console.log(i18n.t("itemCount", { count: 1 })); // "1 item" or "1 个项目"
console.log(i18n.t("itemCount", { count: 5 })); // "5 items" or "5 个项目"
Multilingual Configuration Files
MyGame/
├── i18n/
│ ├── config.ts # i18n initialization config file
│ └── locales/ # Multilingual resources directory
│ ├── en/ # English language pack
│ │ ├── common.json # Common translations
│ │ └── home.json # Homepage-related translations
│ └── zh/ # Chinese language pack
│ ├── common.json # Common translations
│ └── home.json # Homepage-related translations
1. Create JSON Translation Files
English Translation File Example (en/common.json
)
{
"welcome": "Welcome to our application!",
"greeting": "Hello, {{name}}!",
"buttons": {
"submit": "Submit",
"cancel": "Cancel"
},
"itemCount": "{{count}} item",
"itemCount_plural": "{{count}} items"
}
Chinese Translation File Example (zh/common.json
)
{
"welcome": "欢迎使用我们的应用!",
"greeting": "你好,{{name}}!",
"buttons": {
"submit": "提交",
"cancel": "取消"
},
"itemCount": "{{count}} 个项目"
}
zh/home.json
and en/home.json
files are similar and not shown here.
2. Load JSON Configuration
Create i18n/config.ts
configuration file:
import i18n from "i18next";
import zhCNcommon from "./locales/common.json";
import enCommon from "./locales/common.json";
import zhCNhome from "./locales/home.json";
import enHome from "./locales/home.json";
i18n.init({
lng: "en", // Default language set to English
fallbackLng: "en", // Fallback language is also English
supportedLngs: ["en", "zh-CN"], // Supported languages list
resources: {
// Translation resources
en: {
// English resources
translation: enHome, // Default namespace translations
common: enCommon, // 'common' namespace translations
},
"zh-CN": {
// Simplified Chinese resources
translation: zhCNhome, // Default namespace translations
common: zhCNcommon, // 'common' namespace translations
},
},
});
export default i18n;
Usage:
console.log(i18n.t("welcome")); // Outputs welcome from default namespace
console.log(i18n.t("common:welcome")); // Outputs welcome from common namespace
TypeScript Type Support
Create i18n.d.ts
file for enhanced types:
// i18n.d.ts
import "i18next";
import enTranslation from "../locales/en/home.json";
import enCommon from "../locales/en/common.json";
// ...other imports
declare module "i18next" {
interface CustomTypeOptions {
defaultNS: "translation";
resources: {
translation: typeof enTranslation;
common: typeof enCommon;
};
}
}
Summary
Advantages of using pure i18next package in TypeScript client:
- Lightweight: No dependency on other frameworks
- Powerful Features: Supports plurals, interpolation, context, and other advanced features
- Type Safety: Full TypeScript support
- Flexible Extensibility: Plugins can be added as needed
Advantages of configuring i18next with JSON:
- Separation of Concerns: Translation text is separated from code logic
- Easy Maintenance: Non-technical staff can edit translation files
- Version Control Friendly: JSON files are easy to manage in version control and collaborate on
With this configuration approach, you can easily manage internationalization needs for large applications while maintaining clean and maintainable code.