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:
1 parent
7e262dc809
commit
7697b4ff40
19 files changed
+102
-70
No files matched your search
+1
-1
@@ -745,7 +745,7 @@
|
||||
"谢谢": "Thanks",
|
||||
"思考": "Thinking",
|
||||
"合作愉快": "Nice teamwork",
|
||||
"表情套组:虫动": "Emote Set: Slug Moves",
|
||||
"表情套组:虫动": "Emote Set: Slug Style",
|
||||
"合作愉快!": "Nice teamwork!",
|
||||
"谢谢!": "Thanks!",
|
||||
"对不起!": "Sorry!",
|
||||
|
||||
@@ -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,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,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,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
@@ -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 });
|
||||
|
||||
@@ -225,7 +225,7 @@ export class StubMatch {
|
||||
})),
|
||||
fields: [],
|
||||
stub: true,
|
||||
message: '对局核心尚未实现(平台占位 STUB):全员确认本局信息或倒计时结束后将直接结算。',
|
||||
message: '对局核心尚未实现(平台占位 STUB):全员确认本局信息或倒计时结束后将直接结算。', // i18n-ignore: development placeholder
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 },
|
||||
});
|
||||
},
|
||||
|
||||
@@ -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,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):
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
+53
-51
@@ -1,5 +1,7 @@
|
||||
// Shared enums & constants (server + browser). Pure ESM, no Node APIs.
|
||||
|
||||
import { N_ } from './i18n.js';
|
||||
|
||||
export const PROTOCOL_VERSION = 1;
|
||||
/** Release version shown to players (title screen, server banner, /healthz). Kept equal to package.json "version"
|
||||
* (test/version.test.js); PROTOCOL_VERSION above is the separate wire-format number. */
|
||||
@@ -17,7 +19,7 @@ export const ROOM_CODE_LEN = 4;
|
||||
export const NAME_MAX_LEN = 12;
|
||||
|
||||
export const DIFFICULTIES = ['FUNNY', 'NORMAL', 'HARD', 'ABYSS'];
|
||||
export const DIFFICULTY_NAMES = { FUNNY: '标准模拟', NORMAL: '险境模拟', HARD: '绝境模拟', ABYSS: '终极模拟' };
|
||||
export const DIFFICULTY_NAMES = { FUNNY: N_('标准模拟'), NORMAL: N_('险境模拟'), HARD: N_('绝境模拟'), ABYSS: N_('终极模拟') };
|
||||
export const DIFFICULTY_COLORS = { FUNNY: '#f6a329', NORMAL: '#e85a1a', HARD: '#e73118', ABYSS: '#ff0024' };
|
||||
|
||||
// modeId in data/config.json = `mode_${type}_${difficulty.toLowerCase()}` with type single|multi
|
||||
@@ -41,9 +43,9 @@ export const PHASE = Object.freeze({
|
||||
});
|
||||
|
||||
export const PHASE_NAMES = {
|
||||
LOBBY: '等待中', INFO_CHECK: '确认本局信息', BAND_DRAFT: '选择策略', BATTLE_CHECK: '协议启动',
|
||||
ROUND_START: '回合开始', SP_DRAFT: '机变阶段', PREP: '休整期', COMBAT: '作战中', UNITE: '联防阶段',
|
||||
SETTLE: '结算', FINAL_ASSAULT: '最终攻势', HIDDEN_CORE: '隐秘核心', RESULT: '模拟结束',
|
||||
LOBBY: N_('等待中'), INFO_CHECK: N_('确认本局信息'), BAND_DRAFT: N_('选择策略'), BATTLE_CHECK: N_('协议启动'),
|
||||
ROUND_START: N_('回合开始'), SP_DRAFT: N_('机变阶段'), PREP: N_('休整期'), COMBAT: N_('作战中'), UNITE: N_('联防阶段'),
|
||||
SETTLE: N_('结算'), FINAL_ASSAULT: N_('最终攻势'), HIDDEN_CORE: N_('隐秘核心'), RESULT: N_('模拟结束'),
|
||||
};
|
||||
|
||||
// Board geometry on the 19x21 stage grid (row 0 = bottom). See DESIGN §3.
|
||||
@@ -145,12 +147,12 @@ export const ERR = Object.freeze({
|
||||
});
|
||||
|
||||
export const ERR_TEXT = {
|
||||
BAD_MSG: '无效的请求', RATE: '操作过于频繁', NOT_IN_ROOM: '你不在房间中', ROOM_NOT_FOUND: '未找到该同盟密钥对应的房间',
|
||||
ROOM_FULL: '房间已满', ROOM_STARTED: '模拟已开始', NOT_HOST: '只有房主可以操作', NOT_READY: '仍有玩家未就绪',
|
||||
WRONG_PHASE: '当前阶段无法进行该操作', NO_FUNDS: '资金不足', HAND_FULL: '整备区已满', BOARD_FULL: '已达到部署上限',
|
||||
BAD_TILE: '无法部署在该位置', BAD_TARGET: '无效的目标', SOLD_OUT: '已售出', MAX_LEVEL: '调度中心已达最高等级',
|
||||
NOT_YOUR_TURN: '尚未轮到你', ALREADY: '已完成该操作', TEMP_NOT_EMPTY: '临时整备区不为空', ELIMINATED: '你已被淘汰',
|
||||
SPECTATOR: '观战中无法进行该操作', INTERNAL: '服务器内部错误',
|
||||
BAD_MSG: N_('无效的请求'), RATE: N_('操作过于频繁'), NOT_IN_ROOM: N_('你不在房间中'), ROOM_NOT_FOUND: N_('未找到该同盟密钥对应的房间'),
|
||||
ROOM_FULL: N_('房间已满'), ROOM_STARTED: N_('模拟已开始'), NOT_HOST: N_('只有房主可以操作'), NOT_READY: N_('仍有玩家未就绪'),
|
||||
WRONG_PHASE: N_('当前阶段无法进行该操作'), NO_FUNDS: N_('资金不足'), HAND_FULL: N_('整备区已满'), BOARD_FULL: N_('已达到部署上限'),
|
||||
BAD_TILE: N_('无法部署在该位置'), BAD_TARGET: N_('无效的目标'), SOLD_OUT: N_('已售出'), MAX_LEVEL: N_('调度中心已达最高等级'),
|
||||
NOT_YOUR_TURN: N_('尚未轮到你'), ALREADY: N_('已完成该操作'), TEMP_NOT_EMPTY: N_('临时整备区不为空'), ELIMINATED: N_('你已被淘汰'),
|
||||
SPECTATOR: N_('观战中无法进行该操作'), INTERNAL: N_('服务器内部错误'),
|
||||
};
|
||||
|
||||
// ---- Emotes (交流, research 09 §4) -----------------------------------------------------------------------------
|
||||
@@ -166,53 +168,53 @@ export const ERR_TEXT = {
|
||||
// derived from the id (autochess_battle_fooldoctor_03…06 → pic_fooldoctor_04/05/06/08_battle).
|
||||
const emo = (id, sortId, picId, label) => Object.freeze({ id, sortId, picId, label });
|
||||
export const EMOTE_THEMES = Object.freeze([
|
||||
{ themeId: 'emoticon_autochess_basic', dir: 'basic', sortId: 100000, isBasic: true, name: '表情套组:卫戍协议', emotes: [
|
||||
emo('autochess_battle_happy', 1001, 'pic_happy_battle', '开心'),
|
||||
emo('autochess_battle_scared', 1002, 'pic_scared_battle', '害怕'),
|
||||
emo('autochess_battle_sorry', 1003, 'pic_sorry_battle', '对不起'),
|
||||
emo('autochess_battle_thanks', 1004, 'pic_thanks_battle', '谢谢'),
|
||||
emo('autochess_battle_thinking', 1005, 'pic_thinking_battle', '思考'),
|
||||
emo('autochess_battle_nice_cooperate', 1006, 'pic_cooperate_battle', '合作愉快'),
|
||||
{ themeId: 'emoticon_autochess_basic', dir: 'basic', sortId: 100000, isBasic: true, name: N_('表情套组:卫戍协议'), emotes: [
|
||||
emo('autochess_battle_happy', 1001, 'pic_happy_battle', N_('开心')),
|
||||
emo('autochess_battle_scared', 1002, 'pic_scared_battle', N_('害怕')),
|
||||
emo('autochess_battle_sorry', 1003, 'pic_sorry_battle', N_('对不起')),
|
||||
emo('autochess_battle_thanks', 1004, 'pic_thanks_battle', N_('谢谢')),
|
||||
emo('autochess_battle_thinking', 1005, 'pic_thinking_battle', N_('思考')),
|
||||
emo('autochess_battle_nice_cooperate', 1006, 'pic_cooperate_battle', N_('合作愉快')),
|
||||
] },
|
||||
{ themeId: 'emoticon_originium_slug', dir: 'slug', sortId: 1001, isBasic: false, name: '表情套组:虫动', emotes: [
|
||||
emo('slug_autochess_battle_nice_work', 2001, 'pic_nice_work_battle', '合作愉快!'),
|
||||
emo('slug_autochess_battle_thanks', 2002, 'pic_thanks_battle', '谢谢!'),
|
||||
emo('slug_autochess_battle_sorry', 2003, 'pic_sorry_battle', '对不起!'),
|
||||
emo('slug_autochess_battle_bye', 2004, 'pic_bye_battle', '再见!'),
|
||||
{ themeId: 'emoticon_originium_slug', dir: 'slug', sortId: 1001, isBasic: false, name: N_('表情套组:虫动'), emotes: [
|
||||
emo('slug_autochess_battle_nice_work', 2001, 'pic_nice_work_battle', N_('合作愉快!')),
|
||||
emo('slug_autochess_battle_thanks', 2002, 'pic_thanks_battle', N_('谢谢!')),
|
||||
emo('slug_autochess_battle_sorry', 2003, 'pic_sorry_battle', N_('对不起!')),
|
||||
emo('slug_autochess_battle_bye', 2004, 'pic_bye_battle', N_('再见!')),
|
||||
emo('slug_autochess_battle_distrust', 2005, 'pic_distrust_battle', '???'),
|
||||
emo('slug_autochess_battle_very_soon', 2006, 'pic_very_soon_battle', '很快就好!'),
|
||||
emo('slug_autochess_battle_very_soon', 2006, 'pic_very_soon_battle', N_('很快就好!')),
|
||||
] },
|
||||
{ themeId: 'emoticon_autochess_basic_2', dir: 'basic_2', sortId: 100001, isBasic: true, name: '表情套组:卫戍协议', emotes: [
|
||||
emo('autochess_battle_noproblem', 1007, 'pic_noproblem_battle', '没问题!'),
|
||||
emo('autochess_battle_respect', 1008, 'pic_respect_battle', '敬礼!'),
|
||||
emo('autochess_battle_call', 1009, 'pic_call_battle', '欢呼!'),
|
||||
emo('autochess_battle_playingcool', 1010, 'pic_playingcool_battle', '酷!'),
|
||||
emo('autochess_battle_sad', 1011, 'pic_sad_battle', '伤心'),
|
||||
emo('autochess_battle_dying', 1012, 'pic_dying_battle', '快死了'),
|
||||
{ themeId: 'emoticon_autochess_basic_2', dir: 'basic_2', sortId: 100001, isBasic: true, name: N_('表情套组:卫戍协议'), emotes: [
|
||||
emo('autochess_battle_noproblem', 1007, 'pic_noproblem_battle', N_('没问题!')),
|
||||
emo('autochess_battle_respect', 1008, 'pic_respect_battle', N_('敬礼!')),
|
||||
emo('autochess_battle_call', 1009, 'pic_call_battle', N_('欢呼!')),
|
||||
emo('autochess_battle_playingcool', 1010, 'pic_playingcool_battle', N_('酷!')),
|
||||
emo('autochess_battle_sad', 1011, 'pic_sad_battle', N_('伤心')),
|
||||
emo('autochess_battle_dying', 1012, 'pic_dying_battle', N_('快死了')),
|
||||
] },
|
||||
{ themeId: 'emoticon_foolsday_doctor', dir: 'fooldoctor', sortId: 1002, isBasic: false, name: '表情套组:博士士', emotes: [
|
||||
emo('autochess_battle_fooldoctor_01', 1020, 'pic_fooldoctor_01_battle', '博士士 1'),
|
||||
emo('autochess_battle_fooldoctor_02', 1021, 'pic_fooldoctor_02_battle', '博士士 2'),
|
||||
emo('autochess_battle_fooldoctor_03', 1022, 'pic_fooldoctor_04_battle', '博士士 3'),
|
||||
emo('autochess_battle_fooldoctor_04', 1023, 'pic_fooldoctor_05_battle', '博士士 4'),
|
||||
emo('autochess_battle_fooldoctor_05', 1024, 'pic_fooldoctor_06_battle', '博士士 5'),
|
||||
emo('autochess_battle_fooldoctor_06', 1025, 'pic_fooldoctor_08_battle', '博士士 6'),
|
||||
{ themeId: 'emoticon_foolsday_doctor', dir: 'fooldoctor', sortId: 1002, isBasic: false, name: N_('表情套组:博士士'), emotes: [
|
||||
emo('autochess_battle_fooldoctor_01', 1020, 'pic_fooldoctor_01_battle', N_('博士士 1')),
|
||||
emo('autochess_battle_fooldoctor_02', 1021, 'pic_fooldoctor_02_battle', N_('博士士 2')),
|
||||
emo('autochess_battle_fooldoctor_03', 1022, 'pic_fooldoctor_04_battle', N_('博士士 3')),
|
||||
emo('autochess_battle_fooldoctor_04', 1023, 'pic_fooldoctor_05_battle', N_('博士士 4')),
|
||||
emo('autochess_battle_fooldoctor_05', 1024, 'pic_fooldoctor_06_battle', N_('博士士 5')),
|
||||
emo('autochess_battle_fooldoctor_06', 1025, 'pic_fooldoctor_08_battle', N_('博士士 6')),
|
||||
] },
|
||||
{ themeId: 'emoticon_foolsday_amiya', dir: 'foolamiya', sortId: 1003, isBasic: false, name: '表情套组:米米子', emotes: [
|
||||
emo('autochess_battle_foolamiya_01', 1040, 'pic_foolamiya_01_battle', '米米子 1'),
|
||||
emo('autochess_battle_foolamiya_02', 1041, 'pic_foolamiya_02_battle', '米米子 2'),
|
||||
emo('autochess_battle_foolamiya_03', 1042, 'pic_foolamiya_03_battle', '米米子 3'),
|
||||
emo('autochess_battle_foolamiya_04', 1043, 'pic_foolamiya_04_battle', '米米子 4'),
|
||||
emo('autochess_battle_foolamiya_05', 1044, 'pic_foolamiya_05_battle', '米米子 5'),
|
||||
emo('autochess_battle_foolamiya_06', 1045, 'pic_foolamiya_06_battle', '米米子 6'),
|
||||
{ themeId: 'emoticon_foolsday_amiya', dir: 'foolamiya', sortId: 1003, isBasic: false, name: N_('表情套组:米米子'), emotes: [
|
||||
emo('autochess_battle_foolamiya_01', 1040, 'pic_foolamiya_01_battle', N_('米米子 1')),
|
||||
emo('autochess_battle_foolamiya_02', 1041, 'pic_foolamiya_02_battle', N_('米米子 2')),
|
||||
emo('autochess_battle_foolamiya_03', 1042, 'pic_foolamiya_03_battle', N_('米米子 3')),
|
||||
emo('autochess_battle_foolamiya_04', 1043, 'pic_foolamiya_04_battle', N_('米米子 4')),
|
||||
emo('autochess_battle_foolamiya_05', 1044, 'pic_foolamiya_05_battle', N_('米米子 5')),
|
||||
emo('autochess_battle_foolamiya_06', 1045, 'pic_foolamiya_06_battle', N_('米米子 6')),
|
||||
] },
|
||||
{ themeId: 'emoticon_foolsday_wisdel', dir: 'foolwisdel', sortId: 1004, isBasic: false, name: '表情套组:维维美', emotes: [
|
||||
emo('autochess_battle_foolwisdel_01', 1060, 'pic_foolwisdel_01_battle', '维维美 1'),
|
||||
emo('autochess_battle_foolwisdel_02', 1061, 'pic_foolwisdel_02_battle', '维维美 2'),
|
||||
emo('autochess_battle_foolwisdel_03', 1062, 'pic_foolwisdel_03_battle', '维维美 3'),
|
||||
emo('autochess_battle_foolwisdel_04', 1063, 'pic_foolwisdel_04_battle', '维维美 4'),
|
||||
emo('autochess_battle_foolwisdel_05', 1064, 'pic_foolwisdel_05_battle', '维维美 5'),
|
||||
emo('autochess_battle_foolwisdel_06', 1065, 'pic_foolwisdel_06_battle', '维维美 6'),
|
||||
{ themeId: 'emoticon_foolsday_wisdel', dir: 'foolwisdel', sortId: 1004, isBasic: false, name: N_('表情套组:维维美'), emotes: [
|
||||
emo('autochess_battle_foolwisdel_01', 1060, 'pic_foolwisdel_01_battle', N_('维维美 1')),
|
||||
emo('autochess_battle_foolwisdel_02', 1061, 'pic_foolwisdel_02_battle', N_('维维美 2')),
|
||||
emo('autochess_battle_foolwisdel_03', 1062, 'pic_foolwisdel_03_battle', N_('维维美 3')),
|
||||
emo('autochess_battle_foolwisdel_04', 1063, 'pic_foolwisdel_04_battle', N_('维维美 4')),
|
||||
emo('autochess_battle_foolwisdel_05', 1064, 'pic_foolwisdel_05_battle', N_('维维美 5')),
|
||||
emo('autochess_battle_foolwisdel_06', 1065, 'pic_foolwisdel_06_battle', N_('维维美 6')),
|
||||
] },
|
||||
].map((t) => Object.freeze({ ...t, emotes: Object.freeze(t.emotes) })));
|
||||
/** Every emote with its theme: `{ id, sortId, picId, label, themeId, dir }`, in wheel order. */
|
||||
|
||||
+3
-3
@@ -155,12 +155,12 @@ export function diyPool(tier, { data, kitted = null } = { data: null }) {
|
||||
*/
|
||||
export function checkDiyPick(slotId, pick, data) {
|
||||
const slot = diySlot(slotId, data);
|
||||
if (!slot) return { error: `not a 自选 slot: ${slotId}` };
|
||||
if (!slot) return { error: `not a 自选 slot: ${slotId}` }; // i18n-ignore: developer detail
|
||||
if (!isObj(pick) || typeof pick.charId !== 'string' || !ID.test(pick.charId)) return { error: `${slot.baseId}: bad pick` };
|
||||
const { charId } = pick;
|
||||
const diy = diyOf(data);
|
||||
const proto = isPrototypePick(data, slot.tier, charId);
|
||||
if (!proto && !(diy.ownedPool ?? []).includes(charId)) return { error: `${charId} is not a tier-${slot.tier} 自选 pick` };
|
||||
if (!proto && !(diy.ownedPool ?? []).includes(charId)) return { error: `${charId} is not a tier-${slot.tier} 自选 pick` }; // i18n-ignore: developer detail
|
||||
const want = pick.skillIndex ?? null;
|
||||
const wantMod = pick.uniEquipId === undefined || pick.uniEquipId === 'none' ? null : pick.uniEquipId;
|
||||
if (want !== null && !(Number.isInteger(want) && want >= 0 && want <= 9)) return { error: `${charId}: bad skill index ${want}` };
|
||||
@@ -172,7 +172,7 @@ export function checkDiyPick(slotId, pick, data) {
|
||||
const lk = lockedSelection(data, slot.tier, charId);
|
||||
if (!lk) return { error: `${charId}: no locked selection at tier ${slot.tier}` };
|
||||
// omitted / null = its locked selection; another skill, another module or an explicit 'none' is refused
|
||||
if (want !== null && want !== lk.skillIndex) return { error: `${charId}: a prototype carries skill ${lk.skillIndex} (与系统补位时一致)` };
|
||||
if (want !== null && want !== lk.skillIndex) return { error: `${charId}: a prototype carries skill ${lk.skillIndex} (与系统补位时一致)` }; // i18n-ignore: developer detail
|
||||
if (pick.uniEquipId != null && wantMod !== lk.uniEquipId) return { error: `${charId}: a prototype carries module ${lk.uniEquipId ?? 'none'}` };
|
||||
({ skillIndex, uniEquipId } = lk);
|
||||
} else {
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
// §战斗部署), and is not read. The battle keeps position MELEE: on a 高台 the unit attacks and blocks nothing.
|
||||
|
||||
/** The trait line that lets a MELEE chess stand on a ranged (高台) tile. */
|
||||
export const PLACE_ON_RANGED = '可以放置于远程位';
|
||||
export const PLACE_ON_RANGED = '可以放置于远程位'; // i18n-ignore: matched against the data's trait text
|
||||
|
||||
/**
|
||||
* The record's trait text without a module ('' when it has none).
|
||||
|
||||
@@ -184,4 +184,12 @@ test('tools/i18n.mjs: msgids of template literals name their params; the codemod
|
||||
const aliased = await scanSource("import { t as tr } from '../../../shared/i18n.js';\nconst t = 3;\nexport const f = () => tr('替补') + t;", 'x.js');
|
||||
assert.deepEqual(aliased.msgids.map((m) => [m.msgid, m.via]), [['替补', 't']]);
|
||||
assert.deepEqual(aliased.literals.filter((l) => !l.reason), []);
|
||||
// developer text is not UI text: an error result's detail, a logger call, a file marked i18n-ignore-file
|
||||
const dev = await scanSource([
|
||||
"export const a = (x) => ({ error: 'BAD_TARGET', detail: `不是自选格子 ${x ? '甲' : '乙'}` });",
|
||||
"export function b(ev, log) { ev.detail = '已经是精锐'; log.warn(`[match] 自选 被忽略`); this.m.log?.info?.('信息'); return '界面文字'; }",
|
||||
].join('\n'), 'x.js');
|
||||
assert.deepEqual(dev.literals.filter((l) => !l.reason).map((l) => l.msgid), ['界面文字']);
|
||||
const whole = await scanSource("// (i18n-ignore-file: developer reports)\nexport const r = ['开发者报告', `第${1}条`];", 'x.js');
|
||||
assert.deepEqual(whole.literals.filter((l) => !l.reason), []);
|
||||
});
|
||||
+20
-4
@@ -143,6 +143,17 @@ const calleeName = (call) => {
|
||||
return null;
|
||||
};
|
||||
const isConsoleCall = (call) => call?.callee?.type === 'MemberExpression' && call.callee.object?.type === 'Identifier' && call.callee.object.name === 'console';
|
||||
/** A logger call (`log.warn(…)`, `this.m.log?.warn?.(…)`): developer text, like console.*. */
|
||||
const isLogCall = (call) => {
|
||||
const c = call?.callee;
|
||||
if (!c || c.type !== 'MemberExpression') return false;
|
||||
const o = c.object;
|
||||
const LOG = /^(log|logger)$/;
|
||||
return (o.type === 'Identifier' && LOG.test(o.name)) || (o.type === 'MemberExpression' && !o.computed && o.property.type === 'Identifier' && LOG.test(o.property.name));
|
||||
};
|
||||
/** The developer detail of an error result (`{ error: 'BAD_TARGET', detail: '…' }`, `ev.detail = '…'`): never shown to players. */
|
||||
const isDetail = (node, parent) => (parent.type === 'Property' && parent.value === node && !parent.computed && (parent.key.name ?? parent.key.value) === 'detail')
|
||||
|| (parent.type === 'AssignmentExpression' && parent.right === node && parent.left.type === 'MemberExpression' && !parent.left.computed && parent.left.property.name === 'detail');
|
||||
|
||||
/** Why a literal must not be wrapped (null = wrap it). `aliases`: local name → shared/i18n.js export (`t as tr`). */
|
||||
function skipReason(node, ancestors, aliases = new Map()) {
|
||||
@@ -150,7 +161,7 @@ function skipReason(node, ancestors, aliases = new Map()) {
|
||||
for (let i = ancestors.length - 1; i >= 0; i--) {
|
||||
const a = ancestors[i];
|
||||
if (a.type === 'CallExpression') {
|
||||
if (isConsoleCall(a)) return 'console';
|
||||
if (isConsoleCall(a) || isLogCall(a)) return 'console';
|
||||
const name = calleeName(a);
|
||||
if (a.callee.type === 'Identifier' && SKIP_CALLS.has(aliases.get(name) ?? name)) return 'in-call';
|
||||
}
|
||||
@@ -159,6 +170,8 @@ function skipReason(node, ancestors, aliases = new Map()) {
|
||||
if (a.type === 'ImportDeclaration' || a.type === 'ExportAllDeclaration' || (a.type === 'ExportNamedDeclaration' && a.source)) return 'import';
|
||||
}
|
||||
if (!parent) return null;
|
||||
// (inside a detail too: `detail: \`… ${x ? '甲' : '乙'}\``)
|
||||
if (isDetail(node, parent) || ancestors.some((a, i) => i > 0 && isDetail(a, ancestors[i - 1]))) return 'detail';
|
||||
if (parent.type === 'Property' && parent.key === node && !parent.computed) return 'key';
|
||||
if (parent.type === 'MemberExpression' && parent.property === node) return 'key';
|
||||
if (parent.type === 'BinaryExpression' && ['===', '!==', '==', '!=', 'in', 'instanceof'].includes(parent.operator)) return 'compare';
|
||||
@@ -333,6 +346,8 @@ export async function scanSource(src, file = '<src>') {
|
||||
const fnName = (callee) => (callee.type === 'Identifier' ? aliases.get(callee.name) ?? callee.name : null);
|
||||
// a line marked `i18n-ignore` (in a comment) holds no UI text (font samples, data keys …)
|
||||
const ignored = new Set(src.split('\n').map((l, i) => (/i18n-ignore/.test(l) ? i + 1 : 0)).filter(Boolean));
|
||||
// a file marked `i18n-ignore-file` (developer reports, bilingual error pages) holds no UI text to translate
|
||||
const fileIgnored = /\/\/[^\n]*i18n-ignore-file/.test(src);
|
||||
walk(ast, (node, ancestors) => {
|
||||
if (node.type === 'CallExpression' && fnName(node.callee) === 'tc') {
|
||||
const [c, a] = node.arguments;
|
||||
@@ -347,7 +362,8 @@ export async function scanSource(src, file = '<src>') {
|
||||
if (node.type === 'TaggedTemplateExpression' && node.tag.type === 'Identifier' && node.tag.name === 'html') {
|
||||
for (const seg of htmlSegments(node.quasi)) {
|
||||
const { msgid, params } = templateMsgid(seg.quasis, seg.exprs.map((e) => src.slice(e.start, e.end)));
|
||||
literals.push({ kind: seg.kind, attr: seg.attr, start: seg.start, end: seg.end, line: lineOf(seg.start), msgid, params, exprs: seg.exprs.map((e) => [e.start, e.end]), module: !inFunction(ancestors), reason: null });
|
||||
const line = lineOf(seg.start);
|
||||
literals.push({ kind: seg.kind, attr: seg.attr, start: seg.start, end: seg.end, line, msgid, params, exprs: seg.exprs.map((e) => [e.start, e.end]), module: !inFunction(ancestors), reason: fileIgnored || ignored.has(line) ? 'ignored' : null });
|
||||
}
|
||||
// expressions inside are walked normally (nested html``, strings in ${…})
|
||||
return true;
|
||||
@@ -365,7 +381,7 @@ export async function scanSource(src, file = '<src>') {
|
||||
return true;
|
||||
}
|
||||
const line = lineOf(node.start);
|
||||
const reason = ignored.has(line) ? 'ignored' : skipReason(node, ancestors, aliases);
|
||||
const reason = fileIgnored || ignored.has(line) ? 'ignored' : skipReason(node, ancestors, aliases);
|
||||
literals.push({ kind: 'str', start: node.start, end: node.end, line, msgid: node.value, params: [], module: !inFunction(ancestors), reason });
|
||||
return true;
|
||||
}
|
||||
@@ -374,7 +390,7 @@ export async function scanSource(src, file = '<src>') {
|
||||
if (parent && parent.type === 'TaggedTemplateExpression') return true;
|
||||
const cooked = node.quasis.map((q) => q.value.cooked ?? q.value.raw);
|
||||
if (!cooked.some((s) => HAN.test(s))) return true;
|
||||
const reason = ignored.has(lineOf(node.start)) ? 'ignored' : skipReason(node, ancestors, aliases);
|
||||
const reason = fileIgnored || ignored.has(lineOf(node.start)) ? 'ignored' : skipReason(node, ancestors, aliases);
|
||||
const { msgid, params } = templateMsgid(cooked, node.expressions.map((e) => src.slice(e.start, e.end)));
|
||||
literals.push({ kind: 'tpl', start: node.start, end: node.end, line: lineOf(node.start), msgid, params, exprs: node.expressions.map((e) => [e.start, e.end]), module: !inFunction(ancestors), reason });
|
||||
return true;
|
||||
|
||||
Reference in new issue
Block a user