From 166c4fbbfb289a84e7b519bb6110edbf0cdab6f7 Mon Sep 17 00:00:00 2001 From: sganggs Date: Tue, 6 Oct 2026 17:52:19 +0800 Subject: [PATCH] =?UTF-8?q?feedback5=20W5S:=20i18n=20phase=202,=20screens?= =?UTF-8?q?=20=E2=80=94=20band=20draft,=20briefing,=20game=20screen=20and?= =?UTF-8?q?=20its=20overlays,=20=E5=B9=B2=E5=91=98=E8=B0=83=E9=85=8D=20/?= =?UTF-8?q?=20=E5=B9=B2=E5=91=98=E6=8C=81=E6=9C=89,=20result=20through=20t?= =?UTF-8?q?()=20/=20tParts()=20(sentences=20with=20a=20number=20or=20a=20=20inside=20stay=20whole),=20English=20for=20the=2058=20?= =?UTF-8?q?new=20msgids;=20Chinese=20output=20unchanged=20(zh=20render=20p?= =?UTF-8?q?roof:=20only=20ownership's=20=E9=98=B6=20heading=20is=20a=20tex?= =?UTF-8?q?t=20node=20instead=20of=20a=20,=20same=20text=20and=20lay?= =?UTF-8?q?out)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/i18n/en.json | 60 +++++++- public/js/screens/bandDraft.js | 58 ++++---- public/js/screens/briefing.js | 19 +-- public/js/screens/game.js | 31 ++-- public/js/screens/game/overlays.js | 19 +-- public/js/screens/loadout.js | 219 +++++++++++++++-------------- public/js/screens/ownership.js | 20 +-- public/js/screens/result.js | 39 ++--- test/ui/loadout-stats.test.js | 4 +- test/ui/match-info.test.js | 2 +- 10 files changed, 269 insertions(+), 202 deletions(-) diff --git a/public/i18n/en.json b/public/i18n/en.json index 6fc41f91..35303830 100644 --- a/public/i18n/en.json +++ b/public/i18n/en.json @@ -952,5 +952,63 @@ "下一局生效": "Applies next match", "这份数据来自更新的版本(v{v}),请先更新游戏": "This data comes from a newer version (v{v}). Update the game first.", "这不是自选编队的数据": "This is not Custom Squad data", - "里面没有自选编队的数据": "It holds no Custom Squad data" + "里面没有自选编队的数据": "It holds no Custom Squad data", + "本局禁用{names}盟约,此策略效果可能无法发挥": "{names} disabled this match: this Strategy may not take full effect", + "【{name}】": "[{name}]", + "每位博士有 {n} 秒": "each Doctor has {n} {n|second|seconds}", + "{skip};{clock}": "{skip}; {clock}", + "{skip};{clock},超时将自动选择当前选中的「{autoName}」": "{skip}; {clock}. When time runs out, the highlighted \"{autoName}\" is picked for you", + "{skip};{clock},超时将自动选择「{autoName}」": "{skip}; {clock}. When time runs out, \"{autoName}\" is picked for you", + "已选择「{name}」": "Selected \"{name}\"", + "跳过": "Skip", + "第 {r} 回合 · 最终攻势": "Round {r} · Final Assault", + "道具无法出售。确定要销毁「{name}」吗?": "Items cannot be sold. Destroy \"{name}\"?", + "展开盟约": "Expand Alliances", + "收起盟约": "Collapse Alliances", + "正在查看 {name} 的阵地(只读)": "Viewing {name}'s field (read-only)", + "观战中 · 点击左侧成员头像切换查看": "Spectating · tap a member's avatar on the left to switch", + "未持有(干员持有):由替补干员上场": "Not owned (Operator Roster): its stand-in takes the field", + "未持有": "Not owned", + "{name} 调配": "{name} Loadout", + "干员持有中标记为未持有:此棋子由替补干员 {name} 上场,技能与模组固定(补位干员技能不可更改);这里的调配在改回「持有」后生效": "Marked as not owned in the Operator Roster: this chess takes the field as its stand-in {name}, with a fixed skill and module (a stand-in's skill cannot be changed); the loadout here applies once it is set back to \"Owned\"", + "干员持有中标记为未持有:此棋子由替补干员上场,技能与模组固定(补位干员技能不可更改);这里的调配在改回「持有」后生效": "Marked as not owned in the Operator Roster: this chess takes the field as its stand-in, with a fixed skill and module (a stand-in's skill cannot be changed); the loadout here applies once it is set back to \"Owned\"", + "全部持有": "Own All", + "将 {nNotOwned} 名未持有的干员恢复为持有(由本人上场)?": "Set {nNotOwned} not-owned {nNotOwned|Operator|Operators} back to owned (fielded as themselves)?", + "将 {nChanged} 名干员的技能与模组恢复为默认配置?": "Reset the skills and modules of {nChanged} {nChanged|Operator|Operators} to the default loadout?", + "文件过大,请选择「导出」下载的干员持有文件": "The file is too large. Choose an Operator Roster file downloaded with \"Export\".", + "已导入:{applied} 名干员未持有(另有 {dropped} 项无效,未导入)": "Imported: {applied} {applied|Operator|Operators} not owned ({dropped} invalid {dropped|entry|entries} skipped)", + "已导入:{applied} 名干员未持有": "Imported: {applied} {applied|Operator|Operators} not owned", + "已导入:全部持有": "Imported: all owned", + "已导入 {applied} 名干员(另有 {dropped} 项未导入)": "Imported {applied} {applied|Operator|Operators} ({dropped} {dropped|entry|entries} not imported)", + "已导入 {applied} 名干员的调配": "Imported the loadout of {applied} {applied|Operator|Operators}", + "干员持有是局外设置:本局按开局时的设置进行,修改将在下一局生效": "The Operator Roster is set outside matches: this match keeps the roster it started with; changes apply from the next match", + "局外设置,下一局生效 · 联机时只影响你自己的棋子 · 默认全部持有": "Set outside matches, applies from the next match · in co-op it only affects your own chess · everything is owned by default", + "标记未持有的干员:由官方指定的替补干员上场": "Mark the Operators you do not own: their official stand-ins take the field", + "干员持有": "Operator Roster", + "{nNotOwned} 名未持有": "{nNotOwned} not owned", + "导出干员持有(可复制或下载)": "Export the Operator Roster (copy or download)", + "导入干员持有(粘贴或选择文件)": "Import an Operator Roster (paste or choose a file)", + "导出干员持有": "Export Operator Roster", + "导入干员持有": "Import Operator Roster", + "共 {n} 名干员未持有。复制或下载这份数据,即可在别的设备或浏览器上导入。": "{n} {count|Operator is|Operators are} not owned. Copy or download this data to import it on another device or browser.", + "把导出的干员持有数据粘贴到下方,或点「选择文件」。导入会{overwrite}当前的干员持有设置。": "Paste exported Operator Roster data below, or click \"Choose File\". Importing will {overwrite} the current Operator Roster.", + "共 {n} 名干员已调整。复制或下载这份数据,即可在别的设备或浏览器上导入。": "{n} {count|Operator has|Operators have} a custom loadout. Copy or download this data to import it on another device or browser.", + "导入会{overwrite}当前的 {nChanged} 名干员调配。": "Importing will {overwrite} the current loadout of {nChanged} {nChanged|Operator|Operators}.", + "在此粘贴导出的干员持有内容…": "Paste the exported Operator Roster here…", + "调整干员携带的技能与模组 · 干员持有": "Change the skills and modules your Operators carry · Operator Roster", + "{n} 名干员已调整": "{n} {n|Operator|Operators} customized", + "{off} 名干员未持有,由替补干员上场": "{off} {off|Operator|Operators} not owned, fielded as {off|its|their} stand-in", + "{off} 名干员未持有(由替补干员上场)": "{off} {off|Operator|Operators} not owned (fielded as {off|its|their} stand-in)", + "{name}:持有": "{name}: owned", + "{name}:未持有,由 {standIn} 上场": "{name}: not owned, {standIn} takes the field", + "替补干员": "the stand-in", + "点击标记为未持有:由 {standIn} 上场": "Click to mark as not owned: {standIn} takes the field", + "点击恢复为持有": "Click to set back to owned", + "无替补数据": "No stand-in data", + "持有": "Owned", + "仅看未持有": "Not owned only", + "全部持有:没有由替补干员上场的棋子": "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}" } diff --git a/public/js/screens/bandDraft.js b/public/js/screens/bandDraft.js index 76a84f2f..0f0b3a03 100644 --- a/public/js/screens/bandDraft.js +++ b/public/js/screens/bandDraft.js @@ -29,18 +29,19 @@ import { useStore } from '../store.js'; import { data } from '../data.js'; import { audio } from '../audio.js'; import { modeOffBonds, bandOffBonds, bandOffLine } from '../ui/gameLogic.js'; +import { t, tParts, tName } from '../../../shared/i18n.js'; const cx = (...p) => p.flat().filter(Boolean).join(' '); /** 本局禁用 on a strategy card whose bonds `names` the mode switches off (bandOffBonds); nothing otherwise. */ export function BandOffTag({ names = [] }) { - return names.length ? html`本局禁用` : null; + return names.length ? html`${t('本局禁用')}` : null; } /** The detail pane's note for such a strategy: "本局禁用【拉特兰】盟约,此策略效果可能无法发挥" (the bond names struck through). */ export function BandOffNote({ names = [] }) { if (!names.length) return null; - return html`

<${Icon} name="info" />本局禁用${names.map((n, i) => html`【${n}】`)}盟约,此策略效果可能无法发挥

`; + return html`

<${Icon} name="info" />${tParts('本局禁用{names}盟约,此策略效果可能无法发挥', { names: names.map((n, i) => html`${tParts('【{name}】', { name: html`${n}` })}`) })}

`; } /** @@ -125,11 +126,12 @@ export function autoPickBand(sel, { bands, taken, myPick = null, defaultId = DEF * selected: the auto pick is the highlighted band (not the default standing in for a band a teammate holds) */ export function draftTip({ timed, turnSeconds = null, autoName = null, selected = true }) { - const skip = '联合模拟在选择策略时可以进行一次跳过'; - if (!timed) return `${skip};本局不限时`; - const clock = Number(turnSeconds) > 0 ? `每位博士有 ${Math.round(turnSeconds)} 秒` : '每位博士限时决策'; - if (!autoName) return `${skip};${clock}`; - return `${skip};${clock},超时将自动选择${selected ? '当前选中的' : ''}「${autoName}」`; + const skip = t('联合模拟在选择策略时可以进行一次跳过'); + if (!timed) return t('{skip};本局不限时', { skip }); + const clock = Number(turnSeconds) > 0 ? t('每位博士有 {n} 秒', { n: Math.round(turnSeconds) }) : t('每位博士限时决策'); + if (!autoName) return t('{skip};{clock}', { skip, clock }); + return selected ? t('{skip};{clock},超时将自动选择当前选中的「{autoName}」', { skip, clock, autoName }) + : t('{skip};{clock},超时将自动选择「{autoName}」', { skip, clock, autoName }); } /** @@ -154,11 +156,11 @@ export function draftClock(pub) { * @returns {{ text: string, secs: number|null, tone: 'mint'|'gold'|'warn'|'dim' }} */ export function draftInfoStatus({ myPick = null, pickName = null, myTurn, turnName = null, secs = null, waiting = false }) { - if (myPick) return { text: `已选择「${pickName || ''}」${waiting ? ',等待其他博士' : ''}`, secs: null, tone: 'mint' }; + if (myPick) return { text: `${t('已选择「{name}」', { name: pickName || '' })}${waiting ? t(',等待其他博士') : ''}`, secs: null, tone: 'mint' }; const s = Number.isFinite(secs) ? Math.max(0, Math.round(secs)) : null; const tone = s != null && s <= 10 ? 'warn' : 'gold'; - if (myTurn) return { text: '轮到你决策', secs: s, tone }; - return { text: turnName ? `${turnName} 决策中` : '等待轮到你', secs: s, tone: s != null ? tone : 'dim' }; + if (myTurn) return { text: t('轮到你决策'), secs: s, tone }; + return { text: turnName ? t('{turnName} 决策中', { turnName }) : t('等待轮到你'), secs: s, tone: s != null ? tone : 'dim' }; } /** BAND_DRAFT screen. */ @@ -221,7 +223,7 @@ export function BandDraftScreen() { // what a timeout gives me: the highlighted band while free, else the default (never 队友已选 — match/phases.js timeoutBand) const autoId = autoPickBand(sel, { bands, taken, myPick, defaultId }); - const autoName = (autoId && gd.band(autoId)?.name) || gd.band(defaultId)?.name || '华法琳'; + const autoName = (autoId && gd.band(autoId)?.name) || gd.band(defaultId)?.name || tName('华法琳'); const band = sel ? gd.band(sel) : null; const selTaken = !!band && taken.has(band.bandId); @@ -249,11 +251,11 @@ export function BandDraftScreen() { return html`
- <${StepHeader} step=${2} of=${2} title="选择策略" micro="STRATEGY // BAND CHECK" pub=${clock ? { ...pub, deadline: clock.deadline } : { ...pub, deadline: 0 }} + <${StepHeader} step=${2} of=${2} title=${t('选择策略')} micro="STRATEGY // BAND CHECK" pub=${clock ? { ...pub, deadline: clock.deadline } : { ...pub, deadline: 0 }} total=${clock ? clock.total : null} onExit=${() => setExit(true)} />
`; })} <${Button} variant="secondary" icon="search" block=${true} class="draft-order__info" data-testid="match-info-open" - aria-haspopup="dialog" onClick=${() => setInfoOpen(true)}>查看禁用盟约与干员 + aria-haspopup="dialog" onClick=${() => setInfoOpen(true)}>${t('查看禁用盟约与干员')} ${!solo ? html`

${draftTip({ timed, turnSeconds: turnLen, autoName: myPick ? null : autoName, selected: autoId === sel })}

` : null} -
+
${bands.map((b) => { const who = pickers.get(b.bandId) || []; const isTaken = taken.has(b.bandId); const offNames = bandOffBonds(b, offBonds).map((id) => gd.bond(id)?.name || id); // 本局禁用 (still selectable) return html``; })}
@@ -302,22 +304,22 @@ export function BandDraftScreen() {
<${BandIcon} bandId=${band.bandId} size="xl" />
-
初始生命值<${LpTower} value=${band.totalHp} size="lg" />
+
${t('初始生命值')}<${LpTower} value=${band.totalHp} size="lg" />

${band.name}

<${BandOffNote} names=${bandOffBonds(band, offBonds).map((id) => gd.bond(id)?.name || id)} />
<${MicroLabel} tone="mint">EFFECT ${band.effectName || ''} <${RichText} as="p" text=${band.descRaw || band.desc} class="draft-detail__desc" /> -
` : html`

选择一个策略查看详情

`} + ` : html`

${t('选择一个策略查看详情')}

`}
- ${myPick ? html`

<${Icon} name="check" />已选择「${gd.band(myPick)?.name || ''}」${!solo && !draft.done ? ',等待其他博士' : ''}

` - : selTaken ? html`

<${Icon} name="close" />队友已选,请选择其他策略

` - : !myTurn ? html`

<${Icon} name="hourglass" />${turnName ? `${turnName} 正在决策…` : '等待轮到你'}

` : null} + ${myPick ? html`

<${Icon} name="check" />${t('已选择「{name}」', { name: gd.band(myPick)?.name || '' })}${!solo && !draft.done ? t(',等待其他博士') : ''}

` + : selTaken ? html`

<${Icon} name="close" />${t('队友已选,请选择其他策略')}

` + : !myTurn ? html`

<${Icon} name="hourglass" />${turnName ? t('{turnName} 正在决策…', { turnName }) : t('等待轮到你')}

` : null}
${!solo ? html`<${Button} variant="secondary" size="lg" icon="chevrons" disabled=${!canSkip} loading=${busy === 'skip'} onClick=${skip} - title=${skipsLeft > 0 ? '跳过本轮,稍后再选' : '跳过次数已用完'}>跳过${skipsLeft > 0 ? '' : '(已用)'}` : null} - <${Button} variant="primary" size="lg" icon="check" disabled=${!myTurn || !band || selTaken} loading=${busy === 'pick'} onClick=${confirm}>${selTaken ? '队友已选' : '确认选择'} + title=${skipsLeft > 0 ? t('跳过本轮,稍后再选') : t('跳过次数已用完')}>${t('跳过')}${skipsLeft > 0 ? '' : t('(已用)')}` : null} + <${Button} variant="primary" size="lg" icon="check" disabled=${!myTurn || !band || selTaken} loading=${busy === 'pick'} onClick=${confirm}>${selTaken ? t('队友已选') : t('确认选择')}
diff --git a/public/js/screens/briefing.js b/public/js/screens/briefing.js index 0e6ff775..582bac3e 100644 --- a/public/js/screens/briefing.js +++ b/public/js/screens/briefing.js @@ -16,6 +16,7 @@ import { factionTypes, sortedPlayers, phaseTotalSeconds } from '../ui/gameLogic. import { enemyIconUrl, factionIconUrl } from '../ui/assetUrls.js'; import { useStore } from '../store.js'; import { data } from '../data.js'; +import { t, tParts } from '../../../shared/i18n.js'; const cx = (...p) => p.flat().filter(Boolean).join(' '); @@ -41,7 +42,7 @@ export function BriefingScreen() { const stage = pub.stageId ? gd.stage(pub.stageId) : null; // the mode's battlefield pool (config.json modes[].stages): 标准 is always 战场#01, 险境+ draw one at random const poolN = Array.isArray(mode?.stages) ? mode.stages.length : 0; - const pool = poolN > 1 ? `战场随机(共${poolN}张)` : poolN === 1 ? '战场固定' : ''; + const pool = poolN > 1 ? t('战场随机(共{poolN}张)', { poolN }) : poolN === 1 ? t('战场固定') : ''; const types = factionTypes(pub.factions); const factions = gd.factions?.types || {}; const m = gd.m; @@ -55,20 +56,20 @@ export function BriefingScreen() { return html`
- <${StepHeader} step=${1} of=${2} title="确认本局信息" micro="BRIEFING // INFO CHECK" pub=${pub} + <${StepHeader} step=${1} of=${2} title=${t('确认本局信息')} micro="BRIEFING // INFO CHECK" pub=${pub} total=${phaseTotalSeconds(pub, gd.config)} onExit=${() => setExit(true)} />
- <${MicroLabel} tone="gold">ENEMY LEADER // 敌方领袖 + <${MicroLabel} tone="gold">${t('ENEMY LEADER // 敌方领袖')}
${bossEnemy ? html`<${Img} src=${enemyIconUrl(m, boss.enemyKey)} class="brief-boss__sil" fallback=${html`?`} />` : html`?`}
-

${boss?.name || bossEnemy?.name || '未知领袖'}

- 第 ${mode?.bossRound ?? pub.lastRound ?? 14} 回合 · 最终攻势 +

${boss?.name || bossEnemy?.name || t('未知领袖')}

+ ${tParts('第 {r} 回合 · 最终攻势', { r: html`${mode?.bossRound ?? pub.lastRound ?? 14}` })} ${Array.isArray(boss?.abilities) && boss.abilities.length ? html`
    ${boss.abilities.slice(0, 3).map((a, i) => html`
  • <${RichText} text=${a} />
  • `)}
` : null} @@ -79,11 +80,11 @@ export function BriefingScreen() { <${Icon} name="rook" />${stage?.name || pub.stageId || '—'}
-

特训敌人<${MicroLabel}>SPECIAL ENEMIES

+

${t('特训敌人')}<${MicroLabel}>SPECIAL ENEMIES

${types.length ? types.map((t) => html`
<${Img} src=${factionIconUrl(m, factions[t]?.icon)} /> ${factions[t]?.name || t}${factions[t]?.desc || ''} -
`) : html`

本局没有特训敌人

`} +
`) : html`

${t('本局没有特训敌人')}

`}
@@ -93,11 +94,11 @@ export function BriefingScreen() {
<${LoadoutButton} from="briefing" size="lg" class="brief-loadout" />
- 已就绪 ${readyN}/${players.length} + ${t('已就绪')} ${readyN}/${players.length} ${players.map((p) => html`<${Icon} name="user" />`)}
<${Button} variant="primary" size="xl" icon=${me?.ready ? 'check' : me ? 'play' : 'eye'} active=${!!me?.ready} loading=${busy} - disabled=${!!me?.ready || !me} onClick=${ready}>${me?.ready ? '已就绪' : me ? '准备就绪' : '观战中'} + disabled=${!!me?.ready || !me} onClick=${ready}>${me?.ready ? t('已就绪') : me ? t('准备就绪') : t('观战中')}
<${ExitModal} open=${exit} onClose=${() => setExit(false)} solo=${solo} />
`; diff --git a/public/js/screens/game.js b/public/js/screens/game.js index d5e62705..ab58efca 100644 --- a/public/js/screens/game.js +++ b/public/js/screens/game.js @@ -115,6 +115,7 @@ import { useDocClass, FullscreenButton } from '../ui/device.js'; import { HUD_HZ_MS, MERGE_HL, SEL_RANGE, cx } from './game/marks.js'; import { keepEarly } from './game/early.js'; import { MatchEnded, PausedOverlay } from './game/overlays.js'; +import { t, tParts } from '../../../shared/i18n.js'; /** Router for the in-match screens. */ export function GameScreen() { @@ -127,7 +128,7 @@ export function GameScreen() { if (!pub || !gd.ready) { return html`
<${Spinner} size="lg" label=${pub ? 'LOADING DATA' : 'ENTERING SIMULATION'} /> -

${pub ? '正在载入模拟数据…' : '正在进入模拟…'}

+

${pub ? t('正在载入模拟数据…') : t('正在进入模拟…')}

`; } const mode = phaseMode(pub.phase); @@ -562,7 +563,7 @@ function MatchScreen() { let asked = null; try { asked = new URLSearchParams(globalThis.location?.search || '').get('render'); } catch { asked = null; } if (asked === 'fallback' || globalThis.__SP_RENDER__ === 'fallback') return; - toast('当前设备无法启用 3D / WebGL 渲染,已切换为简化视图(功能不受影响)', 'info', { ttl: 5000 }); + toast(t('当前设备无法启用 3D / WebGL 渲染,已切换为简化视图(功能不受影响)'), 'info', { ttl: 5000 }); }, [viewKind]); // phase changes: banners, sounds, resets @@ -678,9 +679,9 @@ function MatchScreen() { const closeIt = () => { setDetail((d) => (d?.kind === 'piece' && d.uid === piece.uid ? null : d)); setSel((x) => (x && x.uid === piece.uid ? null : x)); }; if (piece.kind === 'item') { // an equipped item is locked (the server refuses g.destroy): replacing it is the equip-replace dialog's job - if (!itemDestroyable(live.current.placeCtx, piece.uid)) { toast('已配发的装备无法销毁', 'warn'); audio.sfx('error', { volume: 0.5 }); return false; } + if (!itemDestroyable(live.current.placeCtx, piece.uid)) { toast(t('已配发的装备无法销毁'), 'warn'); audio.sfx('error', { volume: 0.5 }); return false; } const it = data.lookup('items', piece.id); - const ok = await confirmDialog({ title: '销毁道具', text: `道具无法出售。确定要销毁「${it?.name || '道具'}」吗?`, okText: '销毁', danger: true }); + const ok = await confirmDialog({ title: t('销毁道具'), text: t('道具无法出售。确定要销毁「{name}」吗?', { name: it?.name || t('道具') }), okText: t('销毁'), danger: true }); if (ok && await actions.destroy(piece.uid)) { closeIt(); return true; } return false; } @@ -1021,7 +1022,7 @@ function MatchScreen() { const L = live.current; const uid = L.sel?.uid; const to = uid != null ? retreatSlot(L.placeCtx, uid) : null; - if (!to) { toast('整备区已满', 'warn'); audio.sfx('error', { volume: 0.5 }); return; } + if (!to) { toast(t('整备区已满'), 'warn'); audio.sfx('error', { volume: 0.5 }); return; } setSelBusy(true); if (await actions.move(uid, to)) { setSel(null); @@ -1174,9 +1175,9 @@ function MatchScreen() { const stripFid = strip.fieldId; const liveLayers = (combat || settleMode) && battleState?.bondLayers ? battleState.bondLayers : null; // the observing pill names the player whose bonds the strip shows (the same teammate as the strip's "👁 name" tag) - const observingName = cc && combat && watchedFid ? (!strip.self && stripFid === watchedFid ? strip.name : (players.find((p) => p.fieldId === watchedFid || ownFieldId(p.playerId) === watchedFid)?.name || '队友')) : null; + const observingName = cc && combat && watchedFid ? (!strip.self && stripFid === watchedFid ? strip.name : (players.find((p) => p.fieldId === watchedFid || ownFieldId(p.playerId) === watchedFid)?.name || t('队友'))) : null; const watchedP = watchingOther ? (players.find((p) => p.playerId !== myId && (watching === ownFieldId(p.playerId) || (watching === p.fieldId && String(watching).startsWith('n:')))) || null) : null; - const watchedName = watchingOther ? (watchedP?.name || players.find((p) => watching === p.fieldId)?.name || '队友') : null; + const watchedName = watchingOther ? (watchedP?.name || players.find((p) => watching === p.fieldId)?.name || t('队友')) : null; const stripBonds = strip.bonds; // a popup opened from the strip closes when the strip changes hands (another teammate scouted / a ‹ › half / back to // the own bonds); one opened from a card's chip keeps its unit owner (it carries its own player either way) @@ -1269,7 +1270,7 @@ function MatchScreen() {
+ }}><${Icon} name=${bondsCollapsed ? 'chevronRight' : 'chevronLeft'} />${bondsCollapsed ? t('盟约') : t('收起')}