diff --git a/main.js b/main.js index ea6899aa..0c7f2d29 100644 --- a/main.js +++ b/main.js @@ -3769,10 +3769,15 @@ function createTab(initial, opts = {}) { // Translator state is per-document: a new navigation drops any "translated" // flag, the autoTried latch, and the cached page language. The chip then // re-decides on the next pageLang read whether to light up — and auto- - // translate gets one more shot on the new page. + // translate gets one more shot on the new page. The one bit we preserve + // across the reset is `pending`: setWebsiteLanguage sets it right before + // triggering a reload, and the subsequent did-finish-load uses it to + // translate unconditionally (the user ASKED for a new language). wc.on("did-start-navigation", (_e, _url, _ihr, isMainFrame) => { if (!isMainFrame) return; - tab._tr = null; tab.pageLang = ""; + const wasPending = !!tab._tr?.pending; + tab._tr = wasPending ? { translated: false, source: "", target: "", error: "", pending: true } : null; + tab.pageLang = ""; if (tab.id === activeId) emitTranslateState(tab); }); // After the page has committed, read once so the chip knows @@ -3789,16 +3794,25 @@ function createTab(initial, opts = {}) { if (tab.id === activeId) emitTranslateState(tab); try { const target = translationTargetBase(); - if (settings.translateAutoOffer && - tab.pageLang && tab.pageLang !== target && - isTranslatorSupported(tab.pageLang) && isTranslatorSupported(target) && - !tabTranslateState(tab).translated && - !tabTranslateState(tab).autoTried) { - tabTranslateState(tab).autoTried = true; - // Only auto-translate when this is the active tab, to spare a cold - // backend the N parallel requests of a many-tab restore; a background - // tab gets translated when the user focuses it (same hook, next load). - if (tab.id === activeId) await translateActiveTab(); + const st = tabTranslateState(tab); + // Two gates fire auto-translate here: + // (a) Explicit language switch (st.pending) — the user asked for a + // different language right now; translate the active tab even if + // auto-offer is off and even if pageLang is empty (let the + // backend auto-detect the source). + // (b) Normal auto-offer — translateAutoOffer on, pageLang known and + // different from the user's target, both ends supported, once + // per document (autoTried latch). + const explicit = st.pending && tab.id === activeId; + const pageSupported = tab.pageLang ? isTranslatorSupported(tab.pageLang) : true; // unknown -> try, backend decides + const autoFire = settings.translateAutoOffer && tab.id === activeId && + isTranslatorSupported(target) && pageSupported && + (tab.pageLang ? tab.pageLang !== target : true) && + !st.translated && !st.autoTried; + if (explicit || autoFire) { + st.autoTried = true; + st.pending = false; + await translateActiveTab(); } } catch (e) { console.warn("[translate] auto failed:", e?.message); } }); @@ -4635,7 +4649,7 @@ const TRANSLATOR_SUPPORTED = new Set(["en", "es", "fr", "de", "el", "ru"]); function isTranslatorSupported(tag) { return TRANSLATOR_SUPPORTED.has(String(tag || "").split("-")[0].toLowerCase()); } -function setWebsiteLanguage(mode, value) { +async function setWebsiteLanguage(mode, value) { const before = { mode: settings.languageMode, value: settings.languageValue }; if (mode === "show") { settings.languageMode = "show"; @@ -4651,11 +4665,23 @@ function setWebsiteLanguage(mode, value) { // Nothing short of a reload makes a loaded page re-render in a new language — // the server picked the body from the Accept-Language on the ORIGINAL request. // The chip is a one-click language switch, so the user expects to see it take - // effect on whatever they're looking at: reload the active tab (only). + // effect on whatever they're looking at: reload the active tab (only), and + // if the page is already translated, revert first so the fresh reload lands + // on original HTML rather than a mix of translated nodes and new ones. + // After the reload lands the did-finish-load hook does the translation to + // the new target — unconditionally for an explicit language change (we + // bypass the auto-offer toggle, because the user ASKING to switch languages + // IS the request to translate the current page too). if (changed) { const t = activeTab(); const wc = t && !t.settings && !t.addonId && t.url ? t.view.webContents : null; - try { wc && wc.reload(); } catch {} + if (wc) { + try { + if (tabTranslateState(t).translated) await wc.executeJavaScript(PAGE_TRANSLATE_REVERT).catch(() => null); + } catch {} + t._tr = { translated: false, source: "", target: "", error: "", pending: true }; + try { wc.reload(); } catch {} + } } } // Human-readable name for a BCP-47 tag — the plain language name, no regional @@ -4915,25 +4941,34 @@ ipcMain.handle("website-language-menu-popup", (e, rect) => { { type: "separator" }, ); } - // Languages the translator can actually handle appear enabled; the rest - // show but are greyed out with a "coming later" hint so the user knows the - // picker will grow, and that picking one today only sets Accept-Language. + // Top-level menu stays short: Automatic + the supported-by-the-translator + // languages only. Unsupported entries live inside a "More languages" + // submenu, greyed out — the top strip reads at a glance instead of being + // a 25-row wall. If the current pin is one of the unsupported ones, we + // still include it in the top strip so the ✓ is visible without opening + // the submenu. + const supported = WEBSITE_LANGUAGE_QUICK.filter((L) => isTranslatorSupported(L.tag)); + const unsupported = WEBSITE_LANGUAGE_QUICK.filter((L) => !isTranslatorSupported(L.tag)); + const pinnedUnsupported = unsupported.find((L) => current === L.tag.toLowerCase()); + const topList = pinnedUnsupported ? [...supported, pinnedUnsupported] : supported; + const mkItem = (L, enabled) => { + const check = current === L.tag.toLowerCase() ? " ✓" : ""; + return { + label: enabled ? `${L.label}${check}` : `${L.label}${check}`, + enabled, + click: () => setWebsiteLanguage("manual", L.tag), + }; + }; const template = [ ...translateItems, { label: `Automatic (${languageNameFor(osLoc)})${isAuto ? " ✓" : ""}`, click: () => setWebsiteLanguage("show") }, { type: "separator" }, - ...WEBSITE_LANGUAGE_QUICK.map((L) => { - const supported = isTranslatorSupported(L.tag); - const check = current === L.tag.toLowerCase() ? " ✓" : ""; - return { - label: supported ? `${L.label}${check}` : `${L.label} — translator coming later${check}`, - enabled: supported, - click: () => setWebsiteLanguage("manual", L.tag), - }; - }), + ...topList.map((L) => mkItem(L, isTranslatorSupported(L.tag))), { type: "separator" }, - { label: "More languages…", click: () => { try { openSettingsTab("language"); } catch {} } }, + { label: "More languages (translator coming later)", + submenu: unsupported.map((L) => mkItem(L, false)) }, + { label: "Language settings…", click: () => { try { openSettingsTab("language"); } catch {} } }, ]; const popup = Menu.buildFromTemplate(template); const chromeBounds = chrome ? chrome.getBounds() : { x: 0, y: 0 }; @@ -6674,7 +6709,7 @@ ipcMain.handle("vault-pin-set", async (_e, { pin, masterPassword } = {}) => { try { vaultPin().set(String(pin || ""), String(masterPassword)); return vaultOk(); } catch (e) { return vaultErr(e.message); } }); -ipcMain.handle("vault-pin-clear", () => { vaultPin().clear(); return vaultOk(); }); +ipcMain.handle("vault-pin-clear", () => { vaultPin().clear(); return vaultOk(); }); ipcMain.handle("vault-pin-unlock", () => requestVaultUnlock({ reason: "Open your saved passwords." })); ipcMain.handle("password-status", () => ({ @@ -7161,13 +7196,22 @@ ipcMain.handle("settings-set", (_e, key, val) => { "locationMode", "locationRegion", "locationCountry", "locationLat", "locationLon", "hideMediaDevices"].includes(key)) { applyFingerprintAll(); applyAcceptLanguage(); } // Language changes alter the Accept-Language the server sees on the NEXT // request; an already-rendered page keeps the body it was first served. - // Reload the active tab so the user sees the switch take effect without a - // manual F5. (Only for the language keys; timezone / location are JS-side - // overrides and don't need the server round-trip.) + // Reload the active tab, revert any current translation first, and set + // the "pending" bit so the did-finish-load hook translates the new page + // to the new target even if auto-offer is off — a user who just picked + // a different language expects the current page to follow. if (key === "languageMode" || key === "languageValue") { const t = activeTab(); const wc = t && !t.settings && !t.addonId && t.url ? t.view.webContents : null; - try { wc && wc.reload(); } catch {} + if (wc) { + (async () => { + try { + if (tabTranslateState(t).translated) await wc.executeJavaScript(PAGE_TRANSLATE_REVERT).catch(() => null); + } catch {} + t._tr = { translated: false, source: "", target: "", error: "", pending: true }; + try { wc.reload(); } catch {} + })(); + } } // Quick-links strip: re-layout + push the new state to the strip's view so // show/hide and link edits take effect without a relaunch. If the active