feedback5 W5S: i18n phase 2 coverage — test/i18n.test.js scans the whole client, shared and the server messages (no Chinese literal unwrapped, every msgid translated: extract 0 and check --strict as a test; floors 1 000 catalog entries, 1 350 msgid uses), public/i18n/en.json drops the 64 fragments / renamed msgids the sweep replaced (none is a data text), docs/I18N.md: phase-2 coverage, what stays Chinese and why, the server's game texts, the audit's skip rules
This commit is contained in:
1 parent
0025ceac16
commit
c2e77c7fe9
3 files changed
+67
-90
No files matched your search
+41
-15
@@ -46,8 +46,9 @@ 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 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 |
|
||||
|---|---|---|---|
|
||||
@@ -80,7 +81,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`
|
||||
|
||||
@@ -138,21 +147,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
|
||||
|
||||
|
||||
@@ -183,10 +183,7 @@
|
||||
"战场": "Battlefield",
|
||||
"本局禁用": "Disabled this match",
|
||||
"{skip};本局不限时": "{skip}; no time limit this match",
|
||||
"每位博士有 {0} 秒": "each Doctor has {0} s",
|
||||
"每位博士限时决策": "each Doctor has a time limit",
|
||||
"当前选中的": "the currently highlighted ",
|
||||
"已选择「{0}」{1}": "Selected \"{0}\"{1}",
|
||||
",等待其他博士": ", waiting for other Doctors",
|
||||
"轮到你决策": "Your turn to decide",
|
||||
"{turnName} 决策中": "{turnName} is deciding",
|
||||
@@ -211,8 +208,6 @@
|
||||
"战场随机(共{poolN}张)": "Random Battlefield ({poolN} total)",
|
||||
"确认本局信息": "Confirm Match Info",
|
||||
"ENEMY LEADER // 敌方领袖": "ENEMY LEADER // Enemy Leader",
|
||||
"第": "Round",
|
||||
"回合 · 最终攻势": "· Final Assault",
|
||||
"特训敌人": "Tactical Training Enemies",
|
||||
"未知领袖": "Unknown Leader",
|
||||
"本局没有特训敌人": "No Tactical Training Enemies this match",
|
||||
@@ -228,11 +223,9 @@
|
||||
"当前设备无法启用 3D / WebGL 渲染,已切换为简化视图(功能不受影响)": "3D / WebGL rendering is unavailable on this device; switched to a simplified view (all features still work)",
|
||||
"已配发的装备无法销毁": "Equipped Equipment cannot be destroyed",
|
||||
"销毁道具": "Destroy Item",
|
||||
"道具无法出售。确定要销毁「{0}」吗?": "Items cannot be sold. Destroy \"{0}\"?",
|
||||
"道具": "Item",
|
||||
"销毁": "Destroy",
|
||||
"正在查看": "Viewing",
|
||||
"的阵地(只读)": "'s Field (read-only)",
|
||||
"返回自己": "Back to Mine",
|
||||
"你已被淘汰 · 可继续观战队友": "You have been eliminated · You can keep spectating your Teammates",
|
||||
"已调整": "Customized",
|
||||
@@ -274,7 +267,6 @@
|
||||
"按盟约筛选": "Filter by Alliance",
|
||||
"全部盟约": "All Alliances",
|
||||
"仅看已调整": "Customized Only",
|
||||
"{t}阶": "Tier {t}",
|
||||
"保存中…": "Saving…",
|
||||
"同步中…": "Syncing…",
|
||||
"已同步": "Synced",
|
||||
@@ -308,7 +300,6 @@
|
||||
"把导出的内容粘贴到下方,或点「选择文件」。": "Paste exported data below, or tap [Choose File]. ",
|
||||
"覆盖": "overwrite",
|
||||
"在此粘贴导出的调配内容…": "Paste the exported loadout here…",
|
||||
"调整干员携带的技能与模组": "Change the Skills and Modules your Operators carry",
|
||||
"造成伤害": "Damage Dealt",
|
||||
"击倒敌人": "Enemies Defeated",
|
||||
"领袖伤害": "Leader Damage",
|
||||
@@ -335,8 +326,6 @@
|
||||
"防线已被突破": "The defense line has been breached",
|
||||
"敌方领袖": "Enemy Leader",
|
||||
"隐秘核心": "Hidden Core",
|
||||
"本局耗时": "Match duration:",
|
||||
"分钟": "min",
|
||||
"返回大厅": "Back to Lobby",
|
||||
"暂无结算数据": "No results data",
|
||||
"服务器会话已重置,上一局模拟已结束": "Server session was reset. The previous simulation has ended.",
|
||||
@@ -349,8 +338,6 @@
|
||||
"(含调和 +{harmony})": " (incl. Harmony +{harmony})",
|
||||
"层数叠加已禁用": "Stack accumulation disabled",
|
||||
"调和": "Harmony",
|
||||
"{0} 在场": "{0} on field",
|
||||
"{harmonyName}已激活": "{harmonyName} active",
|
||||
"层数": "Stacks",
|
||||
"盟约效果": "Alliance Effect",
|
||||
"成员": "Members",
|
||||
@@ -358,12 +345,9 @@
|
||||
"盟约:{name}": "Alliance: {name}",
|
||||
"CORE BOND // 核心盟约": "CORE BOND // Core Alliance",
|
||||
"ADD-ON BOND // 附加盟约": "ADD-ON BOND // Add-on Alliance",
|
||||
"的盟约": "'s Alliance",
|
||||
"在场": "On field",
|
||||
"(含整备区)": "(incl. Bench)",
|
||||
"(含{harmonyName} +{harmony})": "(incl. {harmonyName} +{harmony})",
|
||||
"已激活{0}": "Active{0}",
|
||||
" · {tier} 阶": " · Tier {tier}",
|
||||
"未激活": "Inactive",
|
||||
"本模式下该盟约不会激活(其干员仍可能因所属的其他盟约出现)": "This Alliance never activates in this mode (its Operators may still appear through their other Alliances)",
|
||||
"名及以下": " or fewer",
|
||||
@@ -440,7 +424,6 @@
|
||||
"无": "None",
|
||||
"攻击范围": "Attack Range",
|
||||
"全场": "Whole field",
|
||||
"基础 {0}": "Base {0}",
|
||||
"当前作战中的实时数值(绿色为增益,红色为减益)": "Live values in the current battle (green: buffed, red: debuffed)",
|
||||
"下一场作战开始时的数值:已计入装备、盟约层数、特质、策略与机变效果(不含技能与作战中的临时效果)": "Values at the start of the next battle: Equipment, Alliance stacks, Attributes, Strategy and Improv effects included (skills and temporary in-battle effects excluded)",
|
||||
"实时": "Live",
|
||||
@@ -496,7 +479,6 @@
|
||||
"作战开始时在摆放的位置部署一次,之后所属干员每次发动技能时再次出现(未摆放则不会出现)": "Deploys once on its placed tile when the battle starts, then reappears each time its Operator activates a skill (does not appear if not placed)",
|
||||
"所属干员发动技能时才在摆放的位置出现(未摆放则不会出现)": "Appears on its placed tile only when its Operator activates a skill (does not appear if not placed)",
|
||||
"出售精锐干员": "Sell Elite Operator",
|
||||
"确定要出售精锐干员「{0}」吗?出售后获得 {1} 资金。": "Sell the Elite Operator \"{0}\"? You will get {1} Funds.",
|
||||
"详情": "Details",
|
||||
"退出全屏": "Exit Fullscreen",
|
||||
"全屏": "Fullscreen",
|
||||
@@ -509,8 +491,6 @@
|
||||
"表情主题": "Emote themes",
|
||||
"悬赏": "Bounty",
|
||||
"护卫": "Escort",
|
||||
"回合 · 即将迎击": "·",
|
||||
"名敌人": "incoming enemies",
|
||||
"暂无敌方情报": "No enemy intel yet",
|
||||
"本局禁用干员情况": "Banned Operators this match",
|
||||
"我的策略": "My Strategy",
|
||||
@@ -522,22 +502,16 @@
|
||||
"该干员": "This Operator",
|
||||
"将被销毁": "Will be destroyed",
|
||||
"替换装备": "Replace Equipment",
|
||||
"」的装备栏已满(2/2)。选择一件装备进行替换,": "]'s Equipment slots are full (2/2). Choose a piece of Equipment to replace.",
|
||||
"被替换的装备将被销毁": "The replaced Equipment will be destroyed",
|
||||
"新装备": "New Equipment",
|
||||
"选择被替换的装备": "Choose Equipment to replace",
|
||||
"将销毁「": "Destroy [",
|
||||
"」并配发「": "] and equip [",
|
||||
"点击选择要替换的装备": "Tap to choose the Equipment to replace",
|
||||
"上": "Up",
|
||||
"右": "Right",
|
||||
"下": "Down",
|
||||
"左": "Left",
|
||||
"选择{0}朝向": "Choose facing{0}",
|
||||
"「{name}」的": " for [{name}]",
|
||||
"点击取消": "Tap to cancel",
|
||||
"拖回中心区域取消": "Drag back to center to cancel",
|
||||
"朝向:{0}": "Facing: {0}",
|
||||
"干员": "Operator",
|
||||
"整备区": "Bench",
|
||||
"召唤": "Summon",
|
||||
@@ -546,7 +520,6 @@
|
||||
"已淘汰": "Eliminated",
|
||||
"已离开": "Left",
|
||||
"{name}:本局禁用(该盟约不会激活)": "{name}: Disabled this match (this Alliance will not activate)",
|
||||
"本局禁用{0}盟约,此策略效果可能无法发挥": "{0} Alliance disabled this match; this Strategy's effect may not work",
|
||||
"行动中": "Acting",
|
||||
"作战中": "In Combat",
|
||||
"联防中": "Uniting",
|
||||
@@ -561,7 +534,6 @@
|
||||
"作战开始": "Battle Start",
|
||||
"各自行动阶段": "Individual action phase",
|
||||
"联防阶段": "Unite Phase",
|
||||
"联防:{0}": "Unite: {0}",
|
||||
"完美作战的博士迎战突破防线的敌人": "Doctors with a flawless battle take on the enemies that broke through",
|
||||
"最终攻势": "Final Assault",
|
||||
"击败敌方领袖": "Defeat the Enemy Leader",
|
||||
@@ -626,28 +598,16 @@
|
||||
"下一页": "Next page",
|
||||
"页码": "Pages",
|
||||
"联防": "Unite",
|
||||
"{name} 漏过的敌人": "Enemies that got past {name}",
|
||||
"你漏过的敌人": "Enemies that got past you",
|
||||
"队友正在迎战": "teammates are engaging",
|
||||
"{who}已全部被击倒": "{who}: all defeated",
|
||||
"剩余敌人 {v}": "Enemies remaining: {v}",
|
||||
"剩余不足 {cap} 个后,队友每击倒一个少扣 1 点": "once fewer than {cap} remain, each one your teammates defeat costs 1 less",
|
||||
"队友每击倒一个就少扣 1 点": "each one your teammates defeat costs 1 less",
|
||||
"目标生命值 {lp}:联防中,你漏过的敌人还剩 {left} 个,{each};按现在结算扣除 {pending} 点(每回合至多 {cap} 点)": "LP {lp}: Unite in progress, {left} enemies that got past you remain; {each}. Settling now would cost {pending} LP (max {cap} per round)",
|
||||
"目标生命值 {lp}:联防中,队友正在迎战你漏过的敌人,结算时按联防后剩余的敌人扣除(至多 {pending} 点)": "LP {lp}: Unite in progress, teammates are engaging the enemies that got past you; the loss at settlement depends on the enemies left after Unite (max {pending} LP)",
|
||||
"{cap} 个以上": "{cap}+",
|
||||
"{pending} 个": "{pending}",
|
||||
"放入整备区或战场、配发或使用后才能准备就绪;休整期结束时仍留在临时整备区的单位将被销毁": "Move them to the Bench or Battlefield, equip or use them before you can Ready; units still on the Temporary Bench when the Rest Phase ends will be destroyed",
|
||||
"临时整备区还有 {count} 个单位:{TEMP_RULE}": "{count} unit(s) still on the Temporary Bench: {TEMP_RULE}",
|
||||
"取消准备": "Cancel Ready",
|
||||
"临时整备区": "Temporary Bench:",
|
||||
"个单位待处理": "unit(s) pending",
|
||||
"本局信息(策略 / 禁用盟约 / 干员)": "Match Info (Strategies / Banned Alliances / Operators)",
|
||||
"查看即将迎击的敌方单位": "View the enemies you are about to face",
|
||||
"休整期可以查看即将迎击的敌方单位": "Upcoming enemies can be viewed during the Rest Phase",
|
||||
"秒后全队生命值开始流失": "s",
|
||||
"超时 · 生命值": "Overtime · LP",
|
||||
"/秒": "/s",
|
||||
"已流失": "Lost",
|
||||
"暂停": "Pause",
|
||||
"继续作战(Space)": "Resume Battle (Space)",
|
||||
@@ -667,15 +627,12 @@
|
||||
"这不是干员调配的数据": "This is not Operator Loadout data",
|
||||
"里面没有有效的调配条目": "It contains no valid loadout entries",
|
||||
"弹药": "Ammo",
|
||||
"{0}秒": "{0}s",
|
||||
"本局的干员调配已锁定,修改将在下一局生效": "Operator loadout is locked for this match; changes will apply next match",
|
||||
"独立模拟的休整期没有时间限制,你可以随时继续。": "The Rest Phase in Solo Simulation has no time limit. You can continue at any time.",
|
||||
"放弃模拟将立即结束本局并返回大厅,本局进度不会保留,也不会进行结算。": "Abandoning the simulation ends this match immediately and returns you to the lobby. Progress will not be kept and no results will be tallied.",
|
||||
"离开模拟": "Leave Simulation",
|
||||
"暂离(AI 托管)": "Step Away (AI Autopilot)",
|
||||
"暂离": "Step Away",
|
||||
":由 AI 托管你的席位(自动部署、准备与选择),随时可以返回。": ": AI takes over your seat (auto deploy, ready-up and choices). You can return at any time.",
|
||||
":离开后无法返回本局,你的干员将回到共享卡池。": ": You cannot return to this match after leaving, and your Operators go back to the shared pool.",
|
||||
"AI 托管中": "AI Autopilot active",
|
||||
"AUTOPILOT // AI 托管": "AUTOPILOT // AI AUTOPILOT",
|
||||
"AI 正在代为操作你的席位": "AI is playing your seat for you",
|
||||
@@ -697,7 +654,6 @@
|
||||
",再次点击确认": ", tap again to confirm",
|
||||
"已拥有 {copies}/{need}": "Owned {copies}/{need}",
|
||||
"可晋升": "Promotable",
|
||||
"技能{0}:{1}{2}{3}": "Skill{0}: {1}{2}{3}",
|
||||
"(已调配)": " (Assigned)",
|
||||
" · 模组:{name}": " · Module: {name}",
|
||||
" · 未装备模组": " · No Module equipped",
|
||||
@@ -719,23 +675,17 @@
|
||||
"解冻": "Unfreeze",
|
||||
"刷新商店 · R": "Refresh Shop · R",
|
||||
"免费 ×{free}": "Free ×{free}",
|
||||
"联防中,": "In Unite; ",
|
||||
"至多": "up to ",
|
||||
"查看 {name} 的战场": "View {name}'s Battlefield",
|
||||
"查看自己的阵地": "View your Field",
|
||||
"查看 {name} 的阵地": "View {name}'s Field",
|
||||
"连接已断开": "Disconnected",
|
||||
"前往查看": "Go View",
|
||||
"{0} 操作": "{0} actions",
|
||||
"单位": "Unit",
|
||||
"撤退": "Retreat",
|
||||
"撤退至整备区": "Retreat to Bench",
|
||||
"出售,获得 {sell} 资金": "Sell for {sell} Funds",
|
||||
"已准备就绪后进入的单位保留到下个休整期,届时仍在此处的将被销毁(取消准备则在本休整期结束时销毁)": "Units that arrived after Ready are kept until the next Rest Phase; anything still here then will be destroyed (cancel Ready and they are destroyed at the end of this Rest Phase)",
|
||||
"放入整备区或战场、配发或使用后才能准备;休整期结束时仍在此处的将被销毁": "Move to the Bench or Battlefield, equip or use them before you can Ready; anything still here at the end of the Rest Phase will be destroyed",
|
||||
"件道具": "item(s)",
|
||||
"个单位": "unit(s)",
|
||||
"{what}待处理": "{what} pending",
|
||||
"结算": "Settlement",
|
||||
"模拟结束": "Simulation Over",
|
||||
"表情套组:卫戍协议": "Emote Set: Stronghold Protocol",
|
||||
@@ -778,20 +728,6 @@
|
||||
"维维美 4": "Wiwime 4",
|
||||
"维维美 5": "Wiwime 5",
|
||||
"维维美 6": "Wiwime 6",
|
||||
"<!doctype html><html lang=\"zh-CN\"><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n<title>{status} · 卫戍协议:盟约</title><style>\n:root{color-scheme:dark}body{margin:0;min-height:100vh;display:grid;place-items:center;background:#111614;color:#d8e3de;font:16px/1.6 \"Noto Sans SC\",system-ui,sans-serif}\nmain{border:1px solid #2c3a35;padding:32px 40px;max-width:520px;text-align:center}h1{margin:0;color:#4ed8af;font-size:56px;letter-spacing:4px}\np{margin:8px 0}a{color:#4ed8af}</style></head><body><main><h1>{status}</h1><p>{2}</p>\n{3}<p><a href=\"/\">返回首页 · Back to home</a></p></main></body></html>": "<!doctype html><html lang=\"en\"><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n<title>{status} · Stronghold Protocol: Alliance</title><style>\n:root{color-scheme:dark}body{margin:0;min-height:100vh;display:grid;place-items:center;background:#111614;color:#d8e3de;font:16px/1.6 \"Noto Sans SC\",system-ui,sans-serif}\nmain{border:1px solid #2c3a35;padding:32px 40px;max-width:520px;text-align:center}h1{margin:0;color:#4ed8af;font-size:56px;letter-spacing:4px}\np{margin:8px 0}a{color:#4ed8af}</style></head><body><main><h1>{status}</h1><p>{2}</p>\n{3}<p><a href=\"/\">Back to home</a></p></main></body></html>",
|
||||
"页面不存在 · Not found": "Not found",
|
||||
"禁止访问 · Forbidden": "Forbidden",
|
||||
"请求地址过长 · URI too long": "URI too long",
|
||||
"请求地址无效 · Bad request": "Bad request",
|
||||
"不支持的请求方法 · Method not allowed": "Method not allowed",
|
||||
"服务器内部错误 · Internal error": "Internal error",
|
||||
"AI·华法琳": "AI·Warfarin",
|
||||
"AI·阿米娅": "AI·Amiya",
|
||||
"AI·惊蛰": "AI·Leizi",
|
||||
"AI·杜宾": "AI·Dobermann",
|
||||
"AI·凯尔希": "AI·Kal'tsit",
|
||||
"AI·可露希尔": "AI·Closure",
|
||||
"对局核心尚未实现(平台占位 STUB):全员确认本局信息或倒计时结束后将直接结算。": "Match core not implemented (STUB): the match settles once everyone confirms or the countdown ends.",
|
||||
"精打细算玩偶": "Budget-Savvy Doll",
|
||||
"悬赏决策": "Bounty Decision",
|
||||
"道具补给": "Item Supply",
|
||||
|
||||
+26
-11
@@ -4,7 +4,7 @@
|
||||
|
||||
import { test, afterEach } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { readFileSync, readdirSync, statSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import {
|
||||
@@ -116,7 +116,7 @@ test('server wire format: text keeps the Chinese rendering for older clients; pa
|
||||
|
||||
test('public/i18n/en.json: valid, English values, placeholders kept, the pilot screens and server messages translated', () => {
|
||||
const entries = Object.entries(EN).filter(([k]) => !k.startsWith('_'));
|
||||
assert.ok(entries.length > 800, `seeded from PR #70: ${entries.length}`);
|
||||
assert.ok(entries.length >= 1000, `PR #70's seed + phase 2: ${entries.length}`);
|
||||
for (const [k, v] of entries) {
|
||||
assert.equal(typeof v, 'string', k);
|
||||
assert.ok(v.trim(), `empty translation for ${k}`);
|
||||
@@ -126,25 +126,40 @@ test('public/i18n/en.json: valid, English values, placeholders kept, the pilot s
|
||||
for (const p of want) assert.ok(have.has(p), `${k}: {${p}} missing in "${v}"`);
|
||||
}
|
||||
for (const k of ['开始', '博士代号', '设置', '语言', '同盟模拟', '创建同盟', '加入同盟', '开始模拟', 'toggle::关闭', '整备区已满',
|
||||
'第 {r} 回合 · 最终攻势', '本局禁用{names}盟约,此策略效果可能无法发挥', 'bond-threshold::名', 'list::、', '干员持有', '先锋', '开心',
|
||||
'{names}只能部署在召唤者攻击范围内,已退回整备区', '{name}博士的目标生命值已耗尽', '联防阶段:{names} 迎战突破防线的敌人', '【维多利亚】获得{n}件维式重锤']) {
|
||||
assert.ok(EN[k], `en.json lacks ${k}`);
|
||||
}
|
||||
});
|
||||
|
||||
test('every msgid of the pilot screens, the shell and the server messages is in public/i18n/en.json', async () => {
|
||||
const files = ['public/js/screens/title.js', 'public/js/screens/lobby.js', 'public/js/screens/room.js', 'public/js/ui/settings.js',
|
||||
'public/js/screens/diy.js',
|
||||
'public/js/main.js', 'public/js/ui/toasts.js', 'server/match/match/messaging.js', 'server/match/player/placement.js',
|
||||
'server/match/player/basics.js', 'server/match/player/acquire.js', 'server/match/effectsMeta.js', 'server/match/match/settle.js',
|
||||
'server/match/match/platform.js', 'server/match/match/unitePhase.js', 'server/match/match/bossRounds.js',
|
||||
'server/sim/content/bonds/addon/meta.js', 'server/sim/content/bonds/core.js'];
|
||||
test('the whole client, shared and the server messages: no Chinese literal left unwrapped, every msgid in public/i18n/en.json (extract 0, check --strict)', async () => {
|
||||
// what tools/i18n.mjs scans: public/js (not dev / vendor / assets / fonts), shared, server — server/sim only for the
|
||||
// texts it sends (msg() / ctx.toast), its own t() / N_() are other helpers
|
||||
const SKIP = new Set(['node_modules', 'vendor', 'assets', 'fonts', 'dev']);
|
||||
const files = [];
|
||||
const visit = (rel) => {
|
||||
for (const name of readdirSync(path.join(ROOT, rel)).sort()) {
|
||||
if (SKIP.has(name) || name.startsWith('.')) continue;
|
||||
const r = `${rel}/${name}`;
|
||||
if (statSync(path.join(ROOT, r)).isDirectory()) visit(r);
|
||||
else if (/\.m?js$/.test(name)) files.push(r);
|
||||
}
|
||||
};
|
||||
for (const root of ['public/js', 'shared', 'server']) visit(root);
|
||||
assert.ok(files.length > 300, `${files.length} files`);
|
||||
const missing = [];
|
||||
let used = 0;
|
||||
for (const f of files) {
|
||||
const { msgids, literals } = await scanSource(readFileSync(path.join(ROOT, f), 'utf8'), f);
|
||||
const server = f.startsWith('server/');
|
||||
for (const m of msgids) if ((!server || m.via === 'msg' || m.via === 'server') && !EN[m.msgid]) missing.push(`${f}:${m.line} ${m.msgid}`);
|
||||
if (!server) for (const l of literals) if (!l.reason) missing.push(`${f}:${l.line} not wrapped: ${l.msgid}`);
|
||||
for (const m of msgids) {
|
||||
if (server && m.via !== 'msg' && m.via !== 'server') continue;
|
||||
used++;
|
||||
if (!EN[m.msgid]) missing.push(`${f}:${m.line} ${m.msgid}`);
|
||||
}
|
||||
if (!f.startsWith('server/sim/')) for (const l of literals) if (!l.reason) missing.push(`${f}:${l.line} not wrapped: ${l.msgid}`);
|
||||
}
|
||||
assert.ok(used >= 1350, `msgid uses: ${used} (1 404 at phase 2)`);
|
||||
assert.deepEqual(missing, []);
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user