feedback5 W5S: i18n phase 2, shared and server texts — shared/constants.js msgid tables (difficulties, phases, error codes, emote sets and labels) marked N_(); what is not player text is marked and skipped by the audit: error details and logger lines (tools/i18n.mjs reads detail: values, log.* calls and i18n-ignore-file), the developer invariant reports, the bilingual HTTP error pages, the AI seats' player names, the dev STUB match, data keys / fallbacks; the 虫动 emote set follows the official EN name (Slug Style); node tools/i18n.mjs extract now reports 0

This commit is contained in:
sganggs committed 2026-10-06 18:21:43 +08:00
1 parent 7e262dc809
commit 7697b4ff40
19 files changed
+102 -70

No files matched your search

+1
View File
@@ -1,4 +1,5 @@
// server/http/common.js — what every HTTP answer shares:
// (i18n-ignore-file: the error pages are bilingual by design, 中文 · English — docs/I18N.md)
//
// * the security headers, set on every response before any route runs (routes.js);
// * splitUrl (raw path + query, also from absolute-form URLs), the bilingual error page (sendError) and JSON replies
+1
View File
@@ -1,4 +1,5 @@
// server/http/media.js — the extension-less audio route /media/… → public/assets/audio (see serveMedia).
// (i18n-ignore-file: the error pages are bilingual by design, 中文 · English — docs/I18N.md)
import fsp from 'node:fs/promises';
import path from 'node:path';
+1
View File
@@ -1,4 +1,5 @@
// server/http/routes.js — the node:http request listener. Every response gets the security headers (common.js), then:
// (i18n-ignore-file: the error pages are bilingual by design, 中文 · English — docs/I18N.md)
//
// * a URL longer than 4096 characters → 414; one that does not parse → 400;
// * any method but GET / HEAD → 405 with `Allow: GET, HEAD`;
+1
View File
@@ -1,4 +1,5 @@
// server/http/static.js — the static file server (routes.js hands it every GET / HEAD that is not an API route):
// (i18n-ignore-file: the error pages are bilingual by design, 中文 · English — docs/I18N.md)
//
// / → public/ (index.html for directories)
// /data/ → data/ (generated game data)
+1 -1
View File
@@ -119,7 +119,7 @@ export const LOBBY_DEFAULTS = Object.freeze({
export const SOLO_RECONNECT_FALLBACK_SEC = 86_400;
/** Display names for AI teammates (the tutorial NPCs first, then a few familiar faces). */
export const BOT_NAMES = Object.freeze(['AI·华法琳', 'AI·阿米娅', 'AI·惊蛰', 'AI·杜宾', 'AI·凯尔希', 'AI·可露希尔']);
export const BOT_NAMES = Object.freeze(['AI·华法琳', 'AI·阿米娅', 'AI·惊蛰', 'AI·杜宾', 'AI·凯尔希', 'AI·可露希尔']); // i18n-ignore: player names (docs/I18N.md)
const OK = Object.freeze({ ok: true });
const fail = (code, detail) => (detail ? { error: code, detail } : { error: code });
+1 -1
View File
@@ -225,7 +225,7 @@ export class StubMatch {
})),
fields: [],
stub: true,
message: '对局核心尚未实现(平台占位 STUB):全员确认本局信息或倒计时结束后将直接结算。',
message: '对局核心尚未实现(平台占位 STUB):全员确认本局信息或倒计时结束后将直接结算。', // i18n-ignore: development placeholder
};
}
+1
View File
@@ -1,4 +1,5 @@
// server/match/audit.js — rule auditor for sweeps and tests (tools/matchrun.mjs --check, test/match/fullmatch.test.js).
// (i18n-ignore-file: developer reports in English with the game's terms, never shown to players — docs/I18N.md)
//
// attachAudit(m) wraps a live Match's phase transitions and a few prep handlers (instance-level wrappers; the engine
// is untouched) and records every rule violation it observes, next to the structural invariants of invariants.js:
+1 -1
View File
@@ -91,7 +91,7 @@ const ITEM_HANDLERS = {
const n = int(paramsOf(ctx, ev.item).count, 1);
const rec = ctx.gd.item(ev.item.id);
ctx.addEffect({
id: `doll:${ev.item.uid}`, key: 'effect:builtin_round_coin', name: rec ? rec.name : '精打细算玩偶', desc: rec ? rec.desc : '',
id: `doll:${ev.item.uid}`, key: 'effect:builtin_round_coin', name: rec ? rec.name : '精打细算玩偶', desc: rec ? rec.desc : '', // i18n-ignore: the item's data name
iconKind: 'item', iconId: rec ? rec.iconId || rec.id : ev.item.id, battle: false, params: { count: n },
});
},
+3 -3
View File
@@ -63,7 +63,7 @@
import { weightedPick } from './waves.js';
export const FAMILY_NAMES = { bounty: '悬赏决策', supply: '道具补给', shop: '机密商店', tactic: '战术决策' };
export const FAMILY_NAMES = { bounty: '悬赏决策', supply: '道具补给', shop: '机密商店', tactic: '战术决策' }; // i18n-ignore: = choices.json families (the client shows the localized record)
/**
* Battles a multi-round bounty card lasts (data `rounds` 99, official text "之后 / 后续的<@ba.vdown>每场</>作战":
@@ -82,7 +82,7 @@ export function bountyBattles(c) {
const r = Number(c && c.rounds);
return Math.max(1, Math.min(99, Number.isInteger(r) ? r : 1));
}
const N_ZH = ['', '一', '两', '三', '四', '五'];
const N_ZH = ['', '一', '两', '三', '四', '五']; // i18n-ignore: rewrites the official Chinese bounty text
/**
* A bounty text as the card lasts: a multi-round card's "之后的 / 后续每场作战" (rich `<@ba.vdown>每场</>` or plain) reads
* "接下来<@ba.vup>两场作战</>" like the official two-battle cards while MULTI_ROUND_BOUNTY_BATTLES is set; any other
@@ -91,7 +91,7 @@ const N_ZH = ['', '一', '两', '三', '四', '五'];
export function bountyText(text, c) {
if (typeof text !== 'string' || !text || !isMultiRoundBounty(c) || !Number.isInteger(MULTI_ROUND_BOUNTY_BATTLES)) return text;
const n = MULTI_ROUND_BOUNTY_BATTLES;
const battles = `${N_ZH[n] || n}场作战`;
const battles = `${N_ZH[n] || n}场作战`; // i18n-ignore
return text
.replace(/(?:之后的|后续的?)<@ba\.vdown>每场<\/>作战/g, `接下来<@ba.vup>${battles}</>`)
.replace(/(?:之后的|后续的?)每场作战/g, `接下来${battles}`);
+1
View File
@@ -1,4 +1,5 @@
// server/match/invariants.js — engine invariants (DESIGN §11) as a non-throwing check. Used by the test harness
// (i18n-ignore-file: developer reports in English with the game's terms, never shown to players — docs/I18N.md)
// (test/match/harness.js checkInvariants asserts the list is empty) and by tools/matchrun.mjs --check sweeps.
//
// collectViolations(m) → string[] (empty when every invariant holds):
+1 -1
View File
@@ -166,7 +166,7 @@ export class MatchPhases {
if (this.draft.picks[ps.playerId]) return fail(ERR.ALREADY);
if (this.draftTurn() !== ps.playerId) return fail(ERR.NOT_YOUR_TURN);
if (typeof bandId !== 'string' || !this.gd.bandAllowed(bandId)) return fail(ERR.BAD_TARGET);
if (this.bandTaken(bandId, ps.playerId)) return fail(ERR.BAD_TARGET, '队友已选');
if (this.bandTaken(bandId, ps.playerId)) return fail(ERR.BAD_TARGET, '队友已选'); // i18n-ignore: developer detail (players see ERR_TEXT)
this._applyBand(ps, bandId);
return OK;
}
+1 -1
View File
@@ -112,7 +112,7 @@ export class MatchSpDraft {
const rounds = bountyBattles(card);
const b = {
id: `bounty:${this.nextUid()}`,
card: { effectId: card.effectId ?? card.id ?? null, name: card.name ?? '悬赏', desc: card.desc ?? '', tier: card.tier ?? 1, coin: Math.max(0, Math.trunc(Number(card.coin) || 0)), payout: card.payout === 'perfect' ? 'perfect' : 'kill', rounds, multiRound: isMultiRoundBounty(card), enemyKey: card.enemyKey, count: Math.max(1, Math.min(20, Number.isInteger(card.count) ? card.count : 1)) },
card: { effectId: card.effectId ?? card.id ?? null, name: card.name ?? '悬赏', desc: card.desc ?? '', tier: card.tier ?? 1, coin: Math.max(0, Math.trunc(Number(card.coin) || 0)), payout: card.payout === 'perfect' ? 'perfect' : 'kill', rounds, multiRound: isMultiRoundBounty(card), enemyKey: card.enemyKey, count: Math.max(1, Math.min(20, Number.isInteger(card.count) ? card.count : 1)) }, // i18n-ignore: a data-less card's fallback name
roundsLeft: rounds,
};
ps.bounties.push(b);
+2 -2
View File
@@ -42,8 +42,8 @@ export class PlayerViews {
const left = b.roundsLeft >= 90 ? null : b.roundsLeft;
const eff = b.card.effectId ? this.gd.effect(b.card.effectId) : null;
out.push({
id: b.id, name: b.card.name || '悬赏', desc: bountyText((eff && eff.descRaw) || b.card.desc || '', b.card), iconKind: 'choice', iconId: b.card.effectId || 'bounty',
counter: left, counterText: left == null ? '之后的每场作战' : `还剩 ${left} 场作战`,
id: b.id, name: b.card.name || '悬赏', desc: bountyText((eff && eff.descRaw) || b.card.desc || '', b.card), iconKind: 'choice', iconId: b.card.effectId || 'bounty', // i18n-ignore: a data-less card's fallback name
counter: left, counterText: left == null ? '之后的每场作战' : `还剩 ${left} 场作战`, // i18n-ignore: older clients (ui/effectsList.js builds it from counter)
});
}
return out;