feedback5 WAD: docs for language packs and content packs — docs/PACKS.md (the manifest, the two layouts, discovery and the index, what is served, the types and how a later type plugs in), docs/I18N.md §5 "Adding a language" (step by step for a translator) with the fallback chain, placeholder validation and plural categories, CONTRIBUTING / README pointers, DEPLOY §7 (packs/ and packs/index.json in the zips), ARCHITECTURE, docs/history/0.2.0.md §25.16; build-i18n --dict (a community dictionary for a language without an official client); the browser check of a pack file and a pack folder dropped in while the server runs (test/ui/i18n.e2e.test.js); golden unchanged

This commit is contained in:
sganggs committed 2026-10-07 02:39:11 +08:00
1 parent 25b2a87b26
commit 1ee39a79b5
11 files changed
+466 -64

No files matched your search

+107 -23
View File
@@ -1,17 +1,22 @@
# I18N.md — languages (中文 / English)
# I18N.md — languages (中文, English, language packs)
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).
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 and is the complete one. 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 the English | Mechanism |
| Text | Source of a language | 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 |
| 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()`
@@ -27,9 +32,25 @@ const TABLE = { a: N_('独立模拟') }; // module level: N_ only marks it;
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 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).
- **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
@@ -46,10 +67,13 @@ html`<span>${tParts('第 {r} 回合 · 最终攻势', { r: html`<b class="num">$
### 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 the code uses is present. A
translation may use more params than its msgid (`'{n} {count|unit|units}'` with `count` passed beside a vnode `n`). Use
the official EN terms of the game (the EN client's own wording, e.g. its tips and mode texts):
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 |
|---|---|---|---|
@@ -69,8 +93,16 @@ the official EN terms of the game (the EN client's own wording, e.g. its tips an
- `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 [paths] [--strict] [--stale]` — msgids the code uses that `public/i18n/en.json` lacks (`--strict` exits 1;
`--stale` lists entries no code uses yet).
- `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
@@ -92,18 +124,22 @@ line marked `// i18n-ignore` and a file marked `i18n-ignore-file` in a comment (
`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/en.json`
## 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).
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
@@ -131,9 +167,27 @@ player sees the current Chinese text, never an outdated number).
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.
- **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). Only English is generated in the repository: a game-text
overlay ships with the UI pack of its language (a pack is listed by its UI file).
- **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
@@ -181,6 +235,36 @@ Not player text, never translated: developer details and logs (`detail`, `log.*`
(`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
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 (`["万", "億"]`).
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, 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).
## Credits
The English UI strings were seeded from GitHub PR #70 (branch `en-translation`) by **@YuriRestia**, reused with credit