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:
1 parent
e596478bef
commit
a12a539e4d
13 files changed
+2102
-171
No files matched your search
+27
-16
@@ -23,6 +23,9 @@
|
||||
// kept in sync with the server by installLoadoutSync (room.loadout after every welcome and edit).
|
||||
// Game data: every text of the game is static data (/data/*.json) downloaded once per page; the in-match files are
|
||||
// warmed in the background as soon as the player is in a room (warmGameData), before the match needs them.
|
||||
// Language (ui/lang.js, docs/I18N.md): chosen before the first render (initLang); App re-renders on a switch (useLang).
|
||||
// Server texts are translated on arrival: m.toast / m.ticker frames (msgid + params, or the text as a msgid; a
|
||||
// config.broadcasts line from its id + args), error codes (ui/toasts.js describeError) and room.closed reasons.
|
||||
|
||||
// Polyfills first (older Safari / Firefox ESR): every module evaluated after this one sees them.
|
||||
import './ui/compat.js';
|
||||
@@ -46,6 +49,8 @@ import { installDeviceSupport } from './ui/device.js';
|
||||
import { LoadoutHost } from './screens/loadout.js';
|
||||
import { installLoadoutSync } from './ui/loadoutSync.js';
|
||||
import { startBuildGuard } from './ui/buildGuard.js';
|
||||
import { initLang, useLang, tickerText } from './ui/lang.js';
|
||||
import { t, N_, translateWire } from '../../shared/i18n.js';
|
||||
|
||||
const RESTORE_GRACE_MS = 1500;
|
||||
const JOIN_DELAY_MS = 350;
|
||||
@@ -92,7 +97,7 @@ function schedulePendingJoin() {
|
||||
const code = s.ui.pendingJoin;
|
||||
if (!code || joinInFlight || !s.session.entered || net.status !== 'online') return;
|
||||
if (s.room) {
|
||||
if (s.room.code !== code) toast('你已在其他同盟中,请先离开当前同盟', 'warn');
|
||||
if (s.room.code !== code) toast(t('你已在其他同盟中,请先离开当前同盟'), 'warn');
|
||||
clearPendingJoin();
|
||||
return;
|
||||
}
|
||||
@@ -146,7 +151,7 @@ function onWelcome(msg) {
|
||||
// expired on it): whatever we showed before is gone — back to the lobby cleanly and say why.
|
||||
const notice = sessionResetNotice(prev, msg.playerId);
|
||||
backToLobby();
|
||||
if (notice) toast(notice, 'warn', { ttl: 7000 });
|
||||
if (notice) toast(t(notice), 'warn', { ttl: 7000 });
|
||||
} else if (prev.room || prev.match.public) {
|
||||
// Resumed session: the server re-pushes room/match state; drop whatever it doesn't.
|
||||
store.patch('ui', { restoring: true });
|
||||
@@ -170,7 +175,7 @@ function onRoomState(msg) {
|
||||
const seats = Array.isArray(room.seats) ? room.seats : [];
|
||||
if (myId != null && seats.length && !seats.some((s) => s && s.playerId === myId) && !isSpectating(room, myId)) {
|
||||
// We are no longer seated (kicked / left elsewhere) — neither in a player seat nor a spectator seat.
|
||||
if (store.get().room) toast('你已不在该同盟中', 'warn');
|
||||
if (store.get().room) toast(t('你已不在该同盟中'), 'warn');
|
||||
store.set({ room: null, match: emptyMatch() });
|
||||
return;
|
||||
}
|
||||
@@ -184,8 +189,8 @@ function onRoomState(msg) {
|
||||
|
||||
const CLOSE_REASON = {
|
||||
// 'timeout' = this player was removed after staying disconnected past the lobby grace (server/lobby.js)
|
||||
host_left: '创建者已离开,同盟已解散', timeout: '由于长时间断开连接,你已离开同盟', empty: '同盟已解散',
|
||||
kicked: '你已被移出同盟', ended: '模拟已结束', expired: '同盟已过期', shutdown: '服务器维护中,同盟已关闭',
|
||||
host_left: N_('创建者已离开,同盟已解散'), timeout: N_('由于长时间断开连接,你已离开同盟'), empty: N_('同盟已解散'),
|
||||
kicked: N_('你已被移出同盟'), ended: N_('模拟已结束'), expired: N_('同盟已过期'), shutdown: N_('服务器维护中,同盟已关闭'),
|
||||
};
|
||||
|
||||
function wireNet() {
|
||||
@@ -201,12 +206,13 @@ function wireNet() {
|
||||
net.on('clock', (c) => store.set({ clock: { offset: c.offset, rtt: c.rtt, synced: c.synced } }));
|
||||
net.on('welcome', onWelcome);
|
||||
net.on('helloError', (err) => toastError(err));
|
||||
net.on('replaced', () => toast('该身份已在其他页面登录,本页已断开', 'warn', { ttl: 6000 }));
|
||||
net.on('replaced', () => toast(t('该身份已在其他页面登录,本页已断开'), 'warn', { ttl: 6000 }));
|
||||
net.on('unhandledError', (err) => toastError(err));
|
||||
net.on('room.state', onRoomState);
|
||||
net.on('room.closed', (msg) => {
|
||||
backToLobby();
|
||||
toast(CLOSE_REASON[msg.reason] || (typeof msg.reason === 'string' && msg.reason.length < 60 ? `同盟已关闭:${msg.reason}` : '同盟已关闭'), 'warn');
|
||||
const known = Object.hasOwn(CLOSE_REASON, String(msg.reason)) ? CLOSE_REASON[msg.reason] : null;
|
||||
toast(known ? t(known) : typeof msg.reason === 'string' && msg.reason.length < 60 ? t('同盟已关闭:{reason}', { reason: msg.reason }) : t('同盟已关闭'), 'warn');
|
||||
});
|
||||
net.on('m.public', (msg) => { matchAt = Date.now(); store.patch('match', { public: payload(msg) }); maybeFinishRestore(); });
|
||||
net.on('m.private', (msg) => { matchAt = Date.now(); store.patch('match', { private: payload(msg) }); });
|
||||
@@ -214,17 +220,19 @@ function wireNet() {
|
||||
net.on('m.result', (msg) => store.patch('match', { result: payload(msg) }));
|
||||
net.on('m.toast', (msg) => {
|
||||
const kind = ['info', 'success', 'warn', 'error'].includes(msg.kind) ? msg.kind : 'info';
|
||||
toast(msg.text, kind);
|
||||
// msgid + params (server ≥ 0.2.0) or the text itself as a msgid, in the current language
|
||||
toast(translateWire(msg), kind);
|
||||
});
|
||||
net.on('m.ticker', (msg) => {
|
||||
if (typeof msg.text !== 'string') return;
|
||||
const text = tickerText(msg);
|
||||
// type, player + the round it came in: a BOSS_HIT line is dropped once its boss round is over and superseded by the
|
||||
// same player's next one (ui/ticker.js tickerLineLive / tickerSupersedes)
|
||||
const type = typeof msg.type === 'string' ? msg.type : null;
|
||||
const playerId = typeof msg.playerId === 'string' ? msg.playerId : null;
|
||||
// its broadcast priority: the strip plays the highest first (ui/ticker.js enqueueTickerLines)
|
||||
const priority = Number.isFinite(msg.priority) ? msg.priority : 0;
|
||||
store.set((s) => ({ ticker: [...s.ticker.slice(-(TICKER_KEEP - 1)), { id: ++seq, text: msg.text, at: Date.now(), type, playerId, round: s.match?.public?.round ?? null, priority }] }));
|
||||
store.set((s) => ({ ticker: [...s.ticker.slice(-(TICKER_KEEP - 1)), { id: ++seq, text, at: Date.now(), type, playerId, round: s.match?.public?.round ?? null, priority }] }));
|
||||
});
|
||||
net.on('m.emote', (msg) => {
|
||||
store.set((s) => ({ emotes: [...s.emotes.slice(-(EMOTE_KEEP - 1)), { seq: ++seq, playerId: msg.playerId, id: msg.id, at: Date.now() }] }));
|
||||
@@ -257,15 +265,16 @@ function ScreenCrashed({ error, reset }) {
|
||||
return html`<div class="screen crash">
|
||||
<div class="crash__box brackets">
|
||||
<${MicroLabel} tone="mint">SYSTEM FAULT<//>
|
||||
<h2>界面发生错误</h2>
|
||||
<h2>${t('界面发生错误')}</h2>
|
||||
<p class="t-lo">${String(error?.message || error).slice(0, 200)}</p>
|
||||
<${Button} variant="primary" icon="refresh" onClick=${reset}>重新加载界面<//>
|
||||
<${Button} variant="primary" icon="refresh" onClick=${reset}>${t('重新加载界面')}<//>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function App() {
|
||||
const route = useStore(selectRoute);
|
||||
useLang(); // a language switch re-renders the whole tree in place
|
||||
const [error, resetError] = useErrorBoundary((err) => console.error('[ui] screen crashed', err));
|
||||
const Screen = SCREENS[route] || LobbyScreen;
|
||||
return html`<div class="app-root">
|
||||
@@ -285,8 +294,8 @@ async function waitForFonts(ms) {
|
||||
const fonts = document.fonts;
|
||||
if (!fonts || typeof fonts.load !== 'function') return;
|
||||
const loads = [
|
||||
fonts.load('900 1em "Noto Sans SC"', '卫戍协议盟约'),
|
||||
fonts.load('700 1em "Noto Sans SC"', '开始'),
|
||||
fonts.load('900 1em "Noto Sans SC"', '卫戍协议盟约'), // i18n-ignore (font sample)
|
||||
fonts.load('700 1em "Noto Sans SC"', '开始'), // i18n-ignore (font sample)
|
||||
fonts.load('700 1em Bender', '0123456789'),
|
||||
fonts.load('700 1em Rajdhani', '0123456789'),
|
||||
].map((p) => p.catch(() => null));
|
||||
@@ -301,7 +310,7 @@ function installGlobalErrorHandlers() {
|
||||
if (err && (err.name === 'NotAllowedError' || err.name === 'AbortError')) { console.warn('[app] ignored rejection', err.name); return; }
|
||||
console.error('[app] unhandled rejection', err);
|
||||
if (err instanceof NetError) toastError(err);
|
||||
else toast(`发生意外错误:${describeError(err)}`.slice(0, 120), 'error');
|
||||
else toast(t('发生意外错误:{error}', { error: describeError(err) }).slice(0, 120), 'error');
|
||||
});
|
||||
window.addEventListener('error', (ev) => {
|
||||
if (!(ev instanceof ErrorEvent)) return; // resource load errors are not script errors
|
||||
@@ -342,7 +351,9 @@ async function boot() {
|
||||
if (entered) net.setName(savedName);
|
||||
else net.connect();
|
||||
});
|
||||
await Promise.all([waitForFonts(1200), connectWhenReady]);
|
||||
// the language (and its UI translations) before the first render: no Chinese flash for an English player
|
||||
const langReady = initLang().catch((err) => console.warn('[app] language setup failed', err));
|
||||
await Promise.all([waitForFonts(1200), connectWhenReady, langReady]);
|
||||
const root = document.getElementById('app');
|
||||
render(html`<${App} />`, root);
|
||||
|
||||
@@ -369,5 +380,5 @@ async function boot() {
|
||||
boot().catch((err) => {
|
||||
console.error('[app] boot failed', err);
|
||||
const el = document.getElementById('boot-err');
|
||||
if (el) el.textContent = '启动失败,请刷新页面重试';
|
||||
if (el) el.textContent = t('启动失败,请刷新页面重试');
|
||||
});
|
||||
Reference in new issue
Block a user