feedback5 WI: UI strings through t() with an English switch on the title screen and in 设置; pilot screens converted

public/i18n/en.json: Chinese msgid -> English, seeded from PR #70 (@YuriRestia, credited) for every Chinese
literal of the current code (≈830 entries; 966 of the 1 036 literals left for phase 2 already translated),
completed by hand in the official EN terms. public/js/ui/lang.js: boot language (?lang=en|zh, then
localStorage sp.pref.lang, else Chinese; the browser language is not consulted), switchLang, useLang,
LangToggle; main.js renders after initLang and re-renders in place on a switch, and translates server
texts (m.toast / m.ticker frames, room.closed reasons, error codes via ui/toasts.js describeError).
Converted: title screen (English title, wordmark hidden), lobby, room, settings (语言 row, On/Off,
quality, hotkeys), the shell (crash screen, toasts) and DifficultyTag.

tools/i18n.mjs: extract (msgids + untranslated literals per file), check (msgids missing from en.json),
codemod (html`` text / attributes / strings / template literals -> t(), nested strings, N_ for module
level, i18n-ignore lines), seed (fill en.json from a msgid map).
This commit is contained in:
sganggs committed 2026-10-05 20:19:58 +08:00
1 parent e596478bef
commit a12a539e4d
13 files changed
+2102 -171

No files matched your search

+23 -16
View File
@@ -1,4 +1,5 @@
// Title screen: season-style backdrop, big title 卫戍协议:盟约, remembered nickname, 开始.
// Title screen: season-style backdrop, big title 卫戍协议:盟约, remembered nickname, 开始, the language switch
// (中文 | English, ui/lang.js; in English the big title is the English one and the small wordmark above it hides).
//
// Pressing 开始 validates the nickname (1..NAME_MAX_LEN chars, no control characters), stores it,
// marks this tab as "entered" (so reloads skip the title) and hands the name to net.js, which
@@ -17,6 +18,8 @@ import { net, identity } from '../net.js';
import { store, useStore, shallowEqual } from '../store.js';
import { data, useData } from '../data.js';
import { FullscreenButton, detectFeatures } from '../ui/device.js';
import { LangToggle, useLang } from '../ui/lang.js';
import { t, N_ } from '../../../shared/i18n.js';
// Same character classes as server/net.js sanitizeName (control, zero-width, bidi, BOM), so a name
// the client accepts is never rejected by the server's hello validation.
@@ -174,14 +177,15 @@ function Ridges() {
}
const STATUS_TEXT = {
idle: '准备连接', connecting: '正在连接服务器', connected: '已连接服务器', handshaking: '正在验证身份',
online: '已连接服务器', reconnecting: '连接中断,正在重连', closed: '连接已关闭',
idle: N_('准备连接'), connecting: N_('正在连接服务器'), connected: N_('已连接服务器'), handshaking: N_('正在验证身份'),
online: N_('已连接服务器'), reconnecting: N_('连接中断,正在重连'), closed: N_('连接已关闭'),
};
/** Title screen component. */
export function TitleScreen() {
const conn = useStore((s) => s.connection, shallowEqual);
const pendingJoin = useStore((s) => s.ui.pendingJoin);
const lang = useLang();
const [name, setName] = useState(() => store.get().me.name || identity.loadName() || '');
const assetsSettled = useData('assets');
const assets = data.get('assets');
@@ -199,7 +203,7 @@ export function TitleScreen() {
const valid = isValidName(name);
const start = () => {
if (!valid) { toast('请输入博士代号', 'warn'); return; }
if (!valid) { toast(t('请输入博士代号'), 'warn'); return; }
enterSession(name);
};
@@ -232,39 +236,42 @@ export function TitleScreen() {
<div><${MicroLabel} tone="mint">RHODES ISLAND // SIMULATION SERVICE<//><br /><${MicroLabel}>TACTICAL CO-OP NODE · 02<//></div>
</div>
<div class="title-corner title-corner--tr">
<${MicroLabel} tone="hi">TARGET POINT<//><br /><${MicroLabel}>STRONGHOLD PROTOCOL<//>
<div>
<${LangToggle} class="title-lang" />
<${MicroLabel} tone="hi">TARGET POINT<//><br /><${MicroLabel}>STRONGHOLD PROTOCOL<//>
</div>
</div>
<main class="title-main">
<${Emblem} />
<div class="title-en">
${lang === 'en' ? null : html`<div class="title-en">
<span class="title-en__a">STRONGHOLD PROTOCOL</span>
<span class="title-en__b">ALLIANCE</span>
</div>
<h1 class="title-cn">卫戍协议<span class="title-cn__colon">:</span><em>盟约</em></h1>
<p class="title-tag">调配资金与干员,与同伴协同布防,抵御多波次进攻,直至击败敌方领袖。</p>
</div>`}
<h1 class=${`title-cn${lang === 'en' ? ' title-cn--latin' : ''}`}>${t('卫戍协议')}<span class="title-cn__colon">${lang === 'en' ? ': ' : ':'}</span><em>${t('盟约')}</em></h1>
<p class="title-tag">${t('调配资金与干员,与同伴协同布防,抵御多波次进攻,直至击败敌方领袖。')}</p>
<div class="title-login">
${pendingJoin ? html`<div class="title-invite">
<${Icon} name="key" />
<span>收到同盟邀请</span><b class="num">${pendingJoin}</b><span class="t-lo">· 输入代号后将自动加入</span>
<span>${t('收到同盟邀请')}</span><b class="num">${pendingJoin}</b><span class="t-lo">${t('· 输入代号后将自动加入')}</span>
</div>` : null}
<${TextField} label="博士代号" micro="CALLSIGN" size="lg" icon="user" value=${name} maxLength=${NAME_MAX_LEN}
placeholder="输入你的代号(最多 ${NAME_MAX_LEN} 字)" autoFocus=${!touchUi}
<${TextField} label=${t('博士代号')} micro="CALLSIGN" size="lg" icon="user" value=${name} maxLength=${NAME_MAX_LEN}
placeholder=${t('输入你的代号(最多 {NAME_MAX_LEN} 字)', { NAME_MAX_LEN })} autoFocus=${!touchUi}
onInput=${setName} onEnter=${start} />
<${Button} variant="primary" size="xl" block=${true} iconRight="chevrons" disabled=${!valid} onClick=${start}>开始<//>
<${Button} variant="primary" size="xl" block=${true} iconRight="chevrons" disabled=${!valid} onClick=${start}>${t('开始')}<//>
<div class="title-conn">
<span class=${`status-dot ${dotClass}`}></span>
<span>${STATUS_TEXT[conn.status] || conn.status}</span>
<span>${STATUS_TEXT[conn.status] ? t(STATUS_TEXT[conn.status]) : conn.status}</span>
${conn.status === 'online' ? html`<${PingPill} ms=${conn.ping} />` : null}
<${GuideButton} class="title-guide" />
<${GuideButton} class="title-guide" label=${t('玩法说明')} />
<${FullscreenButton} class="title-fs" />
</div>
</div>
</main>
<footer class="title-foot">
<span>非官方同人复刻 · 游戏素材版权归 上海鹰角网络 / Yostar 所有</span>
<span>${t('非官方同人复刻 · 游戏素材版权归 上海鹰角网络 / Yostar 所有')}</span>
<${MicroLabel}>v${APP_VERSION} · WEB SIMULATION<//>
</footer>
</div>`;