feedback5: merge W5S — i18n phase 2: every player-facing Chinese literal in the client, shared and server goes through t() / tParts() / N_() (1 128 → 0; en.json 1 015 entries), server-sent data texts show the English record, a language switch refreshes cached names; test/i18n.test.js enforces extract 0 / check --strict over the whole tree; Chinese output unchanged (per-file render proof); golden unchanged

This commit is contained in:
sganggs committed 2026-10-06 18:39:35 +08:00
commit 3ac88dea43
73 files changed
+1280 -903

No files matched your search

+51 -18
View File
@@ -24,25 +24,32 @@ t('还剩 {n} 秒', { n }); // params: {name}; arrays fill {0},
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 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).
- **Markup inside a sentence** (a number in `<b>`, a `<kbd>`): `tParts(msgid, params)` returns the translation split at
its placeholders, with a param that is markup (a vnode, or an array of vnodes) kept as it is — the translator gets the
whole sentence and its word order, the Chinese pieces are the text around the markup exactly as before. Do not split
such a sentence into fragments (`第` … `回合`).
- **Game-data names** inside a message are marked `dn(name)` (`{ dn: '…' }`) and translated through the localized data
(`tName(name)` does the same for a bare name). **Player names are never translated** — pass them as plain strings.
- **Data texts are not msgids**: `getChess(id).name`, `getMode(id).desc`, `data.get('config').tips` … already come in the
current language (§2). Do not wrap them in `t()`.
- **Re-rendering**: `main.js` App subscribes with `useLang()` (`public/js/ui/lang.js`), so the whole tree re-renders on a
switch. A component that caches text outside rendering (a `useMemo` returning text, a module-level string) must
depend on the language (`useLang()`) or move the `t()` call into rendering.
depend on the language (`useLang()`) or move the `t()` call into rendering; a memo over data records (the roster, the
strategy list, `useGameData()`) lists `data.locale()` among its dependencies, so the game texts follow the switch.
- Font samples, data keys and other Chinese that is not UI text can carry `// i18n-ignore` on the line.
### Translating
Edit `public/i18n/en.json` (`"msgid": "English"`, two-space JSON; `_about` is metadata). Keep every `{placeholder}`;
`test/i18n.test.js` checks that, that no Chinese is left in a value, and that every msgid of the converted screens is
present. Use the official EN terms of the game (the EN client's own wording, e.g. its tips and mode texts):
`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):
| 中文 | English | 中文 | English |
|---|---|---|---|
@@ -59,8 +66,9 @@ present. Use the official EN terms of the game (the EN client's own wording, e.g
### Tools: `node tools/i18n.mjs`
- `extract [paths] [--list]` — per file: the msgids in `t()` / `tc()` / `N_()` / `msg()`, and the Chinese literals still
shown untranslated; the total says how many of those already have a translation (ready for the codemod).
- `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).
- `codemod <files> [--write]` — wraps a file's Chinese literals: html`` text and attribute values → `${t('…')}`, strings →
@@ -74,7 +82,15 @@ present. Use the official EN terms of the game (the EN client's own wording, e.g
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).
`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/en.json`
@@ -132,21 +148,38 @@ protocol is backward compatible both ways (new fields are optional).
this.m.toast(this, 'warn', msg('{names}只能部署在召唤者攻击范围内,已退回整备区', { names: back.map(dn) }));
```
## 4. Phase 2 (after the 0.2.0 feature screens land)
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:
(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.
1. `node tools/i18n.mjs extract` lists the rest (2026-10-05: 1 033 literals in 56 files — briefing, band draft, game
screen and its HUD / panels / overlays, loadout, result, guide, connection banner, components …; 964 of them already
have a translation seeded from PR #70).
2. Per file (one stream per file group to avoid conflicts): `node tools/i18n.mjs codemod <file> --write`, review the
diff (module-level tables: `t()` at the use site; join split sentences; `tc()` where one Chinese text needs two
English ones), then `node tools/i18n.mjs check` and translate what is missing, in the official terms above.
3. Texts the server puts in `m.public` / `m.private` (机变 card names and descriptions, effect names in views) should be
looked up by id on the client (`getEffect` / `data.lookup`) instead of shown as sent.
4. `public/js/ui/guide.js` chapter / page titles, the `GuideButton` / `LoadoutButton` default labels and tooltips.
5. When the sweep is complete: `node tools/i18n.mjs check --strict` in CI, and `extract` reporting 0.
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.
## Credits