299 lines
26 KiB
Markdown
299 lines
26 KiB
Markdown
# I18N.md — languages (中文, English, language packs)
|
||
|
||
Chinese is the default; every other language is a switch (the owner's decision of 2026-10-05): an English browser still
|
||
opens in Chinese until the player picks a language on the title screen (top right) or in 设置 / Settings. Every
|
||
language but Chinese is a **language pack** — a file in the language folder, nothing else (the owner's decision of
|
||
2026-10-07; §5 "Adding a language"): the menu lists every pack the server finds, each under its own name (buttons up to
|
||
four languages, a list beyond). English is a pack like any other; it ships with the game, complete. Japanese, Korean
|
||
and Traditional Chinese ship too, with the official game texts and complete UI strings made by machine, open to
|
||
corrections (§5 "Shipped packs"). The choice is kept in `localStorage` (`sp.pref.lang`); `?lang=<code>` in the address bar (`?lang=en`, `?lang=zh`) sets it
|
||
once and is then removed; a stored language whose pack is gone opens in Chinese. A switch re-renders the page in place
|
||
(no reload). Language packs are the first type of the content packs (docs/PACKS.md: the manifest, the registry, the
|
||
index, the pack folders).
|
||
|
||
Three kinds of text, three mechanisms — no i18n framework:
|
||
|
||
| Text | Source of a language | Mechanism |
|
||
|---|---|---|
|
||
| UI strings (buttons, labels, hints, toasts) | `public/i18n/<code>.json` — Chinese msgid → translation (English: `public/i18n/en.json`) | `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/<code>.json` — the official client texts by record id and field (English: the EN client) | `public/js/data.js` returns localized records |
|
||
| Server messages (m.toast, m.ticker, error codes) | the same `public/i18n/<code>.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:
|
||
|
||
```js
|
||
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
|
||
html`<span>${tParts('第 {r} 回合 · 最终攻势', { r: html`<b class="num">${r}</b>` })}</span>`; // markup inside a sentence
|
||
```
|
||
|
||
- **Placeholders** are the same in the msgid and the translation: `{name}`, positional `{0}`, and plural forms. Two
|
||
plain forms `{n|one|other}` (`'{n} {n|round|rounds}'`) pick `one` when the language's plural category of the number
|
||
is `one` (`Intl.PluralRules`; in English when n is 1 — as before packs, but for −1, now singular) and `other` otherwise. A language with
|
||
more categories names them: `{n|one:секунда|few:секунды|many:секунд|other:секунды}` (categories `zero`, `one`, `two`,
|
||
`few`, `many`, `other`; `other` is required; three or more plain forms are refused — the order of a language's
|
||
categories differs between engines). A plural form holds no placeholder of its own: write the number beside it
|
||
(`{n} {n|…}`). A value that is an array renders as a list joined with the language's separator (its translation of
|
||
`list::、`; `、` in Chinese, `, ` without one). A missing value leaves the placeholder as it is (official templates such
|
||
as `{0}博士…` pass through untouched).
|
||
- **Validation.** A translation must keep every plain `{name}` of its msgid (a plural form does not count: it does not
|
||
show the number), and may use another name only when the caller passes it (`'{n} {count|unit|units}'` with `count`
|
||
passed beside a markup `n`). A translation that drops or invents a placeholder, or has a broken plural form, is
|
||
skipped like a missing one — the next language of the chain shows — and reported once on the console of a
|
||
development page (a dev build, or a page served from this machine: localhost); `node tools/i18n.mjs check` flags it
|
||
against the call sites.
|
||
- **Fallback chain**, per string: the chosen pack → its base (`_meta.base`, else the language of a regional code:
|
||
`pt-BR` → `pt` when there is a `pt` pack; `zh-TW` has none — Chinese is the source) with the base's own chain → the
|
||
packs its `_meta.fallback` names (`["en"]`: what it lacks shows in English) → the Chinese msgid. A partial pack is
|
||
fine. `tc()` tries its context key and then the plain msgid in each language before the next one.
|
||
- **Markup inside a sentence** (a number in `<b>`, a `<kbd>`): `tParts(msgid, params)` returns the translation split at
|
||
its placeholders, with a param that is markup (a vnode, or an array of vnodes) kept as it is — the translator gets the
|
||
whole sentence and its word order, the Chinese pieces are the text around the markup exactly as before. Do not split
|
||
such a sentence into fragments (`第` … `回合`).
|
||
- **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; a memo over data records (the roster, the
|
||
strategy list, `useGameData()`) lists `data.locale()` among its dependencies, so the game texts follow the switch.
|
||
- Font samples, data keys and other Chinese that is not UI text can carry `// i18n-ignore` on the line.
|
||
|
||
### Translating
|
||
|
||
Edit the pack, e.g. `public/i18n/en.json` (`"msgid": "English"`, two-space JSON; `_meta` is its manifest, `_about` a
|
||
note; keys starting with `_` are never strings). Keep every `{placeholder}` (above); `test/i18n.test.js` checks that,
|
||
that no Chinese is left in an English value, and that every msgid the code uses is in English — English declares
|
||
`"complete": true`, and `node tools/i18n.mjs check --strict` holds a complete pack to every msgid, a partial pack only to
|
||
its placeholders. A translation may use more params than its msgid (`'{n} {count|unit|units}'` with `count` passed
|
||
beside a vnode `n`). Use the official terms of the game (for English 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()` / `tParts()` / `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 [<code>… | --all] [paths] [--strict] [--stale] [--list]` — per language pack (default `en`): how many of the
|
||
msgids the code uses it translates, and its errors — a translation that drops a placeholder, uses one no call site
|
||
passes, or has a broken plural form; a value that is not a string; a manifest problem. Missing strings are listed for
|
||
a complete pack (English) or with `--list`; `--stale` lists entries no code uses and no complete pack has. `--strict`
|
||
exits 1 on an error, or when a complete pack misses a msgid. `--root <dir>` reads the packs of another checkout.
|
||
- `template <code> [--fill <code>]` — writes `public/i18n/<code>.json`: a `_meta` manifest to fill in (the names from
|
||
`Intl.DisplayNames`, `"fallback": ["en"]` — none for a Chinese variant, whose gaps show the Chinese msgid) and every
|
||
msgid with an empty value in the order of English (`--fill en`: English values to overwrite). On an existing pack it
|
||
keeps the translations and `_meta` and adds the msgids the pack lacks — run it again after an update.
|
||
- `index` — the same as `node tools/packs.mjs index` (the pack index for a static host, docs/PACKS.md).
|
||
- `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'
|
||
one-line hotkey hint, before the 快捷键 section replaced it, was 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). Not player text,
|
||
so neither wrapped nor counted: `console.*` / logger calls (`log.warn(…)`, `this.m.log?.info?.(…)`), `Error` messages,
|
||
the `detail` of an error result (`{ error: 'BAD_TARGET', detail: '…' }`, `ev.detail = '…'` — players see `ERR_TEXT`), a
|
||
line marked `// i18n-ignore` and a file marked `i18n-ignore-file` in a comment (the developer invariant reports
|
||
`server/match/audit.js` / `invariants.js`, the bilingual HTTP error pages `server/http/*`). An aliased import
|
||
(`import { t as tr }`, where `t` is a local variable) is read as the function it names.
|
||
|
||
`test/i18n.test.js` scans what `extract` / `check` scan (public/js, shared, server) and fails on a Chinese literal left
|
||
unwrapped or a msgid missing from `public/i18n/en.json` — `extract` 0 and `check --strict` are part of the tests.
|
||
|
||
## 2. Game texts: `data/i18n/<code>.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)
|
||
node tools/build-i18n.mjs --lang ja # another official client: ja (JP), ko (KR), zh-TW (TW) → data/i18n/<code>.json
|
||
node tools/build-i18n.mjs --lang fr --dict fr-texts.json # no official client: a community dictionary (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). The same holds for the game texts of every pack: a
|
||
stale text of a pack falls back like a missing one (the next overlay of its chain, then Chinese) until the pack is
|
||
rebuilt.
|
||
|
||
- **Sources.** The English comes from the official EN client tables. Default `--source assets`:
|
||
[ArknightsAssets/ArknightsGamedata](https://github.com/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](https://github.com/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 escaped levels' 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`.
|
||
- **Other official clients** (`--lang`): ArknightsAssets/ArknightsGamedata also mirrors the JP, KR and TW clients
|
||
(`jp/`, `kr/`, `tw/gamedata`), each with both 盟约 seasons (checked 2026-10-07): `--lang ja`, `ko` and `zh-TW` build the
|
||
game texts of a pack of that language the same way (measured on that day: ja 96.5 %, ko 97.4 %, zh-TW 91.1 % of the
|
||
22 114 texts — the TW data is older, 51.2, and a Traditional text equal to the Simplified one counts as untranslated
|
||
although it shows the same). The remake's own strings (stage-name composites, the fallback files) are English only;
|
||
for another language the pack's UI file (`public/i18n/<code>.json`) is the fallback dictionary, and what is left falls
|
||
back per text at run time (the pack's chain, then Chinese). A game-text overlay ships with the UI pack of its language
|
||
(a pack is listed by its UI file): the repository generates English and, since the owner's decision of 2026-10-07,
|
||
`data/i18n/ja.json`, `ko.json` and `zh-TW.json` (§5 "Shipped packs"). Rebuild them like English after a data change
|
||
(`--lang <code> --check` says when one is out of date).
|
||
- **A language without an official client** (French, German, Russian …) needs no game texts at all: they follow the
|
||
pack's chain (`"fallback": ["en"]` shows the English ones). To translate some, write a community dictionary — a flat
|
||
JSON `{ "<Chinese data text>": "<translation>" }`; the `names` of `data/i18n/en.json` (every game-data name with its
|
||
English) is a good first list — and run `node tools/build-i18n.mjs --lang <code> --dict <file.json>`: it writes
|
||
`data/i18n/<code>.json` from the dictionary and the pack's UI file (exact matches only: a text whose numbers build-data
|
||
resolved needs its own entry), and every text it does not cover falls back per text at run time. `--dict` also adds to
|
||
an official client (tried after it).
|
||
- **Client**: `data.setLocale(lang, chain)` downloads the overlays of the chain once each — the pack's own, its base's,
|
||
a fallback's (`ui/lang.js dataChain`: the languages of the pack's chain that have game texts, with the URLs the pack
|
||
index gives; English ~1.5 MB, ~190 KB gzipped) — and from then on `get`, `lookup` and `list` (and every getter built
|
||
on them) return localized copies, **each text from the first overlay of the chain that has it**, else the Chinese one
|
||
(`shared/i18nData.js applyFileOverlay(json, overlay, onto)`); `data.getRaw(name)` keeps the Chinese file;
|
||
`data.localeName(zh)` translates a name through the chain.
|
||
|
||
## 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).
|
||
|
||
```js
|
||
this.m.toast(this, 'warn', msg('{names}只能部署在召唤者攻击范围内,已退回整备区', { names: back.map(dn) }));
|
||
```
|
||
|
||
Game texts the server copies into `m.public` / `m.private` in Chinese — a 机变 card's name and description, the draft's
|
||
name, an effect entry of the effects column, a reward offer's label, a result title — are shown from the localized data
|
||
on the client: names through `tName()` (every record name is in the overlay's `names`), texts through
|
||
`ui/lang.js sentText(sent, raw, local)`, which takes the localized record's text when the sent text is that record's own
|
||
Chinese text (`data.lookupRaw` / `data.getRaw`), else the sent text (a line the server reworded — the two-battle text of a
|
||
multi-round 悬赏, `server/match/choices.js bountyText` — stays Chinese). In Chinese both return what the server sent.
|
||
|
||
## 4. Coverage of the UI strings (phase 2, 0.2.0)
|
||
|
||
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
|
||
(2026-10-06) converted the rest: of the 1 128 Chinese literals `extract` listed, 1 056 go through `t()` / `tParts()` /
|
||
`N_()` — the briefing, the band draft, the game screen and its overlays, 干员调配 / 干员持有, the result (275), the HUD,
|
||
shop bar, detail card, bond strip, 机变 overlay, enemy drawer, team panel, guide, emotes, components and the client
|
||
models (579), the phase banners, refusal reasons and formats of `ui/gameLogic` (106), the canvas / fallback-field / facing
|
||
labels (16) and the msgid tables of `shared/constants.js` (80) — and 72 are marked as no player text (above). Now
|
||
`node tools/i18n.mjs extract` reports 0, `check` 1 002 msgids, all in `public/i18n/en.json` (1 015 entries). The Chinese
|
||
output did not change: a render of every converted file in Chinese (each `t()` call put back as the string it renders,
|
||
html`` parsed with htm, text runs compared) equals the code before the sweep; the few rewrites it cannot follow (a
|
||
variable folded into whole sentences, a list param in place of `.join('、')`, `fmtNum`'s English branch) were checked by
|
||
hand. Numbers: `fmtNum` writes 万 / 亿 in Chinese and K / M / B in English.
|
||
|
||
What stays Chinese in English, and why:
|
||
|
||
- player names, including the AI seats' (`AI·华法琳` …, `server/lobby.js BOT_NAMES`) — a name is never translated;
|
||
- a server text the client cannot match to a data record (the reworded multi-round 悬赏 text above);
|
||
- game texts the official EN client does not have yet (§2: some 自选 operators, newer modules);
|
||
- the 玩法说明 pages, which are the official Chinese pictures (their titles are translated).
|
||
|
||
Not player text, never translated: developer details and logs (`detail`, `log.*`, the invariant reports), the data keys
|
||
(`shared/highGround.js` reads the trait text 「可以放置于远程位」), the development STUB match, and the HTTP error pages,
|
||
which are bilingual on purpose.
|
||
|
||
## 5. Adding a language
|
||
|
||
**Shipped packs** (0.2.0; the owner's decisions of 2026-10-07): English and three packs with the official game texts
|
||
and complete UI strings — 日本語 (`ja`: the JP client, 96.5 % of the game texts), 한국어 (`ko`: KR, 97.4 %) and 繁體中文
|
||
(`zh-TW`: TW, 91.1 %; its data is older, and a Traditional text equal to the Simplified one counts as untranslated).
|
||
The three are made by machine and marked so (`"machineTranslated": true`: 设置 shows a note under the language switch
|
||
while one is in use); a string a pack lacks falls back to English (`ja`, `ko`: `"fallback": ["en"]`) or, for `zh-TW`, to
|
||
the Simplified Chinese msgid (no fallback — the owner's choice); `zh-TW` counts large numbers in 萬 / 億.
|
||
|
||
- **繁體中文** is a conversion, not a free translation: each msgid converted from the Simplified Chinese with OpenCC
|
||
(`cn` → `twp`, Traditional characters with Taiwan phrases), the official TW client's word kept where the phrase step
|
||
words a game concept differently (調度中心, not 排程中心; 更新 for the shop refresh; 啟動 for a bond; 遠程位; 查看; 目前 …;
|
||
a whole string the TW tables have, such as 時間較短的模擬訓練, as it is there), then a manual pass over the result
|
||
(one-to-many characters such as 只 / 隻, 重新整理 for a page refresh …). A msgid added later is converted the same way.
|
||
- **日本語 / 한국어** are machine translations that use the official JP / KR game terms. **Corrections are welcome**, to
|
||
any pack: edit the string in `public/i18n/<code>.json`, run `node tools/i18n.mjs check <code> --strict` (0 errors,
|
||
0 missing) and send a pull request on GitHub.
|
||
|
||
A new language is welcome too: `node tools/i18n.mjs template <code>` starts its pack (run on an existing pack, it keeps
|
||
the translations and `_meta` and adds every missing msgid with an empty value); then follow the steps below.
|
||
|
||
A language is a file in the language folder; the server lists it on the next page load (no restart, no build step, no
|
||
code change). For a community translator:
|
||
|
||
1. **Get the code and the tools**: a clone of the repository and `npm install` (the checker reads the code's msgids).
|
||
Pick the language's BCP 47 code in its usual case: `ja`, `ko`, `fr`, `pt-BR`, `zh-TW`.
|
||
2. **Make the skeleton**: `node tools/i18n.mjs template <code>` writes `public/i18n/<code>.json` — the `_meta` manifest
|
||
and every msgid (about 1 000) with an empty value, in the order of the English pack (`--fill en` fills in the English
|
||
to overwrite). Fill in `_meta`: `name` (the language in itself, shown in the menu), `englishName`, `authors`,
|
||
`version`; keep `"fallback": ["en"]` to show English where a string is still empty (drop it to show Chinese); set
|
||
`numberUnits` if the language counts large numbers in 10⁴ steps like Chinese (`["万", "億"]`), and
|
||
`"machineTranslated": true` when the strings come from a machine (players then see a note in 设置; docs/PACKS.md).
|
||
3. **Translate** the values (empty = not yet: it falls back). Keep every `{placeholder}` of the msgid; plural forms as
|
||
in §1; `public/i18n/en.json` is the reference for meaning and context (`ctx::msgid` keys are the same Chinese text in
|
||
another context). Use the official terms of your language's client where it has one.
|
||
4. **Check and try**: `node tools/i18n.mjs check <code>` — coverage, and errors to fix (a dropped or unknown
|
||
placeholder, a broken plural form). Start the server (`npm start`), pick the language on the title screen; a
|
||
translation the game skips is reported on the browser console. `node tools/packs.mjs list` says why a pack is not
|
||
listed (a file name not in its usual case, broken JSON …).
|
||
5. **Game texts** (optional): with an official client of the language, `node tools/build-i18n.mjs --lang <code>` writes
|
||
`data/i18n/<code>.json` (§2); without one, the game texts follow the chain, or a dictionary translates some of them
|
||
(`--dict`, §2). Share the pack as a pull request (the
|
||
file, plus the game-text file if built), or as a folder pack that a server installs by copying it into `packs/`
|
||
(`packs/<id>/pack.json` naming `ui.json` and `data.json`: docs/PACKS.md). After a game update, run `template <code>`
|
||
again: it keeps the translations and adds the new msgids.
|
||
|
||
The tests: `test/i18n-packs.test.js` (discovery, the fallback chain, placeholders, plurals in English and Russian, the
|
||
template and check commands, the `machineTranslated` mark — manifest, index, the client's registry, the 设置 note — and
|
||
that the Chinese and English output of `t()` is what it was before packs: every msgid of `public/i18n/en.json` rendered
|
||
with sample params, compared with the renderer of 0.2.0 before packs); `test/ui/i18n.e2e.test.js` (`SP_E2E=1`: the
|
||
shipped packs in the browser, their UI strings and the 设置 note).
|
||
|
||
## 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.
|