- Stock: fixed trait gifts (缪尔赛思 / 变形者集群 变形同构体, 耶拉 不融冰, 蜜蜡 / 卡涅利安 迅捷作战粮, 诗怀雅 盟约之币) still occupy stock but are granted past the cap; an operator card already shown in the shop or a reward can be taken after the pool runs out and overdraws (GitHub #498, the owner's decision); item buys and item rewards keep the stock check. 洛洛 / 焰尾 sub-pools use the measured weights (PR #497 by @2321Robin; the occupancy idea of PR #496 by @2321Robin re-implemented differently). - Boss pairs are drawn at random once per match and fixed to battle start; the Hidden Core reuses them; a leaver does not split a full pair (the owner's decision). - 阿戈尔 in 联防 no longer re-applies the opening devour HP loss or a false revive; Touch's team medic stays for the match after its holder leaves (owner-approved, official lifetime unverified). - Auto-skill ranges: 司霆惊蛰 S2, 斩业星熊 S3 against air, 予愿安洁莉娜 S3; 自在's post-rebirth invulnerability is also untargetable (#510); Horn / Clementia / catapult / 忍冬 first hit (PR #502 by @Xinglan233); 不融冰 / tear gas trigger on real damage. - Bond layer-up sound in round start / draft / prep / battle (#482): one channel, a new cue replaces the old, no extra cooldown; a prep update that adds layers and activates a bond plays one cue. - Client: multi-swing fix (#506), refresh single-flight (PR #507 by @robll-v1), LP tower keeps the settled value with a −N tick (PR #504 by @robll-v1), export with only 潜能 / 练度 changes (PR #503 by @Xinglan233), spectator rosters list DIY members (PR #505 by @Xinglan233), banned members beside bond tooltips (PR #494 by @YuriRestia), recovery action above the away overlay (PR #511 by @RainoutOvO), the /dev/sim-check.html consistency page (PR #492 by @Cloudnyco), Korean (PR #490 by @D-Roote) and English Victoria wording (PR #499 by @YuriRestia). - golden: 45 of 287 scenarios move against 0.2.4 (roster 17, bonds 3, fields 6, matches 17, standins 2), each attributed in docs/history/0.2.5.md §30.9 / §30.16.
27 KiB
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:
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}') pickonewhen the language's plural category of the number isone(Intl.PluralRules; in English when n is 1 — as before packs, but for −1, now singular) andotherotherwise. A language with more categories names them:{n|one:секунда|few:секунды|many:секунд|other:секунды}(categorieszero,one,two,few,many,other;otheris 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 oflist::、;、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}'withcountpassed beside a markupn). 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 checkflags 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→ptwhen there is aptpack;zh-TWhas none — Chinese is the source) with the base's own chain → the packs its_meta.fallbacknames (["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 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; a memo over data records (the roster, the strategy list,useGameData()) listsdata.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-ignoreon 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 int()/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 (defaulten): 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;--stalelists entries no code uses and no complete pack has.--strictexits 1 on an error, or when a complete pack misses a msgid.--root <dir>reads the packs of another checkout.template <code> [--fill <code>]— writespublic/i18n/<code>.json: a_metamanifest to fill in (the names fromIntl.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_metaand adds the msgids the pack lacks — run it again after an update.index— the same asnode 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 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' one-line hotkey hint, before the 快捷键 section replaced it, was 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). 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/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). - Corrections:
tools/i18n/corrections-<lang>.json(the pack's code:en,ja,ko,zh-TW; optional) is tried before the official data of that language, for an official translation that contradicts the Chinese text and the engine (exact match on the raw Chinese text; the plain twin is stripped from it; every entry carries a_why:note;_-keys are never written to the overlay). Onlycorrections-en.jsonexists. Counted ascorrectionin the coverage report. - 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), 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 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(alsomissing: every untranslated text) and inmeta.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,koandzh-TWbuild 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.jsonandzh-TW.json(§5 "Shipped packs"). Rebuild them like English after a data change (--lang <code> --checksays 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>" }; thenamesofdata/i18n/en.json(every game-data name with its English) is a good first list — and runnode tools/build-i18n.mjs --lang <code> --dict <file.json>: it writesdata/i18n/<code>.jsonfrom 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.--dictalso 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 onget,lookupandlist(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).
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, runnode 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:
- 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. - Make the skeleton:
node tools/i18n.mjs template <code>writespublic/i18n/<code>.json— the_metamanifest and every msgid (about 1 000) with an empty value, in the order of the English pack (--fill enfills 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); setnumberUnitsif the language counts large numbers in 10⁴ steps like Chinese (["万", "億"]), and"machineTranslated": truewhen the strings come from a machine (players then see a note in 设置; docs/PACKS.md). - Translate the values (empty = not yet: it falls back). Keep every
{placeholder}of the msgid; plural forms as in §1;public/i18n/en.jsonis the reference for meaning and context (ctx::msgidkeys are the same Chinese text in another context). Use the official terms of your language's client where it has one. - 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 listsays why a pack is not listed (a file name not in its usual case, broken JSON …). - Game texts (optional): with an official client of the language,
node tools/build-i18n.mjs --lang <code>writesdata/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 intopacks/(packs/<id>/pack.jsonnamingui.jsonanddata.json: docs/PACKS.md). After a game update, runtemplate <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.