feedback5: merge W5AD — language packs as the first type of a generic content pack: a language is a file in the language folder (public/i18n/<code>.json with a _meta manifest, optional data/i18n/<code>.json) or a pack folder (packs/<id>/pack.json); the server lists them live at /packs/index.json, the menu shows every pack, strings fall back pack → base → declared fallback → Chinese, broken placeholders are skipped, plurals follow Intl.PluralRules (zh / en unchanged); tools i18n.mjs template / check, packs.mjs list / index, build-i18n --lang ja|ko|zh-TW; the release zips ship packs/ and packs/index.json; docs/PACKS.md, docs/I18N.md §5; §25.16 for now (renumbered next); golden: unchanged vs feedback5; vs W5AD exactly what feedback5 changed since f1df0d3 (CB5's 18 bot matches, CB2's roster 13 / bonds 7 / fields 6, CB4's roster-016 / 7 boss fields / standin-04/06/08 / diy-028/124)
No conflicts outside docs/history/0.2.0.md; the §25 intro keeps one sentence per stream (W5AD's line that also named the hotkeys' §25.15 joined to CB4's). Gate: non-browser suites 4981 / 4981 (test/i18n-packs 12 new, test/package 1 new), npm run golden ok, test/golden.test.js 9 / 9, lint 0 errors, check:imports ok, typecheck ok, docs-consistency + docs-paths 31 / 31.
This commit is contained in:
commit
66cdf493fc
30 files changed
+2670
-288
No files matched your search
+12
-8
@@ -77,6 +77,7 @@ so old imports keep working: `public/js/ui/gameLogic.js` (`public/js/ui/gameLogi
|
||||
| `server/net.js` | sessions and reconnect tokens, rate limits, message validation |
|
||||
| `server/lobby.js` | rooms, seats, AI seats, spectators; starts a `Match` |
|
||||
| `server/data.js` | loads `data/*.json` once (frozen) |
|
||||
| `server/packs.js` | the content packs (PACKS.md): finds the language packs of `public/i18n/` and the pack folders of `packs/`, validates them, answers `/packs/index.json` and which pack files may be served; re-reads the folders when they change |
|
||||
| `server/match/Match.js` | one match: the phase machine, timers, the round loop, co-op, the views; its methods are in `server/match/match/` (`phases.js`, `prep.js`, `combat.js`, `clientCombat.js`, `reports.js`, `unitePhase.js`, `bossRounds.js`, `settle.js`, `views.js`, `intents.js` …) |
|
||||
| `server/match/PlayerState.js` | one player's shop, hand, board, items, bonds and LP, and every prep intent; its methods are in `server/match/player/` (`economy.js`, `acquire.js`, `placement.js`, `items.js`, `pieces.js`, `prep.js`, `round.js`, `diy.js`, `views.js` …) |
|
||||
| `server/match/` (the rest) | `pool.js` (the shared chess pool), `board.js` (placement), `bondsMeta.js`, `effectsMeta.js` and `builtinMeta.js` (prep-phase effects), `choices.js` (机变), `waves.js`, `unite.js` (联防), `finalAssault.js`, `results.js`, `bot.js` (AI players and AI 托管), `fields.js` (the battles of a combat phase), `scheduler.js`, `gamedata.js` |
|
||||
@@ -88,7 +89,7 @@ so old imports keep working: `public/js/ui/gameLogic.js` (`public/js/ui/gameLogi
|
||||
| `server/sim/content/garrisons/`, `items/`, `bands/` | 特质, equipment and strategies: `battle.js` is the battle side, `meta.js` the prep side (`registerMeta`, META §2) |
|
||||
| `server/sim/content/bonds/` | the 23 bonds: `core.js` the 8 core bonds (both sides), `server/sim/content/bonds/addon/` the 15 add-on bonds (`battle.js`, `meta.js`) |
|
||||
| `server/sim/content/` (the rest) | `tokens.js` (summons), `devices.js` (terrain and stage devices), `generic.js` (the kit built from a skill's data when a chess has none), `choices.js` (机变 cards in battle) |
|
||||
| `shared/` | imported by the server and the browser: `protocol.js`, `constants.js`, `i18n.js` and `i18nData.js`, `standIn.js` (补位), `diy.js` (自选编队), `highGround.js`, `loadoutRecord.js` |
|
||||
| `shared/` | imported by the server and the browser: `protocol.js`, `constants.js`, `i18n.js`, `i18nData.js` and `i18nPacks.js` (languages), `packs.js` (the content-pack format), `standIn.js` (补位), `diy.js` (自选编队), `highGround.js`, `loadoutRecord.js` |
|
||||
|
||||
### Client (`public/`)
|
||||
|
||||
@@ -100,14 +101,15 @@ so old imports keep working: `public/js/ui/gameLogic.js` (`public/js/ui/gameLogi
|
||||
| `public/js/screens/` | `title.js`, `lobby.js`, `room.js`, `loadout.js` (干员调配), `ownership.js` (干员持有), `diy.js` (自选编队), `briefing.js`, `bandDraft.js`, `game.js` with `public/js/screens/game/`, `result.js` |
|
||||
| `public/js/ui/` | the HUD components (`hud.js`, `shopBar.js`, `detailPanel.js`, `bondStrip.js`, `teamPanel.js` …); `public/js/ui/gameLogic/` the pure in-match logic, unit-tested in Node |
|
||||
| `public/js/render/` | the field view: `app.js` with `public/js/render/app/`, `units.js` and `spine.js` (models), `tiles.js`, `projection.js`, `interp.js`, `pick.js`, `drag.js`, `public/js/render/fx/` (effects; `kinds.js` maps the fx kinds), `public/js/render/board3d/` (the official 3D board) |
|
||||
| `public/css/`, `public/i18n/en.json` | the styles; the English UI strings |
|
||||
| `public/css/`, `public/i18n/<code>.json` | the styles; the UI strings of each language pack (English ships) |
|
||||
|
||||
### Data, tools, tests
|
||||
|
||||
| path | what |
|
||||
|---|---|
|
||||
| `data/*.json` | generated by `tools/build-data.mjs` and committed — never edited by hand (DATA.md); `data/backups.json` holds the 补位 and 自选 data, `data/i18n/en.json` the English game texts, `data/assets.json` the art manifest |
|
||||
| `tools/` | `build-data.mjs`, `build-i18n.mjs`, `i18n.mjs` (UI strings), `setup.mjs` / `fetch-assets.mjs` / `tools/assets/` (art and audio), `tools/local-extract/` (art from a local game client), `vendor.mjs`, `golden.mjs`, `check-imports.mjs`, `package.mjs` (the release zip), `doctor.mjs`; sweeps: `matchrun.mjs`, `simrun.mjs`, `botbench.mjs`, `balance.mjs` |
|
||||
| `data/*.json` | generated by `tools/build-data.mjs` and committed — never edited by hand (DATA.md); `data/backups.json` holds the 补位 and 自选 data, `data/i18n/<code>.json` the game texts of a language pack (English), `data/assets.json` the art manifest |
|
||||
| `packs/` | content packs installed as folders, `packs/<id>/pack.json` + files (PACKS.md); empty in the repository but for its readme |
|
||||
| `tools/` | `build-data.mjs`, `build-i18n.mjs`, `i18n.mjs` (UI strings, language packs), `packs.mjs` (content packs), `setup.mjs` / `fetch-assets.mjs` / `tools/assets/` (art and audio), `tools/local-extract/` (art from a local game client), `vendor.mjs`, `golden.mjs`, `check-imports.mjs`, `package.mjs` (the release zip), `doctor.mjs`; sweeps: `matchrun.mjs`, `simrun.mjs`, `botbench.mjs`, `balance.mjs` |
|
||||
| `test/` | `node:test` suites by area: `test/content/` (kits, enemies, bonds, items), `test/sim/` (the engine), `test/match/` (match engine, bots), `test/ui/`, `test/render/`, `test/golden/` (the stored digests), `test/helpers/` (`battleHarness.js`; `designDocs.js`, the design document in § order for the doc tests), `test/e2e/` (browser and bot runs) |
|
||||
| `types/` | JSDoc typedefs of the type-checked slice (`types/README.md`) |
|
||||
| `docs/` | the documents; `docs/DESIGN.md` the index of the design document, `docs/design/` its current rules, `docs/history/` its per-release revisions; `docs/research/` the research on the official mode |
|
||||
@@ -135,10 +137,11 @@ public/assets/ ─────────▶ public/js/render/, public/js/audio
|
||||
the sim through `server/sim/simdata.js`, which turns every record into a frozen engine def.
|
||||
- **In the browser**, `public/js/data.js` fetches the same files once per page for the UI, and the battle runner loads
|
||||
its own frozen copies for the sim (`loadBrowserSim` → `setSimData`), so both sides simulate from the same data.
|
||||
- **Languages.** Chinese is the source language. The English game texts come from the official EN client
|
||||
(`tools/build-i18n.mjs` → `data/i18n/en.json`, applied by `public/js/data.js`); UI strings are wrapped in `t('…')`
|
||||
and translated in `public/i18n/en.json` (`tools/i18n.mjs` lists what is missing); server messages travel as message
|
||||
ids. I18N.md.
|
||||
- **Languages.** Chinese is the source language; every other language is a pack — `public/i18n/<code>.json` (UI
|
||||
strings) and optionally `data/i18n/<code>.json` (game texts), listed by the server at `/packs/index.json`
|
||||
(`server/packs.js`; PACKS.md). The English game texts come from the official EN client (`tools/build-i18n.mjs` →
|
||||
`data/i18n/en.json`, applied by `public/js/data.js`); UI strings are wrapped in `t('…')` and translated in the packs
|
||||
(`tools/i18n.mjs` lists what is missing); server messages travel as message ids. I18N.md.
|
||||
- **Art and audio** are never committed. `npm run setup` downloads them from public mirrors (`tools/fetch-assets.mjs`,
|
||||
planned by `tools/assets/plan.mjs`) into `public/assets/` and writes the manifest `data/assets.json`;
|
||||
`tools/local-extract/` can add art from a local game client. The client only requests URLs listed in the manifest.
|
||||
@@ -181,6 +184,7 @@ public/assets/ ─────────▶ public/js/render/, public/js/audio
|
||||
| a screen or a HUD panel | `public/js/screens/`, `public/js/ui/`; pure logic in `public/js/ui/gameLogic/` | `test/ui/` |
|
||||
| battle visuals | `public/js/render/fx/kinds.js`, `public/js/render/units.js`, `public/js/render/spine.js` | `test/render/` |
|
||||
| a UI text | `t('…')` in the code, the English in `public/i18n/en.json` | I18N.md |
|
||||
| a language | `public/i18n/<code>.json` (`node tools/i18n.mjs template <code>`), no code | I18N.md "Adding a language", PACKS.md |
|
||||
| HTTP, headers, static routes | `server/http/` | `test/version.test.js`, `test/client-static.test.js` |
|
||||
|
||||
Before changing a rule, read the DESIGN section that owns it (module headers cite their sections; the table in
|
||||
|
||||
+1
-1
@@ -300,7 +300,7 @@ npm run package -- --out <目录> # 完整包 Stronghold-Protocol-v<版
|
||||
npm run package:lite -- --out <目录> # 精简包 Stronghold-Protocol-v<版本>-lite.zip
|
||||
```
|
||||
|
||||
- **打进去的**:`git ls-files` 里的 `server/`、`shared/`、`data/`、`public/`(不含 `public/dev/`)、启动脚本、玩家会运行的工具(setup、vendor、fetch-assets 与 `tools/assets/`、doctor,以及 setup 调用的 `tools/local-extract/` 和 `crop-board-atlas.mjs`)、服务器和 fetch-assets 读取的 4 张研究数据表(`docs/research/` 的 `03-operators`、`05-enemies`、`05-maps`、`07-assets` 四个 JSON)、`package.json` / `package-lock.json`、许可证与说明(`LICENSE`、`NOTICE.md`、`THIRD-PARTY-NOTICES.md`、`README.md`、`CHANGELOG.md`)、`docs/PLAYING.md` 和本文;然后在临时目录里 `npm ci --omit=dev` 装上运行依赖和 `public/vendor`。完整包再加上 `data/assets.json` 列出的素材、`public/fonts`,以及本地提取的 `public/assets/local/` 和 `data/local-assets.json`。磁盘上有、清单却没列出的文件不打进去(例如 0.2.0 移出自选的焰狐龙梓兰的旧素材)。
|
||||
- **打进去的**:`git ls-files` 里的 `server/`、`shared/`、`data/`、`public/`(不含 `public/dev/`)、`packs/`(随仓库提交的内容包;只在本机安装、没提交的不打进去)、启动脚本、玩家会运行的工具(setup、vendor、fetch-assets 与 `tools/assets/`、doctor,以及 setup 调用的 `tools/local-extract/` 和 `crop-board-atlas.mjs`)、服务器和 fetch-assets 读取的 4 张研究数据表(`docs/research/` 的 `03-operators`、`05-enemies`、`05-maps`、`07-assets` 四个 JSON)、`package.json` / `package-lock.json`、许可证与说明(`LICENSE`、`NOTICE.md`、`THIRD-PARTY-NOTICES.md`、`README.md`、`CHANGELOG.md`)、`docs/PLAYING.md` 和本文;然后在临时目录里生成 `packs/index.json`(打进去的语言包和内容包的列表,供纯静态托管使用;服务器自己会实时列出,见 [PACKS.md](PACKS.md)),再 `npm ci --omit=dev` 装上运行依赖和 `public/vendor`。完整包再加上 `data/assets.json` 列出的素材、`public/fonts`,以及本地提取的 `public/assets/local/` 和 `data/local-assets.json`。磁盘上有、清单却没列出的文件不打进去(例如 0.2.0 移出自选的焰狐龙梓兰的旧素材)。
|
||||
- **不打进去的**:`test/`、维护用的工具(数据构建、golden、botbench、i18n、导入检查、本工具等)、`scripts/make-windows-bundle.mjs`(Windows 便携包,见 [WINDOWS.md](WINDOWS.md))、其他文档、研究笔记和 `docs/img/`、`handoff/`、`.github/`、`types/`、lint / 编辑器 / Docker 配置。和 0.1.x 的整树打包相比,完整包少了约 570 个文件、解压后小约 25 MB,zip 小约 8 MB。
|
||||
- **打包前的检查**(`--dry-run` 也全部做一遍):拒绝名单(`pv`、`review`、`.cache`、`.claude`、`.git`、`logs`、`.env`、`scripts/service.env.cmd`、`handoff`、`test` 等);每个打进去的模块的相对导入、玩家用的 npm 脚本(start / setup / doctor / launch / postinstall / vendor / assets)都指向包里的文件;完整包里 `data/assets.json` 和 `data/local-assets.json` 列出的文件都在(缺了先运行 `node tools/fetch-assets.mjs`);没有只差大小写的两个路径;包里的文件(二进制素材也查)不含个人目录路径(`/Users/…`、`C:\Users\…`、`/home/…`)或本机的账户名(运行时从系统读取;`SP_PACKAGE_SCAN_NAMES=a,b` 可以再加名字);打进去的已跟踪文件没有未提交的改动(重新生成的 `data/assets.json` 要先提交)。有任何问题都会列出原因、以非零状态结束,不写 zip;正式打包时还会核对临时目录里的文件和计划完全一致。
|
||||
- **需要**:已下载素材的仓库(完整包)——打包前先联网运行一次 `node tools/fetch-assets.mjs`,补齐清单计划但本机还没有的素材(清单只列出磁盘上有的文件,打包工具看不出缺了哪些;`data/assets.json` 有变化就先提交);能访问 npm 的网络(`npm ci`);`zip`(或 bsdtar 的 `tar`,Windows 10 起自带)。`--out` 默认是系统临时目录下的 `stronghold-protocol-release`,不能在仓库里面;`--force` 覆盖已有的 zip,`--keep-stage` 保留打包用的目录供检查。
|
||||
+107
-23
@@ -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
|
||||
|
||||
+164
@@ -0,0 +1,164 @@
|
||||
# PACKS.md — content packs
|
||||
|
||||
A **content pack** is add-on content a server picks up from a folder: a manifest plus its files. The owner's decision of
|
||||
2026-10-07 made the language pack the first type of one general mechanism, so that later resource packs (art, audio,
|
||||
fonts) and server-side data packs reuse the same manifest, registry and index instead of each inventing its own. In
|
||||
0.2.0 one type loads, **`lang`** (a language: docs/I18N.md "Adding a language"); `assets` and `data` are planned.
|
||||
|
||||
| File | Role |
|
||||
|---|---|
|
||||
| `shared/packs.js` | the format: the manifest, the pack types (`PACK_TYPES`), app version ranges, the index (browser, server and tools) |
|
||||
| `shared/i18nPacks.js` | the `lang` type: language codes, the language fields, the fallback chain, the script family |
|
||||
| `server/packs.js` | the registry: discovery, validation, the index, which pack files may be served |
|
||||
| `server/http/static.js` | `GET /packs/index.json` (the live index) and `GET /packs/<id>/<file>` |
|
||||
| `public/js/ui/lang.js` | the client of the `lang` entries: the language menu and the loader |
|
||||
| `tools/packs.mjs` | `list`, `index` (the index file for a static host), `check` |
|
||||
| `tools/package.mjs` | ships the committed packs and writes `packs/index.json` into the release zips |
|
||||
|
||||
## 1. Layouts
|
||||
|
||||
Two layouts, read by the same registry and listed alike:
|
||||
|
||||
```
|
||||
packs/<id>/pack.json + its files a folder pack — any type; the manifest names its files
|
||||
public/i18n/<code>.json a single-file language pack — its manifest is the file's `_meta` block
|
||||
data/i18n/<code>.json (+ its game texts, when present)
|
||||
```
|
||||
|
||||
The single file is the quickest way for a translator (one file, the format English has always had, and how English
|
||||
ships); the folder is the layout every pack type shares — a pack distributed as one folder with its own readme and
|
||||
licence, and the only layout a resource or data pack will have. A language can come either way; the menu does not tell
|
||||
them apart.
|
||||
|
||||
The id of a folder pack is its folder name; the id of a single-file language pack is its language code. Ids are
|
||||
letters, digits, `.`, `_` and `-` (at most 64), unique across types and without regard to case (Windows and macOS
|
||||
would see one folder).
|
||||
|
||||
## 2. The manifest
|
||||
|
||||
`packs/<id>/pack.json`, or the `_meta` block of `public/i18n/<code>.json` (the same fields):
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "lang",
|
||||
"lang": "ja",
|
||||
"name": "日本語",
|
||||
"englishName": "Japanese",
|
||||
"version": "1.0.0",
|
||||
"app": ">=0.2.0",
|
||||
"authors": ["…"],
|
||||
"credits": "…",
|
||||
"license": "CC-BY-4.0",
|
||||
"fallback": ["en"],
|
||||
"files": { "ui": "ui.json", "data": "data.json" }
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Meaning |
|
||||
|---|---|
|
||||
| `id` | optional: the folder or file name decides; a manifest that names another id gets a warning |
|
||||
| `type` | `lang` (a single-file language pack may leave it out); planned: `assets`, `data` |
|
||||
| `version` | the pack's own version |
|
||||
| `app` | the app versions it is made for, a range: `>=0.2.0`, `>=0.2.0 <0.3.0`, `0.2.x`, `^0.2.0`, `~0.2.1`, `*`, alternatives with `||`. A pre-release (0.2.0-dev) counts as its release. Outside the range a language pack still loads — what it lacks falls back — and is flagged `compatible: false` (the server log, `tools/packs.mjs list`); a later type decides what an incompatible pack means for it |
|
||||
| `name`, `englishName` | the name in its own language (a language pack: the language's own name, shown in the menu) and in English (the menu's tooltip) |
|
||||
| `authors`, `credits`, `license` | who made it; free text (`license`: an SPDX id or text) |
|
||||
| `files` | a folder pack: role → path inside the folder. Each type lists its roles and their extensions (`lang`: `ui` required, `data` optional, both `.json`) |
|
||||
| the type's fields | `lang`: `lang`, `base`, `fallback`, `complete`, `numberUnits` (docs/I18N.md) |
|
||||
|
||||
A **problem** keeps a pack from loading (unknown type, no `lang`, a file missing or outside the folder, broken JSON, an
|
||||
id or a language already taken — the language folders' files come first, then the folders by id); a **warning** does
|
||||
not (an id that differs, an app range this version is outside of, an unknown file role). Both are printed when the
|
||||
server starts or notices a change, and by `node tools/packs.mjs list`.
|
||||
|
||||
## 3. Discovery and the index
|
||||
|
||||
The server's registry (`server/packs.js`) scans `public/i18n/`, `data/i18n/` and `packs/` when it starts and again
|
||||
whenever something there changes: on a request for the index or a pack file it compares the names, sizes and times of
|
||||
the files it reads (at most once a second, a few `stat` calls) and scans again if they differ. A pack dropped into a
|
||||
folder shows on the next page load — no restart, no build step; a removed one leaves (a player whose stored language is
|
||||
gone gets Chinese).
|
||||
|
||||
`GET /packs/index.json` (never cached) answers the packs of supported types:
|
||||
|
||||
```json
|
||||
{ "version": 1, "app": "0.2.0", "packs": [
|
||||
{ "id": "en", "type": "lang", "name": "English", "englishName": "English", "version": "0.2.0", "app": ">=0.2.0",
|
||||
"compatible": true, "authors": ["…"], "lang": "en", "fallback": [], "complete": true, "strings": 1017,
|
||||
"files": { "ui": "/i18n/en.json", "data": "/data/i18n/en.json" } } ] }
|
||||
```
|
||||
|
||||
`files` holds URLs on this server, so a client never builds a path itself. A client reads the entries of its type
|
||||
(`readPackIndex(json, 'lang')`: an entry of another type, with a bad id or a URL that is not a path on this server is
|
||||
dropped).
|
||||
|
||||
**A static host** cannot list folders: `node tools/packs.mjs index` writes the same body to `packs/index.json`
|
||||
(git-ignored), and `tools/package.mjs` writes it into the release zips. The live server answers `/packs/index.json`
|
||||
itself and ignores that file. Serving the game from plain files needs the server's URL layout: `public/` at `/`,
|
||||
`data/` at `/data/`, `shared/` at `/shared/`, `server/sim/` at `/sim/` and `packs/` at `/packs/` (the game itself still
|
||||
needs the Node server for its rooms; DEPLOY.md).
|
||||
|
||||
**What is served.** `GET /packs/<id>/<file>` answers only a file that the manifest of a loaded folder pack names under a
|
||||
role of its type, with that role's extension, inside the folder once links are resolved — `pack.json`, a readme, an
|
||||
HTML or script file in the folder are never served; a pack of a planned type serves nothing. A single-file language
|
||||
pack's files are the language folders' own (`/i18n/<code>.json`, `/data/i18n/<code>.json`).
|
||||
|
||||
## 4. The types
|
||||
|
||||
| Type | Status | Where it applies | Loader |
|
||||
|---|---|---|---|
|
||||
| `lang` | supported (0.2.0) | the client: UI strings, game texts | `public/js/ui/lang.js` (the menu, the strings: `shared/i18n.js`; the game texts: `public/js/data.js`) |
|
||||
| `assets` | planned | the client: art, audio, font replacements | — |
|
||||
| `data` | planned | the server: data patches applied at start | — |
|
||||
|
||||
A pack of a planned type is parsed and listed as not supported ("type "assets" is planned …, not loaded by this
|
||||
version"); it is never served and never changes anything.
|
||||
|
||||
### Adding a pack type
|
||||
|
||||
1. **The type** — its entry in `PACK_TYPES` (`shared/packs.js`): `status`, `summary`, `live` (whether a running server
|
||||
may take changes without a restart) and `files` (the roles a folder pack names, with the extensions that may be
|
||||
served for each). Its own manifest fields get a normalizer beside `langFields` and a branch in `normalizeManifest`;
|
||||
its index fields a branch in `packIndexEntry`.
|
||||
2. **The loader** — reads the registry by type: `createPackRegistry(…).list('assets')` on the server, or the index
|
||||
entries of the type in the browser (`readPackIndex(json, 'assets')`), the way `ui/lang.js` reads `lang`. A
|
||||
server-side type (`data`) loads once at start (`live: false`): the simulation's code and data are fixed for the life
|
||||
of the process, and a battle must run the same data on the server and in every browser.
|
||||
3. **The checks** — what `tools/packs.mjs check` and the tests verify for the type (a data pack: the same validation
|
||||
the data builder applies; a resource pack: the files exist and are of the allowed kinds), and its documentation
|
||||
here.
|
||||
|
||||
Things later types must keep (the maintainers' study of mods, 2026-10-07):
|
||||
|
||||
- **Server-side only for gameplay.** A data pack is installed on the server, so every player of a room uses the same
|
||||
content and the server can still check or recompute a battle. A player never installs gameplay content of their own.
|
||||
- **A content hash in the handshake** before data packs ship (`welcome`, `/healthz`, the BattleSpec), so a client and
|
||||
a server can tell they run the same content, and a "modded" mark in the UI and in bug reports.
|
||||
- **No third-party code.** The manifest and its files are data (JSON, images, audio, fonts); a pack type that runs code
|
||||
is a separate decision of the owner's.
|
||||
- **The golden results stay on the original content**; a pack never changes them.
|
||||
|
||||
## 5. Tools
|
||||
|
||||
```
|
||||
node tools/packs.mjs list [--json] the packs by type, what was skipped and why, the warnings
|
||||
node tools/packs.mjs index [--out <file>] write packs/index.json (the static index)
|
||||
node tools/packs.mjs check [--strict] list; --strict exits 1 when a pack is skipped or warned about
|
||||
node tools/i18n.mjs template <code> a new language pack (docs/I18N.md)
|
||||
node tools/i18n.mjs check <code> | --all a language pack's strings: coverage, placeholders
|
||||
```
|
||||
|
||||
`--root <dir>` points `tools/packs.mjs` at another checkout or an unpacked release.
|
||||
|
||||
## 6. Shipping
|
||||
|
||||
`tools/package.mjs` ships the committed packs (`public/i18n/`, `data/i18n/`, `packs/` — tracked files only: a pack
|
||||
installed on this machine and not committed stays out) and writes `packs/index.json` into the stage from the packs
|
||||
that ship. `test/package.test.js` checks both layouts and that a local pack stays out.
|
||||
|
||||
## Tests
|
||||
|
||||
`test/i18n-packs.test.js` (discovery of both layouts, broken / planned / duplicate packs, a pack dropped in or removed
|
||||
while the server runs, the file allowlist, the manifest and app ranges, the fallback chain, placeholders, plurals,
|
||||
the per-text game-text fallback, the client loader and menu, the tools, build-i18n `--lang` / `--dict`),
|
||||
`test/package.test.js`, `test/ui/i18n.e2e.test.js`
|
||||
(`SP_E2E=1`: a pack file and a pack folder dropped in while the server runs show in the menu and switch).
|
||||
+63
-2
@@ -34,7 +34,8 @@ redeploy, 赫默's drone at full SP, the countdown summons' 无敌 / 禁疗 / ti
|
||||
DoT damage, 伺夜's 狼影 and the 吟游者 trait answered) → §25.16; the community reports of 2026-10-06, stream CB4 (enemies, bosses, waves, movement: the 突袭 landing on the 临时整备区 row and the other half of the
|
||||
联防 map, a silenced 萨卡兹枯朽战士's death pollution, 萨卡兹穿刺手's speed after a stun and its first hit, 自制投石机 blocked on the 排气格栅; the briefing types'
|
||||
7–9 waves, 新硎's 断刃, a freed prisoner in 联防 and 鸭爵's two swaps in one wave already answered; the moving leaders' routes
|
||||
checked; 野鬃, 锏's pulls, 昆图斯's footprint and the leaders' attack rate not reproduced) → §25.18.
|
||||
checked; 野鬃, 锏's pulls, 昆图斯's footprint and the leaders' attack rate not reproduced) → §25.18; the rebindable shortcuts
|
||||
→ §25.15; language packs, the first type of content packs (a language is a file in the language folder) → §25.16.
|
||||
|
||||
Decisions of the owner: 2026-10-04 — the server stays on plain `node:http` + `ws`, no web framework (§25.1);
|
||||
2026-10-05 — the 补位 plan (§25.3); the 自选编队 rules, the collab operators out of the pool, ISW-A refused (§25.4);
|
||||
@@ -47,7 +48,8 @@ a melee 突袭 member lands where it can block (§25.11); 华法琳's granted tr
|
||||
lock (§25.13.2); a newly merged elite 拉普兰德 counts as a new 拉普兰德 (§25.13.3); the leader pool = bloodPoint × the players
|
||||
alive at the fight's start, adopting PR #209 and replacing 「保持固定血量」 (§25.13.4); flying units at 1.3 tiles and the two
|
||||
model quirks of PR #211 (§25.13.5); the bounty pays whenever its enemy dies (relayed with the community
|
||||
reports, §25.14.3).
|
||||
reports, §25.14.3); 2026-10-07 — a language is a file in the language folder, and the language pack is the first type
|
||||
of a general content-pack mechanism (§25.16).
|
||||
|
||||
### 25.1 Refactor and tooling: the big files split, golden results, lint, types, import boundaries (refactor plan; owner's decision of 2026-10-04: no web framework) — `server/http/`, `server/match/match/`, `server/match/player/`, `server/sim/battle/`, `content/enemies/`, `kits/ops/`, `public/js/ui/gameLogic/`, `render/fx/`, `render/app/`, `screens/game/`, `tools/golden.mjs`, `tools/check-imports.mjs`, `eslint.config.js`, `jsconfig.json`
|
||||
|
||||
@@ -1505,3 +1507,62 @@ briefing types' share of the waves).
|
||||
卢西恩 attacks only its blocker and blinks past it. A leader and a situation from the reporter would show what differs.
|
||||
- Golden: unchanged. Pinned: `test/sim/feedback5-cb4-pins.test.js` (the four interval leaders at ≥ max − 1 attacks).
|
||||
|
||||
### 25.16 Language packs: a language is a file in the language folder, the first type of content packs (W5AD; the owner's decisions of 2026-10-07) — `shared/packs.js`, `shared/i18nPacks.js`, `shared/i18n.js`, `shared/i18nData.js`, `server/packs.js`, `server/http/static.js`, `public/js/ui/lang.js`, `public/js/data.js`, `tools/i18n.mjs`, `tools/packs.mjs`, `tools/build-i18n.mjs`, `tools/package.mjs`, docs/I18N.md §5, docs/PACKS.md
|
||||
|
||||
- **Seen**: after §25.2 English was wired in by name — the language list `['zh', 'en']`, the switch's labels, English
|
||||
checks in the title screen, `fmtNum`, the tab title, a CSS rule, the overlay URL pattern — so another language meant
|
||||
editing code, and players asked to translate into their own languages.
|
||||
- **Decision**: the owner's decision of 2026-10-07 — putting an adapted language file into the language folder is all
|
||||
it takes for the game to offer that language, so the community can add languages themselves; the same day, design the
|
||||
language pack as the first type of a general content-pack mechanism (later: resource packs for art / audio, then
|
||||
server-side data packs) with one manifest, one registry and one index.
|
||||
- **Now** (docs/I18N.md §5 for translators, docs/PACKS.md for the mechanism):
|
||||
- **A pack** is a manifest plus its files, in one of two layouts the same registry reads: the single file
|
||||
`public/i18n/<code>.json` (manifest: its `_meta` block; optional game texts `data/i18n/<code>.json`) — the
|
||||
translator's way and how English ships — or a folder `packs/<id>/pack.json` naming its files (every future type).
|
||||
The manifest: type (`lang`; `assets` and `data` planned: parsed, listed as not supported, never served), version,
|
||||
the app versions it targets (a range; outside it a language pack still loads and is flagged), its name in its own
|
||||
language and in English, authors / credits / licence, and the type's fields (`lang`, `base`, `fallback`, `complete`,
|
||||
`numberUnits`). English carries one like any pack (`complete: true`).
|
||||
- **Discovery**: `server/packs.js` scans the language folders and `packs/` at start and again when their files
|
||||
change (looked at on each index or pack-file request, at most once a second), validates the manifests, and answers
|
||||
`GET /packs/index.json` (live, never cached; packs of supported types with the URLs of their files) and
|
||||
`GET /packs/<id>/<file>` (only a file a manifest names, with its role's extension, inside the pack folder). A static
|
||||
host uses `node tools/packs.mjs index` (packs/index.json); the release zips ship packs/ and that file. A broken
|
||||
manifest, a planned type, a second pack for an id or a language: skipped with the reason in the server log and
|
||||
`node tools/packs.mjs list`.
|
||||
- **The client** lists every `lang` entry under its own name (title screen and 设置: buttons up to four languages, a
|
||||
list beyond); `?lang=<code>` and the stored choice resolve to a listed pack (`en-US` → `en`; a pack that is gone →
|
||||
Chinese); without an index the stored pack still loads by its code. Nothing in the client names English: the title
|
||||
screen lays out by the script of the pack's title (`scriptOf`: an alphabetic title takes English's layout), the tab
|
||||
title and `<html lang>` follow the pack, `fmtNum` takes `numberUnits` (Chinese 万 / 亿; without them K / M / B).
|
||||
- **Fallback chain** per string: the pack → its base (`pt-BR` → `pt`; `zh-TW` none) → the packs `_meta.fallback`
|
||||
names (English) → the Chinese msgid; game texts per text (each from the first overlay of the chain that has it:
|
||||
`applyFileOverlay(json, overlay, onto)`, `data.setLocale(lang, chain)`).
|
||||
- **Placeholders**: a translation that drops a plain placeholder of its msgid, uses one the caller does not pass, or
|
||||
has a broken plural form is skipped like a missing one (the next language of the chain shows; a development page
|
||||
warns once on the console; `tools/i18n.mjs check` checks the call sites). **Plurals**: `{n|one|other}` picks by
|
||||
`Intl.PluralRules` (English exactly as before), and named categories serve other languages
|
||||
(`{n|one:…|few:…|many:…|other:…}`).
|
||||
- **Tools**: `node tools/i18n.mjs template <code>` (a skeleton: `_meta` and every msgid; on an existing pack only
|
||||
the msgids it lacks), `check <code> | --all` (coverage; errors; a complete pack must have every msgid);
|
||||
`node tools/build-i18n.mjs --lang ja | ko | zh-TW` builds a pack's game texts from the official JP / KR / TW client
|
||||
(ArknightsAssets/ArknightsGamedata `jp/`, `kr/`, `tw/`, each with both 盟约 seasons, checked 2026-10-07: ja 96.5 %,
|
||||
ko 97.4 %, zh-TW 91.1 % of 22 114 texts); a language without a client uses `--dict` (a community dictionary) or no
|
||||
game texts at all (they follow the chain). No language but English ships: there is no Japanese, Korean or
|
||||
Traditional Chinese UI pack yet, and a game-text file without its UI pack is not listed.
|
||||
- **The output in Chinese and English did not change**: `test/i18n-packs.test.js` renders every msgid of
|
||||
`public/i18n/en.json` in both languages with sample params and compares it with the renderer of 0.2.0 before packs;
|
||||
`node tools/build-i18n.mjs --check` still finds `data/i18n/en.json` up to date. One English entry was added, the tab
|
||||
title (`卫戍协议:盟约 · STRONGHOLD PROTOCOL`, the same text as before).
|
||||
- [ASSUMED]: the menu turns into a list above four languages; two plain plural forms follow the language's rules (only
|
||||
-1 renders differently from before in English, "-1 second"); a regional code falls back to its language's pack before
|
||||
the declared fallbacks; with two packs for one language the language folder's file wins; a pack outside its app range
|
||||
still loads; a development page is a dev build or one served from localhost.
|
||||
- Golden: unchanged. Tests: `test/i18n-packs.test.js` (12: discovery of both layouts and of broken / planned /
|
||||
duplicate packs, a pack dropped in or removed while the server runs, the file allowlist, manifests and app ranges,
|
||||
the chain, placeholders, plurals in English and Russian, the per-text game-text fallback, the client loader and menu,
|
||||
the template / check / index tools, build-i18n's `--lang` and `--dict`, zh / en unchanged), `test/package.test.js` (both layouts ship, a local pack does
|
||||
not, the generated index), `test/ui/i18n.e2e.test.js` (`SP_E2E=1`: a pack file and a pack folder dropped in while the
|
||||
server runs show in the menu and switch, English filling the gaps; a removed pack sends the stored choice back to
|
||||
Chinese); `test/i18n.test.js`, `test/i18n-data.test.js`, `test/i18n-server.test.js` unchanged and green.
|
||||
Reference in new issue
Block a user