Files
ark-stronghold-protocol/docs/I18N.md
T

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 in t().
  • Re-rendering: main.js App subscribes with useLang() (public/js/ui/lang.js), so the whole tree re-renders on a switch. A component that caches text outside rendering (a useMemo returning text, a module-level string) must depend on the language (useLang()) or move the t() call into rendering.
  • Font samples, data keys and other Chinese that is not UI text can carry // i18n-ignore on 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 in t() / 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 that public/i18n/en.json lacks (--strict exits 1; --stale lists 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 get N_() and are listed for a manual t() where they are shown; compared literals, object keys, string-method arguments, console.* and Error messages 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] — fills en.json from 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/ArknightsGamedata en/gamedata — current, and its activity_table has both 盟约 seasons (act1autochess, act2autochess, "Stronghold Protocol: Alliance") with the same ids as zh_CN. --source yostar: Kengxxiao/ArknightsGameData_YoStar en_US — archived in 2025-11, it predates 盟约 (only the 2025 test event act1vautochess) 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, so ui/richText.js renders 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), then public/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; names maps every record-level Chinese name to English (server messages and ticker lines name operators / items / bonds in Chinese).
  • Coverage (build of 2026-10-05 with the 自选 data, 22 183 texts): 97.6 % translated, 97.2 % from the official EN data. What stays Chinese is CN-only content the EN client does not have yet: some of the 71 owned 6★ of the 自选 pool (names, skills, talents) and some newer modules (before the 自选 data: 14 931 texts, 99.9 %). Per kind in .cache/build-i18n-report.json (also missing: every untranslated text) and in meta.coverage.
  • Client: data.setLocale(lang) downloads the overlay once (~1.5 MB, ~190 KB gzipped) and from then on get, lookup and list (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:

  1. node tools/i18n.mjs extract lists the rest (2026-10-05: 1 033 literals in 56 files — briefing, band draft, game screen and its HUD / panels / overlays, loadout, result, guide, connection banner, components …; 964 of them already have a translation seeded from PR #70).
  2. 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), then node tools/i18n.mjs check and translate what is missing, in the official terms above.
  3. 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.
  4. public/js/ui/guide.js chapter / page titles, the GuideButton / LoadoutButton default labels and tooltips.
  5. When the sweep is complete: node tools/i18n.mjs check --strict in CI, and extract reporting 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.