diff --git a/chrome.html b/chrome.html index b047e474..6118f399 100644 --- a/chrome.html +++ b/chrome.html @@ -506,21 +506,19 @@ makes it installable (Chrome's omnibox icon). Filled once the app is installed; click then opens it or removes it. --> - + - -
@@ -768,57 +766,30 @@ .then(renderLang).catch(() => {}); T.onSettingsUpdate && T.onSettingsUpdate(renderLang); - // ---- page-translate chip ---- - // Shows when settings.translateAutoOffer is on and the loaded page's - // declared language () differs from the user's target. Click - // toggles between translated and original. Right-click (or long-press) - // opens the chip menu. - const trBtn = $("trBtn"), trCode = $("trCode"); - let trSettingsCached = null, trStateCached = null; - function trTargetBase() { - const s = trSettingsCached || {}; - const tag = s.languageMode === "manual" && s.languageValue ? s.languageValue : systemLocaleCached; - return String(tag || "en").split("-")[0].toLowerCase(); + // The language chip's tooltip also carries the translation state: when + // main broadcasts `translated: true` the tooltip switches to "Translated + // to — menu offers Show original". The actual translate / revert / + // change-language actions live in the menu that opens on click (the + // chip has one surface, not two). + let trStateCached = null; + function trCurrentTarget() { + const el = $("langCode"); const code = el ? el.textContent : "AUTO"; + if (code === "AUTO") return String(systemLocaleCached || "en").split("-")[0].toLowerCase(); + return code.toLowerCase(); } - function renderTr() { - if (!trBtn) return; - const s = trSettingsCached || {}; + function applyTranslateHint() { const st = trStateCached || {}; - const target = trTargetBase(); - const page = String(st.pageLang || "").toLowerCase(); - const translated = !!st.translated; - // Hide when auto-offer is off AND we aren't already translated, OR when - // the page is already in the user's target language. - const shouldShow = translated || (s.translateAutoOffer !== false && page && page !== target); - trBtn.hidden = !shouldShow; - if (!shouldShow) return; - trCode.textContent = (target || "??").toUpperCase().slice(0, 2); - if (translated) { - trBtn.classList.add("on"); - trBtn.title = `Translated to ${langName(target)}. Click to show the original.`; - } else if (st.error) { - trBtn.classList.remove("on"); - trBtn.title = `Translation failed: ${st.error}. Right-click for settings.`; - } else { - trBtn.classList.remove("on"); - trBtn.title = `Translate this page from ${langName(page) || page} to ${langName(target)}`; + const target = trCurrentTarget(); + if (st.translated) { + langBtn.title = `Translated to ${langName(target)}. Menu: Show original.`; + } else if (st.pageLang && st.pageLang !== target) { + langBtn.title = `Page is in ${langName(st.pageLang)}. Menu: translate to ${langName(target)}.`; } + // When page lang matches the user's target, the base renderLang tooltip + // already covers it — we leave it alone. } - if (trBtn) { - trBtn.onclick = () => { - if (trStateCached?.translated) T.pageTranslateRevert && T.pageTranslateRevert(); - else T.pageTranslate && T.pageTranslate(); - }; - trBtn.addEventListener("contextmenu", (e) => { - e.preventDefault(); - const r = trBtn.getBoundingClientRect(); - T.pageTranslateMenu && T.pageTranslateMenu({ x: Math.round(r.left), y: Math.round(r.bottom + 4) }); - }); - } - T.onPageTranslateState && T.onPageTranslateState((d) => { trStateCached = d; renderTr(); }); - T.onSettingsUpdate && T.onSettingsUpdate((s) => { trSettingsCached = s; renderTr(); }); - T.getSettings().then((s) => { trSettingsCached = s; renderTr(); }).catch(() => {}); - if (T.pageTranslateState) T.pageTranslateState().then((d) => { trStateCached = d; renderTr(); }).catch(() => {}); + T.onPageTranslateState && T.onPageTranslateState((d) => { trStateCached = d; applyTranslateHint(); }); + if (T.pageTranslateState) T.pageTranslateState().then((d) => { trStateCached = d; applyTranslateHint(); }).catch(() => {}); // ---- extension dock ---- // One toolbar button per registered addon sidebar-panel, plus a static diff --git a/main.js b/main.js index f3da2b93..c3927aa5 100644 --- a/main.js +++ b/main.js @@ -3734,8 +3734,9 @@ function createTab(initial, opts = {}) { wc.on("did-navigate", () => { if (tab.id === activeId) { notifyTabChange(); emitPwAvailability(); } }); wc.on("did-navigate-in-page", () => { if (tab.id === activeId) notifyTabChange(); }); // Translator state is per-document: a new navigation drops any "translated" - // flag and clears the cached page language. The chip then re-decides on - // the next pageLang read whether to light up for this new page. + // 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. wc.on("did-start-navigation", (_e, _url, _ihr, isMainFrame) => { if (!isMainFrame) return; tab._tr = null; tab.pageLang = ""; @@ -3743,13 +3744,30 @@ function createTab(initial, opts = {}) { }); // After the page has committed, read once so the chip knows // what language the server actually served (which may not match whatever - // we asked for via Accept-Language). + // we asked for via Accept-Language). If auto-translate is on and the page + // is in a language different from the user's, both of them supported, fire + // the translation now so the user reads the page in their own language + // without clicking anything. wc.on("did-finish-load", async () => { try { const lang = await wc.executeJavaScript(`document.documentElement.lang || ""`); tab.pageLang = String(lang || "").toLowerCase().split("-")[0]; } catch { tab.pageLang = ""; } 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(); + } + } catch (e) { console.warn("[translate] auto failed:", e?.message); } }); // Ctrl+wheel / pinch: Chromium only reports the intent on Windows and // Linux, the zoom itself is up to us. @@ -4558,6 +4576,16 @@ const WEBSITE_LANGUAGE_QUICK = [ { tag: "he-IL", label: "עברית" }, { tag: "zh-TW", label: "中文(繁體)" }, ]; +// Base BCP-47 codes the translator backend (silentmode.st/libre) currently has +// models for. The picker shows every entry in WEBSITE_LANGUAGE_QUICK but +// greys out the ones not on this list — a user who picks an unsupported one +// still gets its Accept-Language sent, but the translator stays dark on +// pages served in another language because there's no model path from the +// server's language to theirs. Grow this when the backend --load-only grows. +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) { const before = { mode: settings.languageMode, value: settings.languageValue }; if (mode === "show") { @@ -4817,16 +4845,46 @@ ipcMain.handle("website-language-menu-popup", (e, rect) => { const osLoc = app.getLocale() || "en-US"; const isAuto = settings.languageMode === "show"; const current = isAuto ? null : (settings.languageValue || "").toLowerCase(); + // Translate action goes at the top when the active page can actually be + // translated: a web tab, with the page's declared language different from + // the user's target, both languages on the translator backend. The two + // chips (globe + translate) merged into one — the globe menu covers both. + const t = activeTab(); + const canTranslate = t && !t.settings && !t.addonId && !t.pending && t.pageLang && + t.pageLang !== translationTargetBase() && + isTranslatorSupported(t.pageLang) && isTranslatorSupported(translationTargetBase()); + const st = t ? tabTranslateState(t) : null; + const translateItems = []; + if (st?.translated) { + translateItems.push( + { label: `Show original (${languageNameFor(st.source || t.pageLang || "auto")})`, click: () => revertActiveTab() }, + { type: "separator" }, + ); + } else if (canTranslate) { + translateItems.push( + { label: `Translate this page from ${languageNameFor(t.pageLang)} to ${languageNameFor(translationTargetBase())}`, click: () => translateActiveTab() }, + { 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. const template = [ + ...translateItems, { label: `Automatic (${languageNameFor(osLoc)})${isAuto ? " ✓" : ""}`, click: () => setWebsiteLanguage("show") }, { type: "separator" }, - ...WEBSITE_LANGUAGE_QUICK.map((L) => ({ - label: `${L.label}${current === L.tag.toLowerCase() ? " ✓" : ""}`, - click: () => setWebsiteLanguage("manual", L.tag), - })), + ...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), + }; + }), { type: "separator" }, - { label: "More languages…", click: () => { try { openSettingsTab("general"); } catch {} } }, + { label: "More languages…", click: () => { try { openSettingsTab("language"); } catch {} } }, ]; const popup = Menu.buildFromTemplate(template); const chromeBounds = chrome ? chrome.getBounds() : { x: 0, y: 0 }; diff --git a/settings.html b/settings.html index 5b8d8bb8..52d8ce3d 100644 --- a/settings.html +++ b/settings.html @@ -373,7 +373,7 @@

Page translation

When the page's declared language is different from yours, Theseus can translate its visible text in place. The source stays untouched — click the chip again to revert.

-
Offer to translate
Light up a translate chip in the address bar when the page's language differs from yours.
+
Translate automatically
When the page's declared language is different from yours (and both are supported by the translator), Theseus translates it in place as soon as it loads. Turn this off to leave pages in their original language — the globe chip's menu still offers a one-click translation.
@@ -1174,12 +1174,22 @@ if (!webLangPick) return; let osLoc = "en-US"; try { osLoc = (C.systemLocale && await C.systemLocale()) || "en-US"; } catch {} - // Build options: "Automatic" first, then the quick list, then "Other…". - // No BCP-47 tag in visible labels — the tag surfaces only as the 2-letter - // chip in the URL bar. + // Supported-by-the-translator languages come enabled; the rest are + // shown but disabled with a "— coming later" suffix, so the picker is + // a roadmap, not a lie — the user sees which languages the chip will + // actually translate pages into today. "Other…" is still here for a + // user who needs to pin an unlisted BCP-47 tag for Accept-Language + // alone, but it's marked so they know translation won't follow. + const SUPPORTED = new Set(["en", "es", "fr", "de", "el", "ru"]); + const baseOf = (t) => String(t || "").split("-")[0].toLowerCase(); const opts = [``, - ...WEB_LANG_QUICK.map((L) => ``), - ``]; + ...WEB_LANG_QUICK.map((L) => { + const supported = SUPPORTED.has(baseOf(L.tag)); + return supported + ? `` + : ``; + }), + ``]; webLangPick.innerHTML = opts.join(""); const paint = (cur) => { const mode = cur.languageMode || "show";