diff --git a/public/i18n/en.json b/public/i18n/en.json index 35303830..0ef8dfce 100644 --- a/public/i18n/en.json +++ b/public/i18n/en.json @@ -1010,5 +1010,64 @@ "全部持有:没有由替补干员上场的棋子": "Everything is owned: no chess takes the field as a stand-in", "没有可下掉的干员": "No Operators can be marked", "{name}({note})": "{name} ({note})", - "本局耗时 {n} 分钟": "Duration: {n} {mins|minute|minutes}" + "本局耗时 {n} 分钟": "Duration: {n} {mins|minute|minutes}", + "{tip} · {n} 名成员": "{tip} · {n} {n|member|members}", + "{names} 在场:核心盟约激活人数 +{harmony}": "{names} on the field: Core Alliance head count +{harmony}", + "{harmonyName}已激活:核心盟约激活人数 +{harmony}": "{harmonyName} active: Core Alliance head count +{harmony}", + "层数不显示": "Stacks not shown", + "已激活 · {tier} 阶": "Active · Tier {tier}", + "已激活": "Active", + "bond-threshold::名": "+", + "({note})": " ({note})", + "观战中,可点击左侧成员头像前往查看": "Spectating · tap a member's avatar on the left to view their field", + "基础 {v}": "Base {v}", + "{items} → 【{name}】": "{items} → [{name}]", + "list::、": ", ", + "与{morph}一同装备时,携带者视为【{bond}】成员": "Equipped together with {morph}, the wearer counts as a member of [{bond}]", + ":在场 {count}/{cap}": ": {count}/{cap} on the field", + ":在场 {count}": ": {count} on the field", + "拖拽至干员身上进行配发(每名干员最多 2 件,配发后无法取下)": "Drag onto an Operator to equip (up to 2 per Operator; equipped Equipment cannot be removed)", + "调度中心不出售 · 获取途径:{source}": "Not sold in the Dispatch Center · Source: {source}", + "免疫:{list}": "Immune to: {list}", + "确定要出售精锐干员「{name}」吗?出售后获得 {price} 资金。": "Sell the Elite Operator \"{name}\"? You get {price} Funds.", + "之后的每场作战": "every later battle", + "还剩 {left} 场作战": "{left} {left|battle|battles} left", + "第 {r} 回合 · 即将迎击 {n} 名敌人": "Round {r} · {n} incoming {count|enemy|enemies}", + "特训敌人·": "Tactical Training/", + "{bandOwner} 的策略": "{bandOwner}'s Strategy", + "确认替换": "Replace", + "「{name}」的装备栏已满(2/2)。选择一件装备进行替换,{warn}。": "\"{name}\" has no free Equipment slot (2/2). Choose a piece of Equipment to replace: {warn}.", + "将销毁「{old}」并配发「{item}」": "Destroys \"{old}\" and equips \"{item}\"", + "{name} 漏过的敌人还剩 {n} 个(联防中)": "{n} {n|enemy|enemies} that got past {name} still standing (Unite in progress)", + "你漏过的敌人还剩 {n} 个(队友正在迎战)": "{n} {n|enemy|enemies} that got past you still standing (your teammates are engaging)", + "{name} 漏过的敌人已全部被击倒": "Every enemy that got past {name} has been defeated", + "你漏过的敌人已全部被击倒": "Every enemy that got past you has been defeated", + "目标生命值 {lp}:本回合已有 {cap} 个以上敌人进入蓝门,结算时扣除 {pending} 点(每回合至多 {cap} 点)": "LP {lp}: {cap}+ enemies reached the blue gate this round; settlement costs {pending} LP (max {cap} per round)", + "目标生命值 {lp}:本回合已有 {pending} 个敌人进入蓝门,结算时扣除 {pending} 点(每回合至多 {cap} 点)": "LP {lp}: {pending} {pending|enemy|enemies} reached the blue gate this round; settlement costs {pending} LP (max {cap} per round)", + "临时整备区还有 {count} 个单位:{rule}": "{count} {count|unit is|units are} still on the Temporary Bench: {rule}", + "临时整备区 {n} 个单位待处理": "Temporary Bench: {n} {count|unit|units} pending", + "{n} 秒后全队生命值开始流失": "Team LP starts draining in {n} {count|second|seconds}", + "超时 · 生命值 {n}/秒": "Overtime · LP {n}/s", + "这份调配来自更新的版本(v{v}),请先更新游戏": "This loadout comes from a newer version (v{v}). Update the game first.", + "{n}秒": "{n}s", + "干员持有是局外设置,修改将在下一局生效": "The Operator Roster is set outside matches; changes apply from the next match", + "离开观战": "Stop Spectating", + "离开观战席并返回大厅,本局模拟不受影响;观战席空着时可以凭同盟密钥再次观战。": "Leave the spectator seat and return to the lobby; the simulation goes on. While a spectator seat is free you can watch again with the Alliance Key.", + "{away}:由 AI 托管你的席位(自动部署、准备与选择),随时可以返回。": "{away}: the AI takes over your seat (deploying, readying and picking); you can come back at any time.", + "{quit}:离开后无法返回本局,你的干员将回到共享卡池。": "{quit}: you cannot come back to this match, and your Operators return to the shared pool.", + "灰色:部分盟约所含干员阵容不完整(仍可通过其他盟约的干员或装备激活)": "Gray: some of the Alliance's Operators cannot appear (it can still be activated by Operators of other Alliances or by Equipment)", + "这不是干员持有的数据": "This is not Operator Roster data", + "里面没有干员持有的数据": "It holds no Operator Roster data", + "{name},价格 {price}": "{name}, price {price}", + "技能 {slot}:{name}": "Skill {slot}: {name}", + "技能:{name}": "Skill: {name}", + "目标生命值 {lp},联防中:漏过的敌人还剩 {left} 个,按现在结算扣除 {pending} 点(每回合至多 {cap} 点)": "LP {lp}, Unite in progress: {left} {left|enemy|enemies} that got through still standing; settling now would cost {pending} LP (max {cap} per round)", + "目标生命值 {lp},联防中,结算时扣除至多 {pending} 点": "LP {lp}, Unite in progress: settlement costs up to {pending} LP", + "目标生命值 {lp},结算时扣除 {pending} 点": "LP {lp}: settlement costs {pending} LP", + "{name} 操作": "{name} actions", + "撤退至整备区(Q)": "Retreat to Bench (Q)", + "撤退[Q]": "Retreat [Q]", + "出售(+{sell} 资金,X)": "Sell (+{sell} Funds, X)", + "出售[X]": "Sell [X]", + "临时整备区 {n} 件道具待处理": "Temporary Bench: {n} {count|item|items} pending" } diff --git a/public/js/battle/observe.js b/public/js/battle/observe.js index 7b4112a8..f18708bd 100644 --- a/public/js/battle/observe.js +++ b/public/js/battle/observe.js @@ -10,6 +10,7 @@ import { PHASE } from '../../../shared/constants.js'; import { data } from '../data.js'; +import { t } from '../../../shared/i18n.js'; const isObj = (v) => !!v && typeof v === 'object'; const COMBAT = new Set([PHASE.COMBAT, PHASE.UNITE, PHASE.FINAL_ASSAULT, PHASE.HIDDEN_CORE]); @@ -27,7 +28,7 @@ export function fieldOf(pub, playerId) { /** Display name of a player id ('队友' when unknown). */ export function nameOf(pub, playerId) { - return players(pub).find((p) => p.playerId === playerId)?.name || '队友'; + return players(pub).find((p) => p.playerId === playerId)?.name || t('队友'); } /** @@ -39,20 +40,20 @@ export function nameOf(pub, playerId) { * (its result is on the way to the server) */ export function observeTarget(p, pub, myId, { observing = false, ownDone = false } = {}) { - if (!isObj(p)) return { reason: '无效的目标' }; + if (!isObj(p)) return { reason: t('无效的目标') }; if (p.playerId === myId) return observing ? { back: true } : { reason: null }; - if (p.alive === false || p.status === 'left') return { reason: '该队友已被淘汰,无法查看' }; + if (p.alive === false || p.status === 'left') return { reason: t('该队友已被淘汰,无法查看') }; const phase = pub?.phase; const me = players(pub).find((x) => x.playerId === myId) || null; const meAlive = me ? me.alive !== false : true; if (!COMBAT.has(phase)) return { fieldId: `n:${p.playerId}` }; const target = fieldOf(pub, p.playerId); - if (!target) return { reason: '该队友当前没有战场' }; + if (!target) return { reason: t('该队友当前没有战场') }; const own = fieldOf(pub, myId); if (!meAlive || !own) return { fieldId: target.fieldId }; - if (own.fieldId === target.fieldId) return { reason: '队友与你在同一战场,使用 ‹ › 切换视角' }; - if (target.kind === 'boss' || target.kind === 'hidden') return { reason: '无法查看另一组队友的战场' }; - if (own.kind === 'normal' && own.live !== false && !ownDone) return { reason: '作战中无法查看队友,作战结束后可前往查看' }; + if (own.fieldId === target.fieldId) return { reason: t('队友与你在同一战场,使用 ‹ › 切换视角') }; + if (target.kind === 'boss' || target.kind === 'hidden') return { reason: t('无法查看另一组队友的战场') }; + if (own.kind === 'normal' && own.live !== false && !ownDone) return { reason: t('作战中无法查看队友,作战结束后可前往查看') }; return { fieldId: target.fieldId }; } @@ -91,7 +92,7 @@ export function teammateProgress(pub, myId) { const p = players(pub).find((x) => x.playerId === pid); const pr = isObj(f.progress) ? f.progress : null; out.push({ - playerId: pid, name: p?.name || '队友', isBot: !!p?.isBot, + playerId: pid, name: p?.name || t('队友'), isBot: !!p?.isBot, killed: Number.isFinite(pr?.killed) ? pr.killed : null, total: Number.isFinite(pr?.total) ? pr.total : null, done: f.live === false || !!pr?.done, }); @@ -114,13 +115,13 @@ export function cameraLayers(field, pub, myId) { if (!isObj(field) || (field.kind !== 'unite' && field.kind !== 'boss' && field.kind !== 'hidden')) return []; const sides = sidesOf(field); const at = (side) => Object.keys(sides).find((pid) => sides[pid] === side) || null; - const label = (pid) => (!pid ? '无人在家' : pid === myId ? '你自己' : nameOf(pub, pid)); + const label = (pid) => (!pid ? t('无人在家') : pid === myId ? t('你自己') : nameOf(pub, pid)); const left = at('L'); const right = at('R'); if ((field.kind === 'boss' || field.kind === 'hidden') && (!left || !right)) return []; return [ { key: 'L', label: label(left), self: left === myId, watch: !!left && left !== myId }, - { key: 'ALL', label: '全景', self: false, watch: false }, + { key: 'ALL', label: t('全景'), self: false, watch: false }, { key: 'R', label: label(right), self: right === myId, watch: !!right && right !== myId }, ]; } diff --git a/public/js/net.js b/public/js/net.js index ce2d362f..b921a444 100644 --- a/public/js/net.js +++ b/public/js/net.js @@ -31,6 +31,7 @@ import { PROTOCOL_VERSION, ERR_TEXT } from '../../shared/constants.js'; import { validateC2S } from '../../shared/protocol.js'; +import { N_ } from '../../shared/i18n.js'; export const REQUEST_TIMEOUT_MS = 8000; export const HELLO_TIMEOUT_MS = 8000; @@ -40,12 +41,12 @@ export const BACKOFF = Object.freeze({ base: 500, factor: 2, max: 10000, jitter: /** Client-side error codes (in addition to shared ERR codes). */ export const CLIENT_ERR_TEXT = Object.freeze({ - TIMEOUT: '请求超时,请重试', - OFFLINE: '未连接到服务器', - DISCONNECTED: '连接已断开,请重试', - CLOSED: '连接已关闭', - REPLACED: '该身份已在其他页面登录', - VERSION: '客户端版本与服务器不一致,请刷新页面', + TIMEOUT: N_('请求超时,请重试'), + OFFLINE: N_('未连接到服务器'), + DISCONNECTED: N_('连接已断开,请重试'), + CLOSED: N_('连接已关闭'), + REPLACED: N_('该身份已在其他页面登录'), + VERSION: N_('客户端版本与服务器不一致,请刷新页面'), }); /** Server close code: the session was taken over by another socket (server/net.js CLOSE.REPLACED). */ @@ -62,7 +63,7 @@ const QUIET_SWAP_MIN_AGE_MS = 5000; * @returns {string} */ export function errorText(code, msg) { - return ERR_TEXT[code] || CLIENT_ERR_TEXT[code] || (typeof msg === 'string' && msg) || String(code || '未知错误'); + return ERR_TEXT[code] || CLIENT_ERR_TEXT[code] || (typeof msg === 'string' && msg) || String(code || N_('未知错误')); } /** Error thrown/rejected by requests. `code` is an ERR code or a CLIENT_ERR_TEXT key. */ diff --git a/public/js/screens/loadout.js b/public/js/screens/loadout.js index e9d5a66f..c716f919 100644 --- a/public/js/screens/loadout.js +++ b/public/js/screens/loadout.js @@ -299,7 +299,7 @@ function Detail({ m, chess, golden, entries, onChange, onReset, locked, notOwned
本模式下该盟约不会激活(其干员仍可能因所属的其他盟约出现)
` : null} + ${off ? html`${t('本模式下该盟约不会激活(其干员仍可能因所属的其他盟约出现)')}
` : null}${timed ? '倒计时结束后仍未选定将自动分配' : '选择一项(无时间限制)'}${mine == null && myTurn ? ' · 点击卡牌选中,再次点击确认' : ''}
+ <${MicroLabel} tone="mint">${t('CONTINGENCY // 机变阶段')}${MicroLabel}> +${timed ? t('倒计时结束后仍未选定将自动分配') : t('选择一项(无时间限制)')}${mine == null && myTurn ? t(' · 点击卡牌选中,再次点击确认') : ''}
搭配以下装备时,携带者视为对应盟约的成员:
-${t('搭配以下装备时,携带者视为对应盟约的成员:')}
+暂未生效:需与上表中的一件装备一同携带
` : null} + ${wearer && !rows.some((r) => r.worn) ? html`${t('暂未生效:需与上表中的一件装备一同携带')}
` : null}- <${Icon} name="info" />与${morph.name}一同装备时,携带者视为【${bond.name}】成员${isOff ? html`本局禁用` : null}${worn - ? html`${isOff ? '已搭配' : '生效中'}` : null} + <${Icon} name="info" />${t('与{morph}一同装备时,携带者视为【{bond}】成员', { morph: morph.name, bond: bond.name })}${isOff ? html`${t('本局禁用')}` : null}${worn + ? html`${isOff ? t('已搭配') : t('生效中')}` : null}
`; } @@ -233,7 +233,7 @@ export function BondChips({ bondIds, bonds = [], onBond = null, off = null, gran if (!ids.length) return null; const iso = new Set(Array.isArray(granted) ? granted : []); const mine = new Map((Array.isArray(bonds) ? bonds : []).filter((b) => b && typeof b.bondId === 'string').map((b) => [b.bondId, b])); - return html`<${Icon} name="info" /><${RichText} text=${traitText(body, golden, lo)} />
` : 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">拖拽装备至该干员以配发(最多 2 件)
`} + blocks.equip = piece?.kind === 'chess' ? html`<${Section} key="equip" title=${t('装备')} micro=${`EQUIP ${items.length}/2`} class="dsec--equip"> + ${items.length ? items.map((it) => html`<${ItemRow} key=${it.uid} itemId=${it.id} carried=${items} off=${offBonds} />`) : html`${t('拖拽装备至该干员以配发(最多 2 件)')}
`} />` // no own piece (a teammate's unit, a bond popup's 变形同构体 row): what it carries, read-only - : !piece && carried.length ? html`<${Section} key="equip" title="装备" micro=${`EQUIP ${carried.length}/2`} class="dsec--equip"> + : !piece && carried.length ? html`<${Section} key="equip" title=${t('装备')} micro=${`EQUIP ${carried.length}/2`} class="dsec--equip"> ${carried.map((id, i) => html`<${ItemRow} key=${`${i}:${id}`} itemId=${id} carried=${carried} off=${offBonds} />`)} />` : null; const talents = chessTalents(fr); - blocks.talents = talents.length ? html`<${Section} key="talents" title="天赋" micro="TALENT" class="dsec--talent"> + blocks.talents = talents.length ? html`<${Section} key="talents" title=${t('天赋')} micro="TALENT" class="dsec--talent"> ${talents.map((t, i) => html`<${Icon} name="info" />可晋升:${hint}
`); + if (hint) out.splice(1, 0, html`<${Icon} name="info" />${t('可晋升:{hint}', { hint })}
`); return out; } @@ -478,21 +478,21 @@ export function ItemDetail({ item, piece, editable, onDestroy, offBonds = null }<${Icon} name="info" />${item.note}
` : null} ${item.itemType === 'MAGIC' - ? html`<${Icon} name="info" />将其拖拽至战场上的格子使用
` - : html`<${Icon} name="info" />拖拽至干员身上进行配发(每名干员最多 2 件,配发后无法取下)${item.mergeable ? ';2 件相同装备自动合成进阶装备' : ''}
`} - ${item.shopExcluded ? html`<${Icon} name="info" />调度中心不出售 · 获取途径:${item.shopExcludedBy || '效果获得'}
` : null} - ${piece && editable ? html`<${Icon} name="info" />${t('将其拖拽至战场上的格子使用')}
` + : html`<${Icon} name="info" />${t('拖拽至干员身上进行配发(每名干员最多 2 件,配发后无法取下)')}${item.mergeable ? t(';2 件相同装备自动合成进阶装备') : ''}
`} + ${item.shopExcluded ? html`<${Icon} name="info" />${t('调度中心不出售 · 获取途径:{source}', { source: item.shopExcludedBy || t('效果获得') })}
` : null} + ${piece && editable ? html`<${Icon} name="shield" />免疫:${imm.join('、')}
` : null} - ${Array.isArray(enemy.abilities) && enemy.abilities.length ? html`<${Section} title="能力" micro="ABILITIES"> + ${imm.length ? html`<${Icon} name="shield" />${t('免疫:{list}', { list: imm })}
` : null} + ${Array.isArray(enemy.abilities) && enemy.abilities.length ? html`<${Section} title=${t('能力')} micro="ABILITIES"><${Icon} name="info" />${hint}
` : null} - ${token.descRaw || token.desc ? html`<${Section} title="说明"><${RichText} as="p" text=${token.descRaw || token.desc} class="dtext" />/>` : null} - ${skill ? html`<${Section} title="技能">${skill.name} ${skill.desc}
/>` : null} - ${talents.length ? html`<${Section} title="天赋">${talents.map((t, i) => html`${t.name} ${t.desc}
`)}/>` : null}`; + ${token.descRaw || token.desc ? html`<${Section} title=${t('说明')}><${RichText} as="p" text=${token.descRaw || token.desc} class="dtext" />/>` : null} + ${skill ? html`<${Section} title=${t('技能')}>${skill.name} ${skill.desc}
/>` : null} + ${talents.length ? html`<${Section} title=${t('天赋')}>${talents.map((t, i) => html`${t.name} ${t.desc}
`)}/>` : null}`; } /** @@ -741,18 +741,18 @@ export function DetailPanel({ detail, editable, snapHp, onClose, onSell, onDestr const sellIt = async (piece, chess) => { const golden = piece.golden || chess?.isGolden; if (golden) { - const ok = await confirmDialog({ title: '出售精锐干员', text: `确定要出售精锐干员「${chess?.name || ''}」吗?出售后获得 ${chess?.sellPrice ?? 1} 资金。`, okText: '出售', danger: true }); + const ok = await confirmDialog({ title: t('出售精锐干员'), text: t('确定要出售精锐干员「{name}」吗?出售后获得 {price} 资金。', { name: chess?.name || '', price: chess?.sellPrice ?? 1 }), okText: t('出售'), danger: true }); if (!ok) return; } onSell(piece); }; const destroyIt = async (piece, item) => { - const ok = await confirmDialog({ title: '销毁道具', text: `道具无法出售。确定要销毁「${item?.name || ''}」吗?`, okText: '销毁', danger: true }); + const ok = await confirmDialog({ title: t('销毁道具'), text: t('道具无法出售。确定要销毁「{name}」吗?', { name: item?.name || '' }), okText: t('销毁'), danger: true }); if (ok) onDestroy(piece); }; - return html`