feedback7 G-TOOLS: out-of-order language-pack responses no longer overwrite the newer choice (PR #304 by @siruimei07)

Applied as-is. initLang and switchLang record a request counter before the first await and apply a pack only when that request is still the latest. Choosing the language already on screen cancels an in-flight load. A failed latest choice still saves the preference and leaves the previous UI.

Tests: node --test test/ui/lang.test.js test/ui/i18n.e2e.test.js test/i18n-packs.test.js test/i18n-server.test.js — 29 pass, 0 fail. The Chrome suite in test/ui/i18n.e2e.test.js skipped (SP_E2E unset).
This commit is contained in:
siruimei07 authored and sganggs committed 2026-10-08 18:41:58 +08:00
1 parent 7c307d8613
commit 5662b7ef65
3 files changed
+362 -5

No files matched your search

+19 -5
View File
@@ -39,6 +39,8 @@ const uiLoads = new Map();
let indexLoad = null;
/** Whether the pack index arrived (without it the chain of a pack is taken from its own `_meta` as it loads). */
let indexOk = false;
/** Only the latest boot or explicit selection may apply its loaded language. */
let langRequest = 0;
const INDEX_URL = `${PACKS_URL}${PACK_INDEX_FILE}`;
const defaultFetch = (...a) => globalThis.fetch(...a);
@@ -194,20 +196,23 @@ function applyLang(lang) {
* @returns {Promise<string>} the language in effect
*/
export async function initLang() {
const request = ++langRequest;
wire();
applyDocument(getLang());
const TIMEOUT = Symbol('timeout');
const boot = (async () => {
const listed = await loadLangIndex();
const { lang, fromUrl } = initialLang(undefined, undefined, { tentative: !listed });
if (fromUrl) { savePref(PREF_KEY, lang); stripLangParam(); }
if (fromUrl) stripLangParam();
if (request !== langRequest) return null;
if (fromUrl) savePref(PREF_KEY, lang);
if (lang === DEFAULT_LANG) return null;
return (await loadLangChain(lang)) ? lang : null;
})();
const got = await Promise.race([boot, new Promise((r) => setTimeout(() => r(TIMEOUT), BOOT_WAIT_MS))]);
if (got === TIMEOUT) {
boot.then((lang) => { if (lang && normalizeLang(loadPref(PREF_KEY, null)) === lang) applyLang(lang); });
} else if (got) applyLang(got);
boot.then((lang) => { if (request === langRequest && lang && normalizeLang(loadPref(PREF_KEY, null)) === lang) applyLang(lang); });
} else if (request === langRequest && got) applyLang(got);
return getLang();
}
@@ -218,10 +223,12 @@ export async function initLang() {
* @returns {Promise<string>} the language in effect
*/
export async function switchLang(lang) {
const request = ++langRequest;
wire();
const want = normalizeLang(lang) || DEFAULT_LANG;
savePref(PREF_KEY, want);
if (want !== DEFAULT_LANG && !(await loadLangChain(want))) return getLang();
if (request !== langRequest) return getLang();
applyLang(want);
return getLang();
}
@@ -290,10 +297,17 @@ export function machineTranslationNote() {
export function LangToggle({ class: cls }) {
const lang = useLang();
const menu = langMenuModel(useLangs(), lang);
const pick = (code) => { if (code && code !== getLang()) switchLang(code); };
const pick = async (code, select = null) => {
if (!code) return;
const switched = switchLang(code);
const request = langRequest;
await switched;
// A failed current request keeps the displayed language; stale requests must not reset a newer pending pick.
if (select && request === langRequest) select.value = getLang();
};
if (menu.kind === 'select') {
return html`<label class=${`set-seg lang-toggle lang-select${cls ? ` ${cls}` : ''}`} data-testid="lang-toggle">
<select aria-label=${SWITCH_LABEL} value=${lang} onChange=${(e) => pick(e.currentTarget.value)}>
<select aria-label=${SWITCH_LABEL} value=${lang} onChange=${(e) => pick(e.currentTarget.value, e.currentTarget)}>
${menu.items.map((it) => html`<option key=${it.code} value=${it.code} lang=${it.htmlLang} title=${it.title || undefined} selected=${it.on}>${it.label}</option>`)}
</select>
</label>`;