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

${chess.name}

${chess.appellation || ''} - <${Img} src=${profGlyphUrl(m, chess.profession)} class="lo-dhead__prof lo-profglyph" />${PROF_NAME[chess.profession] || ''} + <${Img} src=${profGlyphUrl(m, chess.profession)} class="lo-dhead__prof lo-profglyph" />${t(PROF_NAME[chess.profession] || '')} <${Img} src=${subProfIconUrl(m, chess)} class="lo-dhead__prof" />${chess.subProfessionName || ''} ${(chess.bonds || []).map((b) => html` @@ -365,8 +365,8 @@ function Filters({ m, filters, onFilters, bonds }) {
${PROF_ORDER.map((p) => html``)} + aria-pressed=${filters.prof === p ? 'true' : 'false'} title=${t(PROF_NAME[p])} onClick=${() => set({ prof: filters.prof === p ? null : p })}> + <${Img} src=${profGlyphUrl(m, p)} class="lo-chip__icon lo-profglyph" fallback=${html`${t(PROF_NAME[p])[0]}`} />${t(PROF_NAME[p])}`)}
`; } const m = data.get('assets'); const shown = sorted.slice(0, max); const strip = html`
+ aria-label=${owner ? t('{owner} 的盟约', { owner }) : t('我的盟约')} data-owner=${owner || null}> ${owner ? html`<${OwnerTag} owner=${owner} />` : null} ${shown.map((b) => { const rec = data.lookup('bonds', b.bondId); @@ -64,8 +64,8 @@ export function BondStrip({ bonds, layersDisabled = false, onOpen, openId = null class=${cx('bslot', 'is-off', openId === b.bondId && 'is-open')}> <${BondDisc} name=${name} icon=${bondIconUrl(m, b.bondId)} tier=${0} maxTier=${Math.max(1, th.length)} active=${false} disabled=${true} size="sm" showName=${true} layersDisabled=${layersDisabled} onClick=${() => onOpen(b.bondId)} - title=${`${briefingBondTip(name, 'off')} · ${b.count ?? 0} 名成员`} /> - 本局禁用 + title=${t('{tip} · {n} 名成员', { tip: briefingBondTip(name, 'off'), n: b.count ?? 0 })} /> + ${t('本局禁用')}
`; } const next = nextThreshold(b.count ?? 0, th); @@ -74,13 +74,13 @@ export function BondStrip({ bonds, layersDisabled = false, onOpen, openId = null <${BondDisc} name=${rec?.name || b.bondId} icon=${bondIconUrl(m, b.bondId)} layers=${rec?.noStack ? undefined : b.layers ?? 0} tier=${b.tier ?? 0} maxTier=${Math.max(1, th.length)} active=${!!b.active} size="sm" showName=${true} layersDisabled=${layersDisabled} onClick=${() => onOpen(b.bondId)} - title=${`${rec?.name || b.bondId} ${b.count ?? 0}/${next ?? th[th.length - 1] ?? '-'}${b.harmony > 0 ? `(含调和 +${b.harmony})` : ''}`} /> + title=${`${rec?.name || b.bondId} ${b.count ?? 0}/${next ?? th[th.length - 1] ?? '-'}${b.harmony > 0 ? t('(含调和 +{harmony})', { harmony: b.harmony }) : ''}`} /> ${b.count ?? 0}/${next ?? th[th.length - 1] ?? '-'} `; })} ${sorted.length > shown.length ? html`+${sorted.length - shown.length}` : null} `; - return layersDisabled ? html`<${Tooltip} text="层数叠加已禁用" placement="bottom">${strip}` : strip; + return layersDisabled ? html`<${Tooltip} text=${t('层数叠加已禁用')} placement="bottom">${strip}` : strip; } /** @@ -122,38 +122,39 @@ export function BondPopup({ bondId, entry, priv, banned = [], onClose, onMember, const si = memberStandIn(priv, rawChess(x.id), data.get('backups'), rawChess); return si ? { ...x, name: si.name } : x; }) : []; - const harmonyName = data.lookup('bonds', HARMONY_BOND)?.name || '调和'; - const harmonyText = `${harmonyBy.length ? `${harmonyBy.map((x) => x.name).join('、')} 在场` : `${harmonyName}已激活`}:核心盟约激活人数 +${harmony}`; + const harmonyName = data.lookup('bonds', HARMONY_BOND)?.name || tName('调和'); + const harmonyText = harmonyBy.length ? t('{names} 在场:核心盟约激活人数 +{harmony}', { names: harmonyBy.map((x) => x.name), harmony }) + : t('{harmonyName}已激活:核心盟约激活人数 +{harmony}', { harmonyName, harmony }); return html`