diff --git a/public/i18n/en.json b/public/i18n/en.json index 0ef8dfce..6b731e88 100644 --- a/public/i18n/en.json +++ b/public/i18n/en.json @@ -1069,5 +1069,11 @@ "撤退[Q]": "Retreat [Q]", "出售(+{sell} 资金,X)": "Sell (+{sell} Funds, X)", "出售[X]": "Sell [X]", - "临时整备区 {n} 件道具待处理": "Temporary Bench: {n} {count|item|items} pending" + "临时整备区 {n} 件道具待处理": "Temporary Bench: {n} {count|item|items} pending", + "{name}:部分盟约所含干员阵容不完整({bannedN} 名干员无法出现)": "{name}: some of the Alliance's Operators cannot appear ({bannedN} {bannedN|Operator|Operators} banned)", + "{name}:部分盟约所含干员阵容不完整": "{name}: some of the Alliance's Operators cannot appear", + "联防:{names}": "Unite: {names}", + "剩余资金": "Funds Left", + "继续整备": "Keep Preparing", + "还有 {funds} 资金未使用。休整期结束时,本回合的剩余资金将清零。确定准备就绪吗?": "You still have {funds} unspent Funds. The round's leftover Funds are cleared when the Rest Phase ends. Ready anyway?" } diff --git a/public/js/ui/gameLogic/bonds.js b/public/js/ui/gameLogic/bonds.js index dca2ba9d..db2481d5 100644 --- a/public/js/ui/gameLogic/bonds.js +++ b/public/js/ui/gameLogic/bonds.js @@ -1,6 +1,7 @@ // ui/gameLogic/bonds.js — bond counts, members, harmony, 本局禁用. Re-exported from ../gameLogic.js. import { int, isObj } from './shared.js'; +import { t } from '../../../../shared/i18n.js'; // ---- bonds ------------------------------------------------------------------------------------------- @@ -275,8 +276,8 @@ export function disabledBondSets(pub, staticInactive = []) { * @param {number} bannedN banned operators of the bond */ export function briefingBondTip(name, state, bannedN = 0) { - if (state === 'off') return `${name}:本局禁用(该盟约不会激活)`; - if (state === 'drawn' || bannedN > 0) return `${name}:部分盟约所含干员阵容不完整${bannedN ? `(${bannedN} 名干员无法出现)` : ''}`; + if (state === 'off') return t('{name}:本局禁用(该盟约不会激活)', { name }); + if (state === 'drawn' || bannedN > 0) return bannedN ? t('{name}:部分盟约所含干员阵容不完整({bannedN} 名干员无法出现)', { name, bannedN }) : t('{name}:部分盟约所含干员阵容不完整', { name }); return name; } @@ -314,5 +315,5 @@ export function bandOffBonds(band, off) { */ export function bandOffLine(names) { const list = (Array.isArray(names) ? names : []).filter((n) => typeof n === 'string' && n); - return list.length ? `本局禁用${list.map((n) => `【${n}】`).join('')}盟约,此策略效果可能无法发挥` : ''; + return list.length ? t('本局禁用{names}盟约,此策略效果可能无法发挥', { names: list.map((n) => t('【{name}】', { name: n })).join('') }) : ''; } diff --git a/public/js/ui/gameLogic/format.js b/public/js/ui/gameLogic/format.js index 3cb778e4..462ee6fd 100644 --- a/public/js/ui/gameLogic/format.js +++ b/public/js/ui/gameLogic/format.js @@ -2,20 +2,21 @@ import { UF } from '../../../../shared/constants.js'; import { clamp, isObj, tileKey } from './shared.js'; +import { N_, getLang } from '../../../../shared/i18n.js'; // ---- players, statuses, fields ------------------------------------------------------------------------ /** Status → glyph + text (research 06 §11.1). */ export const STATUS_META = Object.freeze({ - acting: { glyph: 'dots', text: '行动中', tone: 'lo' }, - ready: { glyph: 'check', text: '已就绪', tone: 'mint' }, - deciding: { glyph: 'hourglass', text: '决策中', tone: 'gold' }, - combat: { glyph: 'sword', text: '作战中', tone: 'orange' }, - done: { glyph: 'check', text: '作战结束', tone: 'mint' }, - helping: { glyph: 'shield', text: '联防中', tone: 'orange' }, - left: { glyph: 'exit', text: '已离开', tone: 'red' }, - dead: { glyph: 'close', text: '已淘汰', tone: 'red' }, + acting: { glyph: 'dots', text: N_('行动中'), tone: 'lo' }, + ready: { glyph: 'check', text: N_('已就绪'), tone: 'mint' }, + deciding: { glyph: 'hourglass', text: N_('决策中'), tone: 'gold' }, + combat: { glyph: 'sword', text: N_('作战中'), tone: 'orange' }, + done: { glyph: 'check', text: N_('作战结束'), tone: 'mint' }, + helping: { glyph: 'shield', text: N_('联防中'), tone: 'orange' }, + left: { glyph: 'exit', text: N_('已离开'), tone: 'red' }, + dead: { glyph: 'close', text: N_('已淘汰'), tone: 'red' }, }); // ---- snapshots / battle HUD --------------------------------------------------------------------------------- @@ -69,12 +70,21 @@ export function attackInterval(bat, aspd = 100) { return b * 100 / a; } -/** Compact number: 12345 → '12,345'; 1.5e6 → '150万'. */ +/** + * Compact number: 12345 → '12,345'; 1.5e6 → '150万' — in English the thousands-based units: 1.5e6 → '1.5M', 2.5e5 → + * '250K', 3e9 → '3B' (万 / 亿 count in 10⁴ / 10⁸ steps, no template can move the decimal point). + */ export function fmtNum(v) { const n = Number(v); if (!Number.isFinite(n)) return '—'; - if (Math.abs(n) >= 1e8) return `${(n / 1e8).toFixed(n >= 1e9 ? 0 : 1)}亿`; - if (Math.abs(n) >= 1e5) return `${(n / 1e4).toFixed(n >= 1e6 ? 0 : 1)}万`; + if (getLang() === 'en') { + if (Math.abs(n) >= 1e9) return `${(n / 1e9).toFixed(Math.abs(n) >= 1e10 ? 0 : 1)}B`; + if (Math.abs(n) >= 1e6) return `${(n / 1e6).toFixed(Math.abs(n) >= 1e7 ? 0 : 1)}M`; + if (Math.abs(n) >= 1e5) return `${Math.round(n / 1e3)}K`; + return Math.round(n).toLocaleString('en-US'); + } + if (Math.abs(n) >= 1e8) return `${(n / 1e8).toFixed(n >= 1e9 ? 0 : 1)}亿`; // i18n-ignore: the Chinese units (English above) + if (Math.abs(n) >= 1e5) return `${(n / 1e4).toFixed(n >= 1e6 ? 0 : 1)}万`; // i18n-ignore return Math.round(n).toLocaleString('en-US'); } diff --git a/public/js/ui/gameLogic/loadout.js b/public/js/ui/gameLogic/loadout.js index a0c30f3a..ac4f9017 100644 --- a/public/js/ui/gameLogic/loadout.js +++ b/public/js/ui/gameLogic/loadout.js @@ -3,6 +3,7 @@ import { MODULE_NONE, loadoutOptions, resolveLoadout } from '../../../../shared/protocol.js'; import { loadoutRecord, resolveRecordLoadout } from '../../../../shared/loadoutRecord.js'; import { isObj } from './shared.js'; +import { t } from '../../../../shared/i18n.js'; // ---- operator loadout (DESIGN §16) ---------------------------------------------------------------------------------- @@ -34,7 +35,7 @@ export function chessLoadout(chess, loadout, getChess = () => null) { let defaultModule = true; if (chess.isGolden) { const id = r.moduleId ?? (chess.module?.active ? chess.module.id : MODULE_NONE); - if (id === MODULE_NONE) module = { id: MODULE_NONE, name: '未装备模组', typeName: '', none: true }; + if (id === MODULE_NONE) module = { id: MODULE_NONE, name: t('未装备模组'), typeName: '', none: true }; else { const rec = (Array.isArray(chess.modules) ? chess.modules : []).find((m) => isObj(m) && m.uniEquipId === id) || (isObj(chess.module) && chess.module.id === id ? { uniEquipId: id, name: chess.module.name, typeName: chess.module.type } : null); diff --git a/public/js/ui/gameLogic/phases.js b/public/js/ui/gameLogic/phases.js index 8a1bc02f..f7afe13d 100644 --- a/public/js/ui/gameLogic/phases.js +++ b/public/js/ui/gameLogic/phases.js @@ -4,6 +4,7 @@ import { PHASE } from '../../../../shared/constants.js'; import { bossLevelSeconds } from '../matchStatus.js'; import { clamp, int, isObj, sortedPlayers } from './shared.js'; import { normalizeSp } from './draft.js'; +import { t } from '../../../../shared/i18n.js'; // ---- phases ---------------------------------------------------------------------------------------- @@ -43,18 +44,18 @@ export const isBossPhase = (phase) => phase === PHASE.FINAL_ASSAULT || phase === export function phaseBanner(phase, pub) { const r = int(pub?.round, 0); switch (phase) { - case PHASE.BATTLE_CHECK: return { title: '协议启动', micro: 'PROTOCOL START', tone: 'mint', sub: '模拟即将开始', duration: 2600 }; - case PHASE.ROUND_START: return { title: `第 ${r} 回合`, micro: `ROUND ${String(r).padStart(2, '0')}`, tone: 'mint', sub: '资金已到账' }; - case PHASE.SP_DRAFT: return { title: '机变阶段', micro: 'CONTINGENCY', tone: 'gold', sub: '依次选择机变' }; - case PHASE.PREP: return { title: '休整期', micro: `ROUND ${String(r).padStart(2, '0')} // REST`, tone: 'mint', sub: '部署干员,准备迎敌' }; - case PHASE.COMBAT: return { title: '作战开始', micro: 'COMBAT', tone: 'orange', sub: '各自行动阶段' }; + case PHASE.BATTLE_CHECK: return { title: t('协议启动'), micro: 'PROTOCOL START', tone: 'mint', sub: t('模拟即将开始'), duration: 2600 }; + case PHASE.ROUND_START: return { title: t('第 {r} 回合', { r }), micro: `ROUND ${String(r).padStart(2, '0')}`, tone: 'mint', sub: t('资金已到账') }; + case PHASE.SP_DRAFT: return { title: t('机变阶段'), micro: 'CONTINGENCY', tone: 'gold', sub: t('依次选择机变') }; + case PHASE.PREP: return { title: t('休整期'), micro: `ROUND ${String(r).padStart(2, '0')} // REST`, tone: 'mint', sub: t('部署干员,准备迎敌') }; + case PHASE.COMBAT: return { title: t('作战开始'), micro: 'COMBAT', tone: 'orange', sub: t('各自行动阶段') }; case PHASE.UNITE: { - const names = new Map(sortedPlayers(pub).map((p) => [p.playerId, p.name || '博士'])); + const names = new Map(sortedPlayers(pub).map((p) => [p.playerId, p.name || t('博士')])); const helpers = Array.isArray(pub?.unite?.helpers) ? pub.unite.helpers.map((id) => names.get(id)).filter(Boolean) : []; - return { title: '联防阶段', micro: 'JOINT DEFENSE', tone: 'orange', sub: helpers.length ? `联防:${helpers.join('、')}` : '完美作战的博士迎战突破防线的敌人' }; + return { title: t('联防阶段'), micro: 'JOINT DEFENSE', tone: 'orange', sub: helpers.length ? t('联防:{names}', { names: helpers }) : t('完美作战的博士迎战突破防线的敌人') }; } - case PHASE.FINAL_ASSAULT: return { title: '最终攻势', micro: 'FINAL ASSAULT', tone: 'red', sub: '击败敌方领袖' }; - case PHASE.HIDDEN_CORE: return { title: '隐秘核心', micro: 'HIDDEN CORE', tone: 'red', sub: '被源石侵蚀的假想敌' }; + case PHASE.FINAL_ASSAULT: return { title: t('最终攻势'), micro: 'FINAL ASSAULT', tone: 'red', sub: t('击败敌方领袖') }; + case PHASE.HIDDEN_CORE: return { title: t('隐秘核心'), micro: 'HIDDEN CORE', tone: 'red', sub: t('被源石侵蚀的假想敌') }; case PHASE.SETTLE: return null; default: return null; } @@ -62,11 +63,11 @@ export function phaseBanner(phase, pub) { /** Label of the prep capsule ("休息一下" in the original). */ export function prepCapsuleLabel(phase) { - if (phase === PHASE.SP_DRAFT) return '机变阶段'; - if (phase === PHASE.ROUND_START) return '回合开始'; - if (phase === PHASE.BATTLE_CHECK) return '协议启动'; - if (phase === PHASE.SETTLE) return '回合结算'; - return '休息一下'; + if (phase === PHASE.SP_DRAFT) return t('机变阶段'); + if (phase === PHASE.ROUND_START) return t('回合开始'); + if (phase === PHASE.BATTLE_CHECK) return t('协议启动'); + if (phase === PHASE.SETTLE) return t('回合结算'); + return t('休息一下'); } // ---- countdown ------------------------------------------------------------------------------------- diff --git a/public/js/ui/gameLogic/placement.js b/public/js/ui/gameLogic/placement.js index affe0813..222a2e30 100644 --- a/public/js/ui/gameLogic/placement.js +++ b/public/js/ui/gameLogic/placement.js @@ -7,6 +7,7 @@ import { pieceDir, rangeTiles } from '../facing.js'; import { isObj, tileKey } from './shared.js'; import { boardTileOf, fieldTile } from './camera.js'; import { deployedRecord, fieldsStandIn, standInOf } from './standIn.js'; +import { t } from '../../../../shared/i18n.js'; // ---- placement (canPlace mirror) ------------------------------------------------------------------------ @@ -310,45 +311,45 @@ function unitAllowed(ctx, piece, row, col, owner = null) { */ export function canPlace(ctx, uid, target) { const no = (code, reason) => ({ ok: false, code, reason }); - if (!ctx || !ctx.editable) return no('WRONG_PHASE', '当前阶段无法进行该操作'); + if (!ctx || !ctx.editable) return no('WRONG_PHASE', t('当前阶段无法进行该操作')); const src = ctx.pieces.get(uid); - if (!src) return no('BAD_TARGET', '找不到该单位'); - if (!isObj(target)) return no('BAD_TILE', '无法部署在该位置'); + if (!src) return no('BAD_TARGET', t('找不到该单位')); + if (!isObj(target)) return no('BAD_TILE', t('无法部署在该位置')); const piece = src.piece; const isMagic = piece.kind === 'item' && ctx.getItem(piece.id)?.itemType === 'MAGIC'; if (target.area === 'hand') { const idx = target.idx; - if (!Number.isInteger(idx) || idx < 0 || idx >= GEO.HAND_SIZE) return no('BAD_TILE', '无法放置在该位置'); - if (src.area === 'hand' && src.idx === idx) return no('ALREADY', '位置未变化'); + if (!Number.isInteger(idx) || idx < 0 || idx >= GEO.HAND_SIZE) return no('BAD_TILE', t('无法放置在该位置')); + if (src.area === 'hand' && src.idx === idx) return no('ALREADY', t('位置未变化')); const occ = ctx.handAt.get(idx); if (!occ) return { ok: true, action: 'move' }; if (piece.kind === 'item') { - if (occ.piece.kind === 'chess') return isMagic ? no('BAD_TARGET', '该道具需要放置在战场上使用') : equipCheck(ctx, piece, occ.piece); + if (occ.piece.kind === 'chess') return isMagic ? no('BAD_TARGET', t('该道具需要放置在战场上使用')) : equipCheck(ctx, piece, occ.piece); return { ok: true, action: 'swap' }; } if (src.area === 'board') { if (piece.kind === 'token') return { ok: true, action: 'move' }; // back onto its stack if (occ.piece.kind === 'chess') { - if (!tileAllows(ctx, occ.piece, src.row, src.col)) return no('BAD_TILE', '交换后的单位无法部署在原位置'); + if (!tileAllows(ctx, occ.piece, src.row, src.col)) return no('BAD_TILE', t('交换后的单位无法部署在原位置')); return { ok: true, action: 'swap' }; } // the withdrawn operator goes to another free slot const free = [...Array(GEO.HAND_SIZE).keys()].some((i) => !ctx.handAt.has(i)); - return free ? { ok: true, action: 'move' } : no('HAND_FULL', '整备区已满'); + return free ? { ok: true, action: 'move' } : no('HAND_FULL', t('整备区已满')); } return { ok: true, action: 'swap' }; } if (target.area === 'board') { const { row, col } = target; - if (!Number.isInteger(row) || !Number.isInteger(col)) return no('BAD_TILE', '无法部署在该位置'); + if (!Number.isInteger(row) || !Number.isInteger(col)) return no('BAD_TILE', t('无法部署在该位置')); const inField = row >= GEO.FIELD.r0 && row <= GEO.FIELD.r1 && col >= GEO.FIELD.c0 && col <= GEO.FIELD.c1; - if (!inField) return no('BAD_TILE', '无法部署在该位置'); + if (!inField) return no('BAD_TILE', t('无法部署在该位置')); const occ = ctx.boardAt.get(tileKey(row, col)); if (piece.kind === 'item') { if (isMagic) return { ok: true, action: 'art' }; - if (!occ) return no('BAD_TARGET', '请将装备拖拽至干员身上'); + if (!occ) return no('BAD_TARGET', t('请将装备拖拽至干员身上')); return equipCheck(ctx, piece, occ.piece); } // its own tile: re-orient in place through the direction wheel (research 09 §1.2) @@ -356,38 +357,38 @@ export function canPlace(ctx, uid, target) { if (!tileAllows(ctx, piece, row, col)) { const deployable = ctx.deploy.ranged.has(tileKey(row, col)); const pos = piecePosition(ctx, piece); - return no('BAD_TILE', deployable && pos === 'MELEE' ? '近战单位只能部署在地面' : deployable && pos === 'HIGH' ? '只能部署在远程位' : '无法部署在该位置'); + return no('BAD_TILE', deployable && pos === 'MELEE' ? t('近战单位只能部署在地面') : deployable && pos === 'HIGH' ? t('只能部署在远程位') : t('无法部署在该位置')); } // a range-bound summon (战术点): inside its owner's attack range — seen from the summon's old tile when it is // dropped onto its own owner (the two swap); an outside-bound one (战术锚点) outside it const ownerSwap = src.area === 'board' && occ && occ.piece.uid === piece.ownerUid ? { row: src.row, col: src.col, piece: occ.piece } : null; const range = summonRange(ctx, piece, ownerSwap); - if (range && !range.has(tileKey(row, col))) return no('BAD_TILE', '只能部署在召唤者攻击范围内'); + if (range && !range.has(tileKey(row, col))) return no('BAD_TILE', t('只能部署在召唤者攻击范围内')); const out = summonExcluded(ctx, piece, ownerSwap); - if (out && out.has(tileKey(row, col))) return no('BAD_TILE', '只能部署在召唤者攻击范围外'); + if (out && out.has(tileKey(row, col))) return no('BAD_TILE', t('只能部署在召唤者攻击范围外')); if (src.area === 'board') { // board → board: move or swap (the occupant must be legal on the source tile — the mover's own summon excepted: // a moved operator's summons go back to the hand anyway) const ownSummon = occ && occ.piece.kind === 'token' && occ.piece.ownerUid === piece.uid; - if (occ && !ownSummon && !unitAllowed(ctx, occ.piece, src.row, src.col)) return no('BAD_TILE', '交换后的单位无法部署在原位置'); + if (occ && !ownSummon && !unitAllowed(ctx, occ.piece, src.row, src.col)) return no('BAD_TILE', t('交换后的单位无法部署在原位置')); return { ok: true, action: occ ? 'swap' : 'move' }; } if (piece.kind === 'token') { - if (occ) return no('BAD_TILE', '该位置已有单位'); + if (occ) return no('BAD_TILE', t('该位置已有单位')); const ownerDeployed = [...ctx.boardAt.values()].some((e) => e.piece.uid === piece.ownerUid); - if (Number.isInteger(piece.ownerUid) && !ownerDeployed) return no('BAD_TARGET', '召唤者尚未部署'); + if (Number.isInteger(piece.ownerUid) && !ownerDeployed) return no('BAD_TARGET', t('召唤者尚未部署')); return { ok: true, action: 'move' }; } - if ((!occ || occ.piece.kind !== 'chess') && ctx.count >= ctx.cap) return no('BOARD_FULL', '已达到部署上限'); + if ((!occ || occ.piece.kind !== 'chess') && ctx.count >= ctx.cap) return no('BOARD_FULL', t('已达到部署上限')); return { ok: true, action: occ ? 'swap' : 'move' }; } - return no('BAD_TILE', '无法放置在该位置'); + return no('BAD_TILE', t('无法放置在该位置')); } function equipCheck(ctx, itemPiece, targetPiece) { const item = ctx.getItem(itemPiece.id); - if (item?.itemType === 'MAGIC') return { ok: false, code: 'BAD_TARGET', reason: '该道具需要放置在战场上使用' }; - if (!isObj(targetPiece) || targetPiece.kind !== 'chess') return { ok: false, code: 'BAD_TARGET', reason: '装备只能配发给干员' }; + if (item?.itemType === 'MAGIC') return { ok: false, code: 'BAD_TARGET', reason: t('该道具需要放置在战场上使用') }; + if (!isObj(targetPiece) || targetPiece.kind !== 'chess') return { ok: false, code: 'BAD_TARGET', reason: t('装备只能配发给干员') }; return { ok: true, action: 'equip' }; } @@ -477,10 +478,10 @@ export function dropFailureReason(ctx, uid, tile) { let target = null; if (tile.area === 'board') target = { area: 'board', row: tile.row, col: tile.col }; else if (tile.area === 'hand') target = { area: 'hand', idx: Number.isInteger(tile.idx) ? tile.idx : tile.col }; - else if (tile.area === 'temp') return ctx.pieces.get(uid).area === 'temp' ? null : '临时整备区无法放入单位'; - else if (Number.isInteger(tile.row) && tile.row >= GEO.FIELD.r0 && tile.row <= GEO.FIELD.r1) return '无法部署在该位置'; + else if (tile.area === 'temp') return ctx.pieces.get(uid).area === 'temp' ? null : t('临时整备区无法放入单位'); + else if (Number.isInteger(tile.row) && tile.row >= GEO.FIELD.r0 && tile.row <= GEO.FIELD.r1) return t('无法部署在该位置'); else return null; const res = canPlace(ctx, uid, target); if (res.ok || res.code === 'ALREADY') return null; - return res.reason || '无法放置在该位置'; + return res.reason || t('无法放置在该位置'); } diff --git a/public/js/ui/gameLogic/result.js b/public/js/ui/gameLogic/result.js index 851bb0e2..9ba156d2 100644 --- a/public/js/ui/gameLogic/result.js +++ b/public/js/ui/gameLogic/result.js @@ -1,6 +1,7 @@ // ui/gameLogic/result.js — end-of-match result normalisation. Re-exported from ../gameLogic.js. import { int, isObj, sortedPlayers } from './shared.js'; +import { t } from '../../../../shared/i18n.js'; // ---- result ----------------------------------------------------------------------------------------------------------- @@ -24,7 +25,7 @@ export function normalizeResult(res, pub) { return { playerId: p.playerId, seat: int(p.seat, int(pp.seat, 0)), - name: p.name || pp.name || '博士', + name: p.name || pp.name || t('博士'), isBot: !!(p.isBot ?? pp.isBot), alive, lp: teamLp != null ? (alive === false ? 0 : Math.max(0, teamLp)) : ownLp, diff --git a/public/js/ui/gameLogic/shop.js b/public/js/ui/gameLogic/shop.js index 79b33d94..a139c285 100644 --- a/public/js/ui/gameLogic/shop.js +++ b/public/js/ui/gameLogic/shop.js @@ -2,6 +2,7 @@ import { GEO } from '../../../../shared/constants.js'; import { isObj } from './shared.js'; +import { t } from '../../../../shared/i18n.js'; // ---- shop --------------------------------------------------------------------------------------------- @@ -120,12 +121,12 @@ export function offerHeader(offer) { const items = slots.some((s) => isObj(s) && s.kind === 'item'); const label = isObj(offer) && typeof offer.label === 'string' && offer.label ? offer.label : null; const queued = isObj(offer) && Number.isInteger(offer.queued) && offer.queued > 0 ? offer.queued : 0; - const tail = { queued, more: queued ? `之后还有 ${queued} 项` : null }; + const tail = { queued, more: queued ? t('之后还有 {queued} 项', { queued }) : null }; if (!items && (!isObj(offer) || offer.source === 'merge' || offer.source == null) && !label) { - return { title: '晋升奖励', micro: 'PROMOTION', sub: '免费选择 1 名', icon: 'crown', items: false, pill: '晋升奖励待选择', ...tail }; + return { title: t('晋升奖励'), micro: 'PROMOTION', sub: t('免费选择 1 名'), icon: 'crown', items: false, pill: t('晋升奖励待选择'), ...tail }; } - const title = label || (items ? '装备补给' : '特殊招募'); - return { title, micro: 'SPECIAL', sub: items ? '免费选择 1 件' : '免费选择 1 名', icon: 'refresh', items, pill: `${title}待选择`, ...tail }; + const title = label || (items ? t('装备补给') : t('特殊招募')); + return { title, micro: 'SPECIAL', sub: items ? t('免费选择 1 件') : t('免费选择 1 名'), icon: 'refresh', items, pill: t('{title}待选择', { title }), ...tail }; } /** @@ -135,26 +136,26 @@ export function offerHeader(offer) { * @returns {string|null} Chinese reason */ export function shopBlockReason(kind, { priv, editable, slot } = {}) { - if (!priv) return '尚未就绪'; - if (priv.alive === false) return '你已被淘汰'; - if (kind === 'ready') return priv.canReady === false ? '临时整备区不为空,请先处理溢出的资源' : null; + if (!priv) return t('尚未就绪'); + if (priv.alive === false) return t('你已被淘汰'); + if (kind === 'ready') return priv.canReady === false ? t('临时整备区不为空,请先处理溢出的资源') : null; if (!editable) { - if (kind === 'reward') return '当前无法选择'; - return priv.ready ? '已准备就绪,取消准备后才能操作' : '当前阶段无法进行该操作'; + if (kind === 'reward') return t('当前无法选择'); + return priv.ready ? t('已准备就绪,取消准备后才能操作') : t('当前阶段无法进行该操作'); } const funds = Number(priv.funds) || 0; const shop = priv.shop || {}; if (kind === 'buy' || kind === 'reward') { - if (!isObj(slot) || slot.sold) return kind === 'reward' ? '已选择' : '已售出'; - if ((Number(slot.price) || 0) > funds) return '资金不足'; + if (!isObj(slot) || slot.sold) return kind === 'reward' ? t('已选择') : t('已售出'); + if ((Number(slot.price) || 0) > funds) return t('资金不足'); // a full hand refuses every purchase / pick, a merge-completing one too (PRTS 卫戍协议/帮助 §手牌区, GitHub #82) - if (handFull(priv)) return '整备区已满'; + if (handFull(priv)) return t('整备区已满'); return null; } - if (kind === 'refresh') return (Number(shop.refreshPrice) || 0) > funds ? '资金不足' : null; + if (kind === 'refresh') return (Number(shop.refreshPrice) || 0) > funds ? t('资金不足') : null; if (kind === 'levelUp') { - if ((Number(shop.level) || 1) >= (Number(shop.maxLevel) || 6)) return '调度中心已达最高等级'; - return (Number(shop.upgradePrice) || 0) > funds ? '资金不足' : null; + if ((Number(shop.level) || 1) >= (Number(shop.maxLevel) || 6)) return t('调度中心已达最高等级'); + return (Number(shop.upgradePrice) || 0) > funds ? t('资金不足') : null; } return null; } @@ -177,7 +178,7 @@ export function readyFundsPrompt(priv, { ready = true, keptBands = null, autopla const kept = Array.isArray(keptBands) ? keptBands : ['band_cannot']; if (typeof priv.bandId === 'string' && kept.includes(priv.bandId)) return null; return { - title: '剩余资金', micro: 'FUNDS LEFT', okText: '准备就绪', cancelText: '继续整备', - text: `还有 ${funds} 资金未使用。休整期结束时,本回合的剩余资金将清零。确定准备就绪吗?`, + title: t('剩余资金'), micro: 'FUNDS LEFT', okText: t('准备就绪'), cancelText: t('继续整备'), + text: t('还有 {funds} 资金未使用。休整期结束时,本回合的剩余资金将清零。确定准备就绪吗?', { funds }), }; } diff --git a/public/js/ui/gameLogic/watch.js b/public/js/ui/gameLogic/watch.js index a8b537a7..72d8f056 100644 --- a/public/js/ui/gameLogic/watch.js +++ b/public/js/ui/gameLogic/watch.js @@ -2,6 +2,7 @@ import { isObj, sortedPlayers } from './shared.js'; import { isCombatPhase } from './phases.js'; +import { t } from '../../../../shared/i18n.js'; /** Own normal field id (DESIGN §8.3). */ @@ -47,8 +48,8 @@ export function cycleField(fields, current, dir = 1) { * @returns {{ fieldId: string } | { reason: string }} */ export function watchTarget(p, pub, myId) { - if (!isObj(p)) return { reason: '无效的目标' }; - if (p.alive === false) return { reason: '该队友已被淘汰,无法查看其阵地' }; + if (!isObj(p)) return { reason: t('无效的目标') }; + if (p.alive === false) return { reason: t('该队友已被淘汰,无法查看其阵地') }; const combat = isCombatPhase(pub?.phase); const fieldId = (combat && typeof p.fieldId === 'string' && p.fieldId) || ownFieldId(p.playerId); if (combat) { @@ -57,7 +58,7 @@ export function watchTarget(p, pub, myId) { const me = sortedPlayers(pub).find((x) => x.playerId === myId); const mine = fields.find((x) => Array.isArray(x.players) && x.players.includes(myId)); if (f && (f.kind === 'boss' || f.kind === 'hidden') && me?.alive !== false && mine && mine.fieldId !== f.fieldId) { - return { reason: '无法查看另一组队友的战场' }; + return { reason: t('无法查看另一组队友的战场') }; } } return { fieldId }; @@ -76,9 +77,9 @@ export function switcherLabel(pub, watching, myId, spectating = false) { const cur = fields.find((f) => f.fieldId === watching); if (cur) return fieldLabel(cur, pub, myId); if (typeof watching === 'string' && watching.startsWith('n:') && watching !== ownFieldId(myId)) { - return sortedPlayers(pub).find((p) => p.playerId === watching.slice(2))?.name || '队友'; + return sortedPlayers(pub).find((p) => p.playerId === watching.slice(2))?.name || t('队友'); } - return spectating ? '观战' : '自己'; + return spectating ? t('观战') : t('自己'); } /** @@ -89,16 +90,16 @@ export function switcherLabel(pub, watching, myId, spectating = false) { */ export function fieldLabel(field, pub, myId) { if (!isObj(field)) return '—'; - const names = new Map(sortedPlayers(pub).map((p) => [p.playerId, p.name || '博士'])); + const names = new Map(sortedPlayers(pub).map((p) => [p.playerId, p.name || t('博士')])); const ps = Array.isArray(field.players) ? field.players : []; - if (field.kind === 'unite') return ps.includes(myId) ? '联防(自己)' : '联防阵地'; + if (field.kind === 'unite') return ps.includes(myId) ? t('联防(自己)') : t('联防阵地'); if (field.kind === 'boss' || field.kind === 'hidden') { - if (ps.includes(myId)) return ps.length > 1 ? '全景' : '自己'; - return ps.map((id) => names.get(id) || '博士').join(' · ') || '领袖战场'; + if (ps.includes(myId)) return ps.length > 1 ? t('全景') : t('自己'); + return ps.map((id) => names.get(id) || t('博士')).join(' · ') || t('领袖战场'); } - if (ps.includes(myId) || field.fieldId === ownFieldId(myId)) return '自己'; + if (ps.includes(myId) || field.fieldId === ownFieldId(myId)) return t('自己'); const id = ps[0] ?? String(field.fieldId || '').replace(/^n:/, ''); - return names.get(id) || '队友'; + return names.get(id) || t('队友'); } /** diff --git a/public/js/ui/teamPanel.js b/public/js/ui/teamPanel.js index a13ed5b5..8ef00f23 100644 --- a/public/js/ui/teamPanel.js +++ b/public/js/ui/teamPanel.js @@ -111,8 +111,8 @@ export function TeamPanel({ pub, myId, watching, bubbles, onWatch, compact = fal <${LpTower} value=${lp.lp} size="sm" tone=${Number.isFinite(lp.lp) && lp.lp - lp.pending <= 5 ? 'danger' : null} pending=${lp.pending} tip=${rowLpTip(lp, cap)} /> ${lp.left != null ? html`<${MissTag} n=${lp.left} name=${self ? null : p.name || t('博士')} />` : null} - <${Tooltip} text=${offline ? t('连接已断开') : meta.text} placement="right"> - + <${Tooltip} text=${offline ? t('连接已断开') : t(meta.text)} placement="right"> + ${offline ? html`<${LocalSprite} name="icon_lost_connect" fallback=${html`<${Icon} name="wifiOff" />`} />` : STATUS_SPRITE[status] ? html`<${LocalSprite} name=${STATUS_SPRITE[status]} fallback=${html`<${GIcon} name=${meta.glyph} />`} />` : html`<${GIcon} name=${meta.glyph} />`}