diff --git a/docs/I18N.md b/docs/I18N.md
index 9c6f3efc..7b3dcfbf 100644
--- a/docs/I18N.md
+++ b/docs/I18N.md
@@ -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`${tParts('第 {r} 回合 · 最终攻势', { r: html`${r}` })}`; // 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 ``, a ``): `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 [--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 --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
diff --git a/public/i18n/en.json b/public/i18n/en.json
index 6fc41f91..325e6795 100644
--- a/public/i18n/en.json
+++ b/public/i18n/en.json
@@ -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",
@@ -745,7 +695,7 @@
"谢谢": "Thanks",
"思考": "Thinking",
"合作愉快": "Nice teamwork",
- "表情套组:虫动": "Emote Set: Slug Moves",
+ "表情套组:虫动": "Emote Set: Slug Style",
"合作愉快!": "Nice teamwork!",
"谢谢!": "Thanks!",
"对不起!": "Sorry!",
@@ -778,20 +728,6 @@
"维维美 4": "Wiwime 4",
"维维美 5": "Wiwime 5",
"维维美 6": "Wiwime 6",
- "\n{status} · 卫戍协议:盟约
diff --git a/public/js/screens/game/overlays.js b/public/js/screens/game/overlays.js
index 4ab312a4..dbe2da76 100644
--- a/public/js/screens/game/overlays.js
+++ b/public/js/screens/game/overlays.js
@@ -3,15 +3,16 @@
import { Button, MicroLabel, html } from '../../ui/components.js';
import { emptyMatch, store } from '../../store.js';
import { localAsset } from '../../data.js';
+import { t } from '../../../../shared/i18n.js';
/** The room went back to its lobby without a result (match aborted): offer the way back. */
export function MatchEnded() {
- return html`
`;
}
@@ -755,7 +755,7 @@ export function doctorNo(id) {
export function PingPill({ ms, online = true, class: cls }) {
const ok = online && Number.isFinite(ms);
const tier = !ok ? 'off' : ms < 60 ? 'low' : ms < 200 ? 'medium' : 'high';
- return html`
+ return html`
<${Icon} name=${ok ? 'signal' : 'wifiOff'} class="ping__icon" />
${ok ? Math.min(9999, Math.round(ms)) : '--'}ms`;
diff --git a/public/js/ui/connBanner.js b/public/js/ui/connBanner.js
index dc8708a4..2b469e05 100644
--- a/public/js/ui/connBanner.js
+++ b/public/js/ui/connBanner.js
@@ -9,6 +9,7 @@ import { html, Button, Icon, useTicker } from './components.js';
import { net, CLIENT_ERR_TEXT } from '../net.js';
import { useStore, shallowEqual } from '../store.js';
import { useDocClass } from './device.js';
+import { t } from '../../../shared/i18n.js';
/** Whether the banner shows for this connection state (mirrors the early returns below). */
export function bannerVisible(conn, entered, restoring, buildStale = false) {
@@ -28,15 +29,15 @@ export function ConnectionBanner() {
if (!entered) return null;
if (conn.status === 'online' && !restoring && !buildStale) return null;
if (conn.status === 'online' && restoring) {
- return html`
<${Icon} name="refresh" />正在同步同盟状态…
`;
+ return html`
<${Icon} name="refresh" />${t('正在同步同盟状态…')}
`;
}
if (conn.status === 'online' && buildStale) {
// the server has a newer build than this page: the guard reloads by itself once the match is over, the button is
// for a player who would rather do it now
return html`
` : null;
// (`fr` is the body the unit fights with — a stand-in's record for a 补位 chess; `chess` is only the range's fallback)
blocks.stats = chessStatsBlock({ rec: fr, chess: c, live });
- blocks.skill = sk ? html`<${Section} key="skill" title="技能" micro="SKILL" class="dsec--skill">
+ blocks.skill = sk ? html`<${Section} key="skill" title=${t('技能')} micro="SKILL" class="dsec--skill">
` : null;
const out = CHESS_SECTIONS.map((k) => blocks[k]).filter(Boolean);
// a merge-completing shop / reward card: where the elite goes (shopBar mergeHint), right under the header
- if (hint) out.splice(1, 0, html`
<${Icon} name="info" />可晋升:${hint}
`);
+ if (hint) out.splice(1, 0, html`
<${Icon} name="info" />${t('可晋升:{hint}', { hint })}