diff --git a/bundled-addons/translate/addon.json b/bundled-addons/translate/addon.json index e25c98a9..4e6e65c8 100644 --- a/bundled-addons/translate/addon.json +++ b/bundled-addons/translate/addon.json @@ -1,7 +1,7 @@ { "id": "translate", "name": "Translate", - "version": "0.1.4", + "version": "0.1.5", "description": "Right-click a selection to translate it. Sidebar panel with LibreTranslate or Google as the backend.", "author": "Silent Mode", "icon": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0Ij4gPGNpcmNsZSBjeD0iMTIiIGN5PSIxMiIgcj0iMTEuNSIgZmlsbD0iIzFkNGVkOCIvPiA8cmVjdCB4PSIwIiB5PSIwLjYiIHdpZHRoPSIxMy40IiBoZWlnaHQ9IjEzLjQiIHJ4PSIyLjgiIGZpbGw9IiNmZmYiLz4gPHRleHQgeD0iNi43IiB5PSIxMi4xIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0ic3lzdGVtLXVpLFNlZ29lIFVJLHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTIuNCIgZm9udC13ZWlnaHQ9IjgwMCIgZmlsbD0iIzFkNGVkOCI+QTwvdGV4dD4gPHJlY3QgeD0iMTAuNiIgeT0iMTAiIHdpZHRoPSIxMy40IiBoZWlnaHQ9IjEzLjQiIHJ4PSIyLjgiIGZpbGw9IiMwYjEyMjAiIHN0cm9rZT0iI2ZmZiIgc3Ryb2tlLXdpZHRoPSIxLjEiLz4gPGcgc3Ryb2tlPSIjZmZmIiBzdHJva2Utd2lkdGg9IjIuMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBmaWxsPSJub25lIj4gPHBhdGggZD0iTTE3LjMgMTIuMnYxLjEiLz4gPHBhdGggZD0iTTEzLjUgMTQuNmg3LjYiLz4gPHBhdGggZD0iTTE4LjIgMTUuNyAxMy45IDIxIi8+IDxwYXRoIGQ9Ik0xNi41IDE1LjcgMjAuOCAyMSIvPiA8L2c+IDwvc3ZnPg==", diff --git a/bundled-addons/translate/panel.html b/bundled-addons/translate/panel.html index 0d909fa6..503fd742 100644 --- a/bundled-addons/translate/panel.html +++ b/bundled-addons/translate/panel.html @@ -186,14 +186,53 @@ ["sv","Swedish"],["th","Thai"],["tr","Turkish"],["uk","Ukrainian"], ["vi","Vietnamese"],["zh","Chinese"], ]; + const LANG_NAME = new Map(LANGS); + + // Most-used languages, pinned above the full list. Ranked by use count and + // capped at RECENT_MAX so the group stays a shortcut rather than a second + // copy of the alphabet. "auto" is excluded โ€” it is already the first row of + // the source select and is not a language anyone is "choosing". + const RECENT_MAX = 10; + function noteLangUse(code) { + if (!code || code === "auto") return; + const r = uiState.recent || (uiState.recent = {}); + r[code] = (r[code] || 0) + 1; + } + function topLangs() { + const r = uiState.recent || {}; + return Object.entries(r) + .filter(([c]) => LANG_NAME.has(c)) + .sort((a, b) => b[1] - a[1] || LANG_NAME.get(a[0]).localeCompare(LANG_NAME.get(b[0]))) + .slice(0, RECENT_MAX) + .map(([c]) => c); + } function fillSelect(el, includeAuto) { + const keep = el.value; el.innerHTML = ""; if (includeAuto) el.appendChild(new Option("Auto-detect", "auto")); - for (const [code, label] of LANGS) el.appendChild(new Option(label, code)); + const top = topLangs(); + if (top.length) { + const g = document.createElement("optgroup"); + g.label = "Frequently used"; + for (const c of top) g.appendChild(new Option(LANG_NAME.get(c), c)); + el.appendChild(g); + const all = document.createElement("optgroup"); + all.label = "All languages"; + for (const [code, label] of LANGS) all.appendChild(new Option(label, code)); + el.appendChild(all); + } else { + for (const [code, label] of LANGS) el.appendChild(new Option(label, code)); + } + // A code appears twice once it is in the pinned group; assigning the value + // selects the first match, which is the pinned one. That is what we want. + if (keep) el.value = keep; + } + function refillLangSelects() { + const s = srcSel.value, t = tgtSel.value; + fillSelect(srcSel, true); + fillSelect(tgtSel, false); + srcSel.value = s; tgtSel.value = t; } - fillSelect(srcSel, true); - fillSelect(tgtSel, false); - // Default target = user's browser language, falling back to English. const browserLang = (navigator.language || "en").split("-")[0].toLowerCase(); const defaultTarget = LANGS.some(([c]) => c === browserLang) && browserLang !== "en" ? browserLang : "en"; @@ -205,7 +244,12 @@ // current working default so the panel isn't broken out of the box. const DEAD_LT_URLS = new Set(["https://translate.argosopentech.com"]); const settingsState = { backend: "libretranslate", ltUrl: DEFAULT_LT_URL, ltKey: "" }; - const uiState = { src: "auto", tgt: defaultTarget, zoom: 100 }; + const uiState = { src: "auto", tgt: defaultTarget, zoom: 100, recent: {} }; + + // Populate the selects now that uiState exists โ€” fillSelect reads the + // frequency table to build the pinned group, so it cannot run before this. + fillSelect(srcSel, true); + fillSelect(tgtSel, false); // ---- text zoom --------------------------------------------------------- // Percent of the 15px base, clamped so the panes stay usable at either end. @@ -233,6 +277,9 @@ settingsState.ltUrl = DEFAULT_LT_URL; if (S) try { await S.set("settings", settingsState); } catch {} } + // Saved counts arrived, so the pinned group can be built for real. + if (!uiState.recent || typeof uiState.recent !== "object") uiState.recent = {}; + refillLangSelects(); backendSel.value = settingsState.backend; ltUrl.value = settingsState.ltUrl; ltKey.value = settingsState.ltKey; @@ -294,6 +341,14 @@ out.textContent = res.text || "(empty response)"; const via = res.via || settingsState.backend; setStatus(res.detected ? `${via} ยท detected ${res.detected}` : via, "ok"); + // Count only translations that actually landed. Counting on every + // dropdown change would rank languages the user skimmed past as highly + // as the ones they work in. A detected source counts as used too โ€” it + // is a language they read, even though they never picked it. + const before = topLangs().join(","); + noteLangUse(target); + noteLangUse(source === "auto" ? res.detected : source); + if (topLangs().join(",") !== before) refillLangSelects(); } catch (e) { out.textContent = ""; out.classList.add("empty");