NOTICE.md and THIRD-PARTY-NOTICES.md credit @YuriRestia (PR #70) for the English UI strings and fallback translations, note that PR #70's ak-spa-database.pages.dev texts are not used and that arknights.wiki.gg is CC BY-SA 4.0, and name the EN data mirrors (ArknightsAssets/ArknightsGamedata, Kengxxiao/ArknightsGameData_YoStar). DATA.md: data/i18n/en.json is written by tools/build-i18n.mjs (rerun after a text-changing build). PLAYING.md: the 中文 / English switch.
12 KiB
I18N.md — languages (中文 / English)
Chinese is the default; English is a switch (the owner's decision of 2026-10-05): an English browser still opens in
Chinese until the player picks English on the title screen (top right) or in 设置 / Settings. The choice is kept in
localStorage (sp.pref.lang); ?lang=en / ?lang=zh in the address bar sets it once and is then removed. A switch
re-renders the page in place (no reload).
Three kinds of text, three mechanisms — no i18n framework:
| Text | Source of the English | Mechanism |
|---|---|---|
| UI strings (buttons, labels, hints, toasts) | public/i18n/en.json — Chinese msgid → English |
t() in the code (shared/i18n.js) |
| Game texts (operators, skills, talents, modules, traits, enemies, bonds, items, effects, strategies, 特质, 机变 cards, modes, stages, tokens) | data/i18n/en.json — the official EN client texts by record id and field |
public/js/data.js returns localized records |
| Server messages (m.toast, m.ticker, error codes) | the same public/i18n/en.json |
the server sends msgid + params, the client translates |
1. UI strings: t()
gettext style — the Chinese source string is the message id, so an untranslated string shows Chinese, never a key:
import { t, tc, N_ } from '../../../shared/i18n.js';
t('整备区已满'); // 'Bench is full' in English
t('还剩 {n} 秒', { n }); // params: {name}; arrays fill {0}, {1} …
t('同盟密钥为 {ROOM_CODE_LEN} 位字母或数字', { ROOM_CODE_LEN });
tc('toggle', '关闭'); // context: the key 'toggle::关闭' ("Off"), else '关闭' ("Close")
const TABLE = { a: N_('独立模拟') }; // module level: N_ only marks it; call t(TABLE.a) where it is shown
- Placeholders are the same in the msgid and the translation:
{name}, positional{0}, and one plural form for English,{n|one|other}('{n} {n|round|rounds}'). A value that is an array renders as a list (、/,). A missing value leaves the placeholder as it is (official templates such as{0}博士…pass through untouched). - Game-data names inside a message are marked
dn(name)({ dn: '…' }) and translated through the localized data (tName(name)does the same for a bare name). Player names are never translated — pass them as plain strings. - Data texts are not msgids:
getChess(id).name,getMode(id).desc,data.get('config').tips… already come in the current language (§2). Do not wrap them int(). - Re-rendering:
main.jsApp subscribes withuseLang()(public/js/ui/lang.js), so the whole tree re-renders on a switch. A component that caches text outside rendering (auseMemoreturning text, a module-level string) must depend on the language (useLang()) or move thet()call into rendering. - Font samples, data keys and other Chinese that is not UI text can carry
// i18n-ignoreon the line.
Translating
Edit public/i18n/en.json ("msgid": "English", two-space JSON; _about is metadata). Keep every {placeholder};
test/i18n.test.js checks that, that no Chinese is left in a value, and that every msgid of the converted screens is
present. Use the official EN terms of the game (the EN client's own wording, e.g. its tips and mode texts):
| 中文 | English | 中文 | English |
|---|---|---|---|
| 盟约 | Alliance | 调度中心 | Dispatch Center |
| 整备区 / 临时整备区 | Bench / temporary Bench | 资金 | Funds |
| 休整期 | Rest Phase | 机变阶段 | Improv Phase |
| 联防 / 联防阶段 | Unite / Unite Phase | 最终攻势 | Final Assault |
| 隐秘核心 | Hidden Core | 目标生命值 | LP |
| 联合模拟 / 同盟模拟 | Team Simulation | 独立模拟 | Solo Simulation |
| 标准 / 险境 / 绝境 / 终极模拟 | Standard / Perilous / Dire / Ultimate Simulation | 入门协议 | Entry Protocol |
| 策略 | Strategy | 特质 | Attribute |
| 精锐(干员) | Elite (Operator) | 层数 | stacks |
| 同盟密钥 | Alliance Key | 创建者 | Host |
Tools: node tools/i18n.mjs
extract [paths] [--list]— per file: the msgids int()/tc()/N_()/msg(), and the Chinese literals still shown untranslated; the total says how many of those already have a translation (ready for the codemod).check [paths] [--strict] [--stale]— msgids the code uses thatpublic/i18n/en.jsonlacks (--strictexits 1;--stalelists entries no code uses yet).codemod <files> [--write]— wraps a file's Chinese literals: html`` text and attribute values →${t('…')}, strings →t('…'), template literals →t('…{name}…', { name })(an expression's param name: identifier → itself,a.b→b,x.length→n, else its position), nested strings in params included; adds the import. Module-level literals getN_()and are listed for a manualt()where they are shown; compared literals, object keys, string-method arguments,console.*andErrormessages are left alone; a text run with a child that is not a plain value is split into its static pieces. Review the diff: join fragments into whole sentences where it reads better (the settings hotkey line is an example), check module-level tables.seed --from <map.json> [--all] [--write]— fillsen.jsonfrom a flat{ msgid: English }map for the msgids the code uses (--all: also the literals not wrapped yet, under the msgid the codemod will give them).
Server code is scanned too: the first argument of msg() and a Chinese literal handed to m.toast / ctx.toast /
tickerText are msgids (server/sim only for those — its own t() helpers are not translation calls).
2. Game texts: data/i18n/en.json
node tools/build-i18n.mjs # downloads the EN tables once into .cache/gamedata-en/, writes data/i18n/en.json
node tools/build-i18n.mjs --offline # never download (the zh_CN tables come from .cache/gamedata, as for build-data)
node tools/build-i18n.mjs --check # exit 1 when data/i18n/en.json is out of date (nothing written)
node tools/build-i18n.mjs --source yostar # the other EN source (see below)
Rebuild it after every node tools/build-data.mjs that changes a text: test/i18n-data.test.js fails while a
translation's Chinese source no longer matches data/*.json, and at run time such a stale leaf is not applied (the
player sees the current Chinese text, never an outdated number).
- Sources. The English comes from the official EN client tables. Default
--source assets: ArknightsAssets/ArknightsGamedataen/gamedata— current, and itsactivity_tablehas both 盟约 seasons (act1autochess,act2autochess, "Stronghold Protocol: Alliance") with the same ids as zh_CN.--source yostar: Kengxxiao/ArknightsGameData_YoStaren_US— archived in 2025-11, it predates 盟约 (only the 2025 test eventact1vautochess) and many operators, so most mode texts and new operators stay Chinese with it. - Matching (no id table to maintain): the zh_CN and EN tables are walked side by side; a data text equal to a
Chinese source takes its English; a text whose blackboard placeholders build-data resolved (
{atk:0%}→80%) is matched against the Chinese templates of the same shape, its numbers are read back and the English template is filled with them (the remake's numbers, not the EN server's); stage names are assembled from their parts. Ties prefer the source on the record's own ids, then the 下半 season (act2autochess). Rich-text tags and placeholders are the same as in Chinese, soui/richText.jsrenders the English unchanged (a few official EN texts leave a tag unclosed; the parser runs it to the end of the text, as the game does). - Fallbacks, in order:
tools/i18n/fallback-remake.json(the remake's own strings: stage-name parts, item notes, the 联防 map names, the season name),tools/i18n/fallback-pr70.json(PR #70's translations of texts the EN client lacks: modules of CN-only operators, the 甄选 slots, 结算称号 criteria, emote labels), thenpublic/i18n/en.json; anything else stays Chinese. Research notes (bonds.*.spec,implFormula,assumed…) are never translated. - Format (
shared/i18nData.js):{ version, lang, meta: { source, coverage, totals }, names, files }.files[file][id]mirrors the record — string leaves replace the Chinese at the same path, arrays are index-aligned (null = keep) — with_h, a 2-character hash of each replaced Chinese string;namesmaps every record-level Chinese name to English (server messages and ticker lines name operators / items / bonds in Chinese). - Coverage (build of 2026-10-05, 14 931 texts): 99.9 % translated, 99.3 % from the official EN data; only nine
CN-only operators of the 甄选 pool keep Chinese names. Per kind in
.cache/build-i18n-report.json(alsomissing: every untranslated text) and inmeta.coverage. - Client:
data.setLocale(lang)downloads the overlay once (~1.5 MB, ~190 KB gzipped) and from then onget,lookupandlist(and every getter built on them) return localized copies;data.getRaw(name)keeps the Chinese file;data.localeName(zh)translates a name.
3. Server messages
The server sends a toast or a custom ticker line as a plain string (its own msgid) or as
msg(msgid, params) (shared/i18n.js). Frames keep text — the Chinese rendering, what older clients show — and add
msgid / params; data names are dn() params. A config.broadcasts line (tickerFor) also carries its args, and
the client fills the localized broadcast template (ui/lang.js tickerText; {0} is the player's name and is never
translated). Error frames already carry their ERR code: ui/toasts.js describeError translates ERR_TEXT[code]. The
protocol is backward compatible both ways (new fields are optional).
this.m.toast(this, 'warn', msg('{names}只能部署在召唤者攻击范围内,已退回整备区', { names: back.map(dn) }));
4. Phase 2 (after the 0.2.0 feature screens land)
Phase 1 converted the title screen, the lobby, the room, the settings panel (with the language switch), the app shell
(main.js: toasts, close reasons, the crash screen), ui/toasts.js, DifficultyTag and the server messages. Phase 2:
node tools/i18n.mjs extractlists the rest (2026-10-05: 1 036 literals in 57 files — briefing, band draft, game screen and its HUD / panels / overlays, loadout, result, guide, connection banner, components …; 966 of them already have a translation seeded from PR #70).- Per file (one stream per file group to avoid conflicts):
node tools/i18n.mjs codemod <file> --write, review the diff (module-level tables:t()at the use site; join split sentences;tc()where one Chinese text needs two English ones), thennode tools/i18n.mjs checkand translate what is missing, in the official terms above. - Texts the server puts in
m.public/m.private(机变 card names and descriptions, effect names in views) should be looked up by id on the client (getEffect/data.lookup) instead of shown as sent. public/js/ui/guide.jschapter / page titles, theGuideButton/LoadoutButtondefault labels and tooltips.- When the sweep is complete:
node tools/i18n.mjs check --strictin CI, andextractreporting 0.
Credits
The English UI strings were seeded from GitHub PR #70 (branch en-translation) by @YuriRestia, reused with credit
(the owner's decision of 2026-10-05); see NOTICE.md and THIRD-PARTY-NOTICES.md. PR #70's texts taken from
ak-spa-database.pages.dev (no licence) are not used. The English game texts are the official EN client data
(© Hypergryph / Yostar), via the community mirrors named above.