theseus/bundled-addons/translate/panel.html

531 lines
25 KiB
HTML
Raw Normal View History

feat(theseus/translate): new sidebar add-on — right-click Translate selection Adds a bundled add-on `translate` with a sidebar panel + a right-click "Translate selection" menu item. Two swappable backends: - LibreTranslate (default) — free MIT engine; the panel's Settings tab lets the user point at any instance (public or self-hosted) and drop in an API key if one's required. - Google (unofficial free endpoint at translate.googleapis.com/ translate_a/single) — no key, wide coverage, but unofficial and Google can break it any time. Opt-in fallback. Flow: user selects text on a page, right-clicks -> "Translate selection". Add-on's context-menu handler stashes the selection under storage.__pending and calls api.revealSidebar("main"); the panel loads, drains __pending on first paint, and translates. Ctrl/Cmd+Enter in the input textarea also translates. Source + target language choices, browser-language default target, swap button, copy-to- clipboard on the output, settings gear. Depends on a new "context-menu-item" capability + api.revealSidebar hook in addons-host.js / main.js. Those wiring changes are prepared but not committed here — a parallel session is refactoring the same functions concurrently, so the safe path is to land translate/ first and let the wiring go in alongside the next host-facing commit. Until the wiring lands, the manifest's "context-menu-item" cap is silently dropped (per validateManifest's unknown-caps policy) and the sidebar panel + the panel's translation UI still work standalone — the right-click entry point is what's gated.
2026-09-20 17:50:06 +02:00
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Translate</title>
<style>
:root { color-scheme: light dark;
--bg:#0e131c; --panel:#141a24; --panel2:#18202c; --line:rgba(255,255,255,.09);
--ink:#e7eaf1; --mut:#8b98a9; --dim:#5e6678; --acid:#d6ff3d;
--btn:#1c2432; --btn-h:#242e40; }
@media (prefers-color-scheme: light) {
:root { --bg:#f8faff; --panel:#ffffff; --panel2:#f0f3fa; --line:rgba(0,0,0,.10);
--ink:#1a1f2b; --mut:#5c6577; --dim:#8a93a5;
--btn:#f0f3fa; --btn-h:#e3e8f2; }
}
* { box-sizing: border-box; }
html, body { margin: 0; height: 100%; }
body { background: var(--bg); color: var(--ink);
font: 13px/1.55 system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
display: flex; flex-direction: column; }
header { display: flex; align-items: center; justify-content: space-between;
padding: 10px 14px; border-bottom: 1px solid var(--line);
background: var(--panel); }
header .t { font-weight: 600; display: flex; gap: 8px; align-items: center; }
header .t .em { font-size: 15px; }
header .m { color: var(--dim); font-size: 11.5px; }
header .m.err { color: #ff9081; }
header .m.ok { color: var(--acid); }
translate 0.1.2 → 0.1.3: real icon, bigger text, zoom, auto-translate on language change Auto-translate: changing either language only called saveUi(), so the pane below kept showing the previous language's result with nothing to say it was stale — you had to notice and press Translate. Both selects now re-run the translation, guarded on empty input and on source === target (which would only echo the input back). Text size: the panes were 13.5px, small for reading a paragraph in a language you don't know well, which is the entire job. Base is now 15px, driven by a --tsize custom property so both panes stay matched. Zoom: −/+ either side of a percentage in the header, 70–220% in steps of 10, clamped with the buttons disabling at each end. Click the percentage to reset. Ctrl/Cmd with +, - or 0 does the same from either pane. Persists per-machine alongside the language choice. Icon: a globe with an A tile and a 文 tile, replacing the 🌐 emoji that was indistinguishable from every other globe in the dock. Drawn against the 16px and 18px rasterizations rather than at a comfortable size — the first pass used a font glyph for 文 and it turned to grey mush at 16px, so the strokes are hand-drawn paths thick enough to survive. Also drops the hardcoded icon in registerSidebarPanel, which would otherwise shadow the manifest's mark. Verified in a harness with a stubbed host API: language change fires exactly one request, the two guards fire none, zoom clamps and persists, and the layout holds at sidebar width.
2026-09-27 18:00:46 +02:00
header .t .em { display: inline-flex; }
header .t .em svg { display: block; width: 17px; height: 17px; }
.zoom { display: flex; align-items: center; gap: 2px; margin-left: 10px; }
.zoom button {
width: 20px; height: 20px; padding: 0; line-height: 1;
display: grid; place-items: center;
background: var(--btn); color: var(--mut);
border: 1px solid var(--line); border-radius: 5px;
cursor: pointer; font: inherit; font-size: 13px;
}
.zoom button:hover:not(:disabled) { background: var(--btn-h); color: var(--ink); }
.zoom button:disabled { opacity: .35; cursor: default; }
.zoom .pct {
min-width: 34px; text-align: center; color: var(--dim);
font-size: 10.5px; font-variant-numeric: tabular-nums; cursor: pointer;
}
.zoom .pct:hover { color: var(--ink); }
feat(theseus/translate): new sidebar add-on — right-click Translate selection Adds a bundled add-on `translate` with a sidebar panel + a right-click "Translate selection" menu item. Two swappable backends: - LibreTranslate (default) — free MIT engine; the panel's Settings tab lets the user point at any instance (public or self-hosted) and drop in an API key if one's required. - Google (unofficial free endpoint at translate.googleapis.com/ translate_a/single) — no key, wide coverage, but unofficial and Google can break it any time. Opt-in fallback. Flow: user selects text on a page, right-clicks -> "Translate selection". Add-on's context-menu handler stashes the selection under storage.__pending and calls api.revealSidebar("main"); the panel loads, drains __pending on first paint, and translates. Ctrl/Cmd+Enter in the input textarea also translates. Source + target language choices, browser-language default target, swap button, copy-to- clipboard on the output, settings gear. Depends on a new "context-menu-item" capability + api.revealSidebar hook in addons-host.js / main.js. Those wiring changes are prepared but not committed here — a parallel session is refactoring the same functions concurrently, so the safe path is to land translate/ first and let the wiring go in alongside the next host-facing commit. Until the wiring lands, the manifest's "context-menu-item" cap is silently dropped (per validateManifest's unknown-caps policy) and the sidebar panel + the panel's translation UI still work standalone — the right-click entry point is what's gated.
2026-09-20 17:50:06 +02:00
.langs { display: flex; gap: 6px; align-items: center;
padding: 8px 12px; border-bottom: 1px solid var(--line); background: var(--panel2); }
.langs select { flex: 1; min-width: 0; padding: 6px 8px; font: inherit;
background: var(--bg); color: var(--ink);
border: 1px solid var(--line); border-radius: 6px; }
.langs .swap { padding: 6px 8px; background: var(--btn); color: var(--ink);
border: 1px solid var(--line); border-radius: 6px; cursor: pointer; font: inherit; }
.langs .swap:hover { background: var(--btn-h); }
.body { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
.zone { display: flex; flex-direction: column; flex: 1; min-height: 0; }
.zone + .zone { border-top: 1px solid var(--line); }
.zone label { color: var(--dim); font-size: 10.5px; letter-spacing: .18em;
text-transform: uppercase; padding: 8px 14px 4px; display: flex;
justify-content: space-between; align-items: baseline; }
.zone label .action { color: var(--mut); font-size: 10.5px; cursor: pointer;
letter-spacing: normal; text-transform: none; }
.zone label .action:hover { color: var(--acid); }
translate 0.1.2 → 0.1.3: real icon, bigger text, zoom, auto-translate on language change Auto-translate: changing either language only called saveUi(), so the pane below kept showing the previous language's result with nothing to say it was stale — you had to notice and press Translate. Both selects now re-run the translation, guarded on empty input and on source === target (which would only echo the input back). Text size: the panes were 13.5px, small for reading a paragraph in a language you don't know well, which is the entire job. Base is now 15px, driven by a --tsize custom property so both panes stay matched. Zoom: −/+ either side of a percentage in the header, 70–220% in steps of 10, clamped with the buttons disabling at each end. Click the percentage to reset. Ctrl/Cmd with +, - or 0 does the same from either pane. Persists per-machine alongside the language choice. Icon: a globe with an A tile and a 文 tile, replacing the 🌐 emoji that was indistinguishable from every other globe in the dock. Drawn against the 16px and 18px rasterizations rather than at a comfortable size — the first pass used a font glyph for 文 and it turned to grey mush at 16px, so the strokes are hand-drawn paths thick enough to survive. Also drops the hardcoded icon in registerSidebarPanel, which would otherwise shadow the manifest's mark. Verified in a harness with a stubbed host API: language change fires exactly one request, the two guards fire none, zoom clamps and persists, and the layout holds at sidebar width.
2026-09-27 18:00:46 +02:00
/* --tsize drives both panes so zoom keeps them matched. 15px is the
default: the old 13.5px was small for reading a paragraph of a language
you don't know well, which is the whole job here. */
feat(theseus/translate): new sidebar add-on — right-click Translate selection Adds a bundled add-on `translate` with a sidebar panel + a right-click "Translate selection" menu item. Two swappable backends: - LibreTranslate (default) — free MIT engine; the panel's Settings tab lets the user point at any instance (public or self-hosted) and drop in an API key if one's required. - Google (unofficial free endpoint at translate.googleapis.com/ translate_a/single) — no key, wide coverage, but unofficial and Google can break it any time. Opt-in fallback. Flow: user selects text on a page, right-clicks -> "Translate selection". Add-on's context-menu handler stashes the selection under storage.__pending and calls api.revealSidebar("main"); the panel loads, drains __pending on first paint, and translates. Ctrl/Cmd+Enter in the input textarea also translates. Source + target language choices, browser-language default target, swap button, copy-to- clipboard on the output, settings gear. Depends on a new "context-menu-item" capability + api.revealSidebar hook in addons-host.js / main.js. Those wiring changes are prepared but not committed here — a parallel session is refactoring the same functions concurrently, so the safe path is to land translate/ first and let the wiring go in alongside the next host-facing commit. Until the wiring lands, the manifest's "context-menu-item" cap is silently dropped (per validateManifest's unknown-caps policy) and the sidebar panel + the panel's translation UI still work standalone — the right-click entry point is what's gated.
2026-09-20 17:50:06 +02:00
textarea, .output {
flex: 1; padding: 8px 14px 14px; border: none; outline: none; resize: none;
background: transparent; color: var(--ink);
translate 0.1.2 → 0.1.3: real icon, bigger text, zoom, auto-translate on language change Auto-translate: changing either language only called saveUi(), so the pane below kept showing the previous language's result with nothing to say it was stale — you had to notice and press Translate. Both selects now re-run the translation, guarded on empty input and on source === target (which would only echo the input back). Text size: the panes were 13.5px, small for reading a paragraph in a language you don't know well, which is the entire job. Base is now 15px, driven by a --tsize custom property so both panes stay matched. Zoom: −/+ either side of a percentage in the header, 70–220% in steps of 10, clamped with the buttons disabling at each end. Click the percentage to reset. Ctrl/Cmd with +, - or 0 does the same from either pane. Persists per-machine alongside the language choice. Icon: a globe with an A tile and a 文 tile, replacing the 🌐 emoji that was indistinguishable from every other globe in the dock. Drawn against the 16px and 18px rasterizations rather than at a comfortable size — the first pass used a font glyph for 文 and it turned to grey mush at 16px, so the strokes are hand-drawn paths thick enough to survive. Also drops the hardcoded icon in registerSidebarPanel, which would otherwise shadow the manifest's mark. Verified in a harness with a stubbed host API: language change fires exactly one request, the two guards fire none, zoom clamps and persists, and the layout holds at sidebar width.
2026-09-27 18:00:46 +02:00
font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
font-size: var(--tsize, 15px); line-height: 1.55;
feat(theseus/translate): new sidebar add-on — right-click Translate selection Adds a bundled add-on `translate` with a sidebar panel + a right-click "Translate selection" menu item. Two swappable backends: - LibreTranslate (default) — free MIT engine; the panel's Settings tab lets the user point at any instance (public or self-hosted) and drop in an API key if one's required. - Google (unofficial free endpoint at translate.googleapis.com/ translate_a/single) — no key, wide coverage, but unofficial and Google can break it any time. Opt-in fallback. Flow: user selects text on a page, right-clicks -> "Translate selection". Add-on's context-menu handler stashes the selection under storage.__pending and calls api.revealSidebar("main"); the panel loads, drains __pending on first paint, and translates. Ctrl/Cmd+Enter in the input textarea also translates. Source + target language choices, browser-language default target, swap button, copy-to- clipboard on the output, settings gear. Depends on a new "context-menu-item" capability + api.revealSidebar hook in addons-host.js / main.js. Those wiring changes are prepared but not committed here — a parallel session is refactoring the same functions concurrently, so the safe path is to land translate/ first and let the wiring go in alongside the next host-facing commit. Until the wiring lands, the manifest's "context-menu-item" cap is silently dropped (per validateManifest's unknown-caps policy) and the sidebar panel + the panel's translation UI still work standalone — the right-click entry point is what's gated.
2026-09-20 17:50:06 +02:00
overflow-y: auto; white-space: pre-wrap; word-wrap: break-word;
}
textarea::placeholder { color: var(--dim); }
.output.empty { color: var(--dim); font-style: italic; }
.actions { display: flex; gap: 6px; padding: 8px 12px;
border-top: 1px solid var(--line); background: var(--panel); }
.actions .btn {
flex: 1; padding: 8px 10px; border: 1px solid var(--line); border-radius: 8px;
background: var(--btn); color: var(--ink); cursor: pointer; font: inherit;
}
.actions .btn.primary { background: var(--acid); color: #101418; border-color: transparent; font-weight: 600; }
.actions .btn:hover:not(:disabled) { background: var(--btn-h); }
.actions .btn.primary:hover:not(:disabled) { filter: brightness(1.05); }
.actions .btn:disabled { opacity: .45; cursor: default; }
.actions .btn.gear { flex: 0 0 auto; padding: 8px 10px; }
.settings {
display: none; padding: 10px 14px; background: var(--panel2);
border-top: 1px solid var(--line); font-size: 12px; color: var(--mut);
flex-direction: column; gap: 8px;
}
.settings.open { display: flex; }
.settings label { display: flex; flex-direction: column; gap: 4px; color: var(--dim); font-size: 11px; }
.settings input, .settings select { padding: 6px 8px; font: inherit;
background: var(--bg); color: var(--ink);
border: 1px solid var(--line); border-radius: 6px; }
.settings .hint { color: var(--dim); font-size: 11px; line-height: 1.5; }
.settings .hint code { background: var(--bg); border: 1px solid var(--line);
padding: 1px 4px; border-radius: 3px; font-size: 10.5px; }
</style>
</head>
<body>
<header>
translate 0.1.2 → 0.1.3: real icon, bigger text, zoom, auto-translate on language change Auto-translate: changing either language only called saveUi(), so the pane below kept showing the previous language's result with nothing to say it was stale — you had to notice and press Translate. Both selects now re-run the translation, guarded on empty input and on source === target (which would only echo the input back). Text size: the panes were 13.5px, small for reading a paragraph in a language you don't know well, which is the entire job. Base is now 15px, driven by a --tsize custom property so both panes stay matched. Zoom: −/+ either side of a percentage in the header, 70–220% in steps of 10, clamped with the buttons disabling at each end. Click the percentage to reset. Ctrl/Cmd with +, - or 0 does the same from either pane. Persists per-machine alongside the language choice. Icon: a globe with an A tile and a 文 tile, replacing the 🌐 emoji that was indistinguishable from every other globe in the dock. Drawn against the 16px and 18px rasterizations rather than at a comfortable size — the first pass used a font glyph for 文 and it turned to grey mush at 16px, so the strokes are hand-drawn paths thick enough to survive. Also drops the hardcoded icon in registerSidebarPanel, which would otherwise shadow the manifest's mark. Verified in a harness with a stubbed host API: language change fires exactly one request, the two guards fire none, zoom clamps and persists, and the layout holds at sidebar width.
2026-09-27 18:00:46 +02:00
<div class="t">
<span class="em" aria-hidden="true"><svg viewBox="0 0 24 24" width="24" height="24"> <circle cx="12" cy="12" r="11.5" fill="#1d4ed8"/> <rect x="0" y="0.6" width="13.4" height="13.4" rx="2.8" fill="#fff"/> <text x="6.7" y="12.1" text-anchor="middle" font-family="system-ui,Segoe UI,sans-serif" font-size="12.4" font-weight="800" fill="#1d4ed8">A</text> <rect x="10.6" y="10" width="13.4" height="13.4" rx="2.8" fill="#0b1220" stroke="#fff" stroke-width="1.1"/> <g stroke="#fff" stroke-width="2.2" stroke-linecap="round" fill="none"> <path d="M17.3 12.2v1.1"/> <path d="M13.5 14.6h7.6"/> <path d="M18.2 15.7 13.9 21"/> <path d="M16.5 15.7 20.8 21"/> </g> </svg></span>
translate 0.1.2 → 0.1.3: real icon, bigger text, zoom, auto-translate on language change Auto-translate: changing either language only called saveUi(), so the pane below kept showing the previous language's result with nothing to say it was stale — you had to notice and press Translate. Both selects now re-run the translation, guarded on empty input and on source === target (which would only echo the input back). Text size: the panes were 13.5px, small for reading a paragraph in a language you don't know well, which is the entire job. Base is now 15px, driven by a --tsize custom property so both panes stay matched. Zoom: −/+ either side of a percentage in the header, 70–220% in steps of 10, clamped with the buttons disabling at each end. Click the percentage to reset. Ctrl/Cmd with +, - or 0 does the same from either pane. Persists per-machine alongside the language choice. Icon: a globe with an A tile and a 文 tile, replacing the 🌐 emoji that was indistinguishable from every other globe in the dock. Drawn against the 16px and 18px rasterizations rather than at a comfortable size — the first pass used a font glyph for 文 and it turned to grey mush at 16px, so the strokes are hand-drawn paths thick enough to survive. Also drops the hardcoded icon in registerSidebarPanel, which would otherwise shadow the manifest's mark. Verified in a harness with a stubbed host API: language change fires exactly one request, the two guards fire none, zoom clamps and persists, and the layout holds at sidebar width.
2026-09-27 18:00:46 +02:00
<span>Translate</span>
<span class="zoom">
<button id="zoomOut" title="Smaller text (Ctrl+-)" aria-label="Smaller text">−</button>
<span class="pct" id="zoomPct" title="Reset to 100%" role="button" tabindex="0">100%</span>
<button id="zoomIn" title="Larger text (Ctrl++)" aria-label="Larger text">+</button>
</span>
</div>
feat(theseus/translate): new sidebar add-on — right-click Translate selection Adds a bundled add-on `translate` with a sidebar panel + a right-click "Translate selection" menu item. Two swappable backends: - LibreTranslate (default) — free MIT engine; the panel's Settings tab lets the user point at any instance (public or self-hosted) and drop in an API key if one's required. - Google (unofficial free endpoint at translate.googleapis.com/ translate_a/single) — no key, wide coverage, but unofficial and Google can break it any time. Opt-in fallback. Flow: user selects text on a page, right-clicks -> "Translate selection". Add-on's context-menu handler stashes the selection under storage.__pending and calls api.revealSidebar("main"); the panel loads, drains __pending on first paint, and translates. Ctrl/Cmd+Enter in the input textarea also translates. Source + target language choices, browser-language default target, swap button, copy-to- clipboard on the output, settings gear. Depends on a new "context-menu-item" capability + api.revealSidebar hook in addons-host.js / main.js. Those wiring changes are prepared but not committed here — a parallel session is refactoring the same functions concurrently, so the safe path is to land translate/ first and let the wiring go in alongside the next host-facing commit. Until the wiring lands, the manifest's "context-menu-item" cap is silently dropped (per validateManifest's unknown-caps policy) and the sidebar panel + the panel's translation UI still work standalone — the right-click entry point is what's gated.
2026-09-20 17:50:06 +02:00
<div class="m" id="status">ready</div>
</header>
<div class="langs">
<select id="src" title="Source language"></select>
<button class="swap" id="swap" title="Swap languages">⇄</button>
<select id="tgt" title="Target language"></select>
</div>
<div class="body">
<div class="zone">
<label>Source <span class="action" id="clearIn">clear</span></label>
<textarea id="in" placeholder="Right-click a selection on a page, or paste text here." spellcheck="false"></textarea>
</div>
<div class="zone">
<label>Translation <span class="action" id="copyOut">copy</span></label>
<div class="output empty" id="out">The translation will appear here.</div>
</div>
</div>
<div class="actions">
<button class="btn primary" id="go">Translate</button>
<button class="btn gear" id="gear" title="Settings">⚙</button>
</div>
<div class="settings" id="settings">
<label>
Backend
<select id="backend">
<option value="libretranslate">LibreTranslate</option>
<option value="google">Google (unofficial free endpoint)</option>
</select>
</label>
<label id="ltUrlLabel">
LibreTranslate server URL
<input id="ltUrl" type="url" placeholder="https://translate.disroot.org" list="ltMirrors" />
<datalist id="ltMirrors">
<option value="https://translate.disroot.org"></option>
<option value="https://translate.plausibility.cloud"></option>
<option value="https://lingva.ml"></option>
</datalist>
feat(theseus/translate): new sidebar add-on — right-click Translate selection Adds a bundled add-on `translate` with a sidebar panel + a right-click "Translate selection" menu item. Two swappable backends: - LibreTranslate (default) — free MIT engine; the panel's Settings tab lets the user point at any instance (public or self-hosted) and drop in an API key if one's required. - Google (unofficial free endpoint at translate.googleapis.com/ translate_a/single) — no key, wide coverage, but unofficial and Google can break it any time. Opt-in fallback. Flow: user selects text on a page, right-clicks -> "Translate selection". Add-on's context-menu handler stashes the selection under storage.__pending and calls api.revealSidebar("main"); the panel loads, drains __pending on first paint, and translates. Ctrl/Cmd+Enter in the input textarea also translates. Source + target language choices, browser-language default target, swap button, copy-to- clipboard on the output, settings gear. Depends on a new "context-menu-item" capability + api.revealSidebar hook in addons-host.js / main.js. Those wiring changes are prepared but not committed here — a parallel session is refactoring the same functions concurrently, so the safe path is to land translate/ first and let the wiring go in alongside the next host-facing commit. Until the wiring lands, the manifest's "context-menu-item" cap is silently dropped (per validateManifest's unknown-caps policy) and the sidebar panel + the panel's translation UI still work standalone — the right-click entry point is what's gated.
2026-09-20 17:50:06 +02:00
</label>
<label id="ltKeyLabel">
LibreTranslate API key (optional)
<input id="ltKey" type="text" placeholder="only if your instance requires one" />
</label>
<div class="hint">
LibreTranslate is a free MIT-licensed engine. Public instances rate-limit — if you
hit "too many requests" or a mirror is down, pick another URL from the list or
self-host. Google (unofficial) uses
<code>translate.googleapis.com/translate_a/single</code> — no key, but unofficial
and Google can break it at any time.
feat(theseus/translate): new sidebar add-on — right-click Translate selection Adds a bundled add-on `translate` with a sidebar panel + a right-click "Translate selection" menu item. Two swappable backends: - LibreTranslate (default) — free MIT engine; the panel's Settings tab lets the user point at any instance (public or self-hosted) and drop in an API key if one's required. - Google (unofficial free endpoint at translate.googleapis.com/ translate_a/single) — no key, wide coverage, but unofficial and Google can break it any time. Opt-in fallback. Flow: user selects text on a page, right-clicks -> "Translate selection". Add-on's context-menu handler stashes the selection under storage.__pending and calls api.revealSidebar("main"); the panel loads, drains __pending on first paint, and translates. Ctrl/Cmd+Enter in the input textarea also translates. Source + target language choices, browser-language default target, swap button, copy-to- clipboard on the output, settings gear. Depends on a new "context-menu-item" capability + api.revealSidebar hook in addons-host.js / main.js. Those wiring changes are prepared but not committed here — a parallel session is refactoring the same functions concurrently, so the safe path is to land translate/ first and let the wiring go in alongside the next host-facing commit. Until the wiring lands, the manifest's "context-menu-item" cap is silently dropped (per validateManifest's unknown-caps policy) and the sidebar panel + the panel's translation UI still work standalone — the right-click entry point is what's gated.
2026-09-20 17:50:06 +02:00
</div>
</div>
<script>
const $ = (id) => document.getElementById(id);
const inp = $("in"), out = $("out"), status = $("status");
const srcSel = $("src"), tgtSel = $("tgt");
const goBtn = $("go"), swapBtn = $("swap"), gearBtn = $("gear");
const settings = $("settings");
const backendSel = $("backend"), ltUrl = $("ltUrl"), ltKey = $("ltKey");
const ltUrlLabel = $("ltUrlLabel"), ltKeyLabel = $("ltKeyLabel");
// ISO-639-1 short list. First `auto` for source only. Users can add more
// via settings later; for v1 this covers ~99% of use.
const LANGS = [
["ar","Arabic"],["bg","Bulgarian"],["cs","Czech"],["da","Danish"],
["de","German"],["el","Greek"],["en","English"],["es","Spanish"],
["et","Estonian"],["fi","Finnish"],["fr","French"],["he","Hebrew"],
["hi","Hindi"],["hu","Hungarian"],["id","Indonesian"],["it","Italian"],
["ja","Japanese"],["ko","Korean"],["lt","Lithuanian"],["lv","Latvian"],
["nb","Norwegian"],["nl","Dutch"],["pl","Polish"],["pt","Portuguese"],
["ro","Romanian"],["ru","Russian"],["sk","Slovak"],["sl","Slovenian"],
["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);
}
translate 0.1.5 → 0.1.6: name the detected language in the source select Auto-detect already worked — the backend returns detectedLanguage:{language,confidence} and the panel put it in the status line. But that only appeared after a translation had already run, in small text, away from the control that raised the question. You could not tell what "Auto-detect" had decided before committing to it. The first row of the source select now says "Auto-detect · German", and it says so while you are still typing. Detection runs on its own via LibreTranslate's /detect, debounced 700 ms and gated at 12 characters, because a detector given two words is guessing and firing per keystroke would pound a public mirror for nothing. It chains the same mirror fallback as translation, so a dead primary does not make detection look broken while translating still works. Confidence below 60 renders as "German?" rather than silently asserting a coin-flip. The Google backend has no detect-only route, so there doDetect returns null instead of burning a request, and the label is filled from the translation response — which every backend returns anyway, so a skipped or failed detect is never worse than before. Detection retires when a source is named explicitly, comes back on returning to Auto-detect, and is wiped by clear. A right-click selection schedules one too, since that text arrives with no keystroke. Verified against the real mirror (de/fr/ja at 100/100/90%) and in the harness: short text fires nothing, long text fires once, four rapid edits debounce to one call, and every transition above lands. Also adds the xray removal script used to take the old engine off all three exits now that they run sing-box.
2026-09-28 21:50:00 +02:00
// What the source select's first row says. Once a language has been
// detected it names it inline — "Auto-detect · German" — so the answer is
// visible where the question was asked, instead of only in the status line
// after a translation has already happened.
let detectedCode = null, detectedConfidence = null;
function autoLabel() {
if (!detectedCode) return "Auto-detect";
const name = LANG_NAME.get(detectedCode) || detectedCode;
const low = detectedConfidence !== null && detectedConfidence < 60;
return `Auto-detect · ${name}${low ? "?" : ""}`;
}
function paintAutoLabel() {
const opt = srcSel.querySelector('option[value="auto"]');
if (opt) opt.textContent = autoLabel();
}
feat(theseus/translate): new sidebar add-on — right-click Translate selection Adds a bundled add-on `translate` with a sidebar panel + a right-click "Translate selection" menu item. Two swappable backends: - LibreTranslate (default) — free MIT engine; the panel's Settings tab lets the user point at any instance (public or self-hosted) and drop in an API key if one's required. - Google (unofficial free endpoint at translate.googleapis.com/ translate_a/single) — no key, wide coverage, but unofficial and Google can break it any time. Opt-in fallback. Flow: user selects text on a page, right-clicks -> "Translate selection". Add-on's context-menu handler stashes the selection under storage.__pending and calls api.revealSidebar("main"); the panel loads, drains __pending on first paint, and translates. Ctrl/Cmd+Enter in the input textarea also translates. Source + target language choices, browser-language default target, swap button, copy-to- clipboard on the output, settings gear. Depends on a new "context-menu-item" capability + api.revealSidebar hook in addons-host.js / main.js. Those wiring changes are prepared but not committed here — a parallel session is refactoring the same functions concurrently, so the safe path is to land translate/ first and let the wiring go in alongside the next host-facing commit. Until the wiring lands, the manifest's "context-menu-item" cap is silently dropped (per validateManifest's unknown-caps policy) and the sidebar panel + the panel's translation UI still work standalone — the right-click entry point is what's gated.
2026-09-20 17:50:06 +02:00
function fillSelect(el, includeAuto) {
const keep = el.value;
feat(theseus/translate): new sidebar add-on — right-click Translate selection Adds a bundled add-on `translate` with a sidebar panel + a right-click "Translate selection" menu item. Two swappable backends: - LibreTranslate (default) — free MIT engine; the panel's Settings tab lets the user point at any instance (public or self-hosted) and drop in an API key if one's required. - Google (unofficial free endpoint at translate.googleapis.com/ translate_a/single) — no key, wide coverage, but unofficial and Google can break it any time. Opt-in fallback. Flow: user selects text on a page, right-clicks -> "Translate selection". Add-on's context-menu handler stashes the selection under storage.__pending and calls api.revealSidebar("main"); the panel loads, drains __pending on first paint, and translates. Ctrl/Cmd+Enter in the input textarea also translates. Source + target language choices, browser-language default target, swap button, copy-to- clipboard on the output, settings gear. Depends on a new "context-menu-item" capability + api.revealSidebar hook in addons-host.js / main.js. Those wiring changes are prepared but not committed here — a parallel session is refactoring the same functions concurrently, so the safe path is to land translate/ first and let the wiring go in alongside the next host-facing commit. Until the wiring lands, the manifest's "context-menu-item" cap is silently dropped (per validateManifest's unknown-caps policy) and the sidebar panel + the panel's translation UI still work standalone — the right-click entry point is what's gated.
2026-09-20 17:50:06 +02:00
el.innerHTML = "";
translate 0.1.5 → 0.1.6: name the detected language in the source select Auto-detect already worked — the backend returns detectedLanguage:{language,confidence} and the panel put it in the status line. But that only appeared after a translation had already run, in small text, away from the control that raised the question. You could not tell what "Auto-detect" had decided before committing to it. The first row of the source select now says "Auto-detect · German", and it says so while you are still typing. Detection runs on its own via LibreTranslate's /detect, debounced 700 ms and gated at 12 characters, because a detector given two words is guessing and firing per keystroke would pound a public mirror for nothing. It chains the same mirror fallback as translation, so a dead primary does not make detection look broken while translating still works. Confidence below 60 renders as "German?" rather than silently asserting a coin-flip. The Google backend has no detect-only route, so there doDetect returns null instead of burning a request, and the label is filled from the translation response — which every backend returns anyway, so a skipped or failed detect is never worse than before. Detection retires when a source is named explicitly, comes back on returning to Auto-detect, and is wiped by clear. A right-click selection schedules one too, since that text arrives with no keystroke. Verified against the real mirror (de/fr/ja at 100/100/90%) and in the harness: short text fires nothing, long text fires once, four rapid edits debounce to one call, and every transition above lands. Also adds the xray removal script used to take the old engine off all three exits now that they run sing-box.
2026-09-28 21:50:00 +02:00
if (includeAuto) el.appendChild(new Option(autoLabel(), "auto"));
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;
feat(theseus/translate): new sidebar add-on — right-click Translate selection Adds a bundled add-on `translate` with a sidebar panel + a right-click "Translate selection" menu item. Two swappable backends: - LibreTranslate (default) — free MIT engine; the panel's Settings tab lets the user point at any instance (public or self-hosted) and drop in an API key if one's required. - Google (unofficial free endpoint at translate.googleapis.com/ translate_a/single) — no key, wide coverage, but unofficial and Google can break it any time. Opt-in fallback. Flow: user selects text on a page, right-clicks -> "Translate selection". Add-on's context-menu handler stashes the selection under storage.__pending and calls api.revealSidebar("main"); the panel loads, drains __pending on first paint, and translates. Ctrl/Cmd+Enter in the input textarea also translates. Source + target language choices, browser-language default target, swap button, copy-to- clipboard on the output, settings gear. Depends on a new "context-menu-item" capability + api.revealSidebar hook in addons-host.js / main.js. Those wiring changes are prepared but not committed here — a parallel session is refactoring the same functions concurrently, so the safe path is to land translate/ first and let the wiring go in alongside the next host-facing commit. Until the wiring lands, the manifest's "context-menu-item" cap is silently dropped (per validateManifest's unknown-caps policy) and the sidebar panel + the panel's translation UI still work standalone — the right-click entry point is what's gated.
2026-09-20 17:50:06 +02:00
}
// 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";
const S = window.silentmode?.storage;
const DEFAULT_LT_URL = "https://translate.disroot.org";
// Old defaults that we now migrate off — argosopentech.com went offline.
// If a user's saved URL matches one of these, silently upgrade it to the
// 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, 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);
translate 0.1.2 → 0.1.3: real icon, bigger text, zoom, auto-translate on language change Auto-translate: changing either language only called saveUi(), so the pane below kept showing the previous language's result with nothing to say it was stale — you had to notice and press Translate. Both selects now re-run the translation, guarded on empty input and on source === target (which would only echo the input back). Text size: the panes were 13.5px, small for reading a paragraph in a language you don't know well, which is the entire job. Base is now 15px, driven by a --tsize custom property so both panes stay matched. Zoom: −/+ either side of a percentage in the header, 70–220% in steps of 10, clamped with the buttons disabling at each end. Click the percentage to reset. Ctrl/Cmd with +, - or 0 does the same from either pane. Persists per-machine alongside the language choice. Icon: a globe with an A tile and a 文 tile, replacing the 🌐 emoji that was indistinguishable from every other globe in the dock. Drawn against the 16px and 18px rasterizations rather than at a comfortable size — the first pass used a font glyph for 文 and it turned to grey mush at 16px, so the strokes are hand-drawn paths thick enough to survive. Also drops the hardcoded icon in registerSidebarPanel, which would otherwise shadow the manifest's mark. Verified in a harness with a stubbed host API: language change fires exactly one request, the two guards fire none, zoom clamps and persists, and the layout holds at sidebar width.
2026-09-27 18:00:46 +02:00
// ---- text zoom ---------------------------------------------------------
// Percent of the 15px base, clamped so the panes stay usable at either end.
const ZOOM_MIN = 70, ZOOM_MAX = 220, ZOOM_STEP = 10, ZOOM_BASE = 15;
function applyZoom() {
const z = Math.min(ZOOM_MAX, Math.max(ZOOM_MIN, uiState.zoom || 100));
uiState.zoom = z;
document.documentElement.style.setProperty("--tsize", (ZOOM_BASE * z / 100).toFixed(2) + "px");
$("zoomPct").textContent = z + "%";
$("zoomOut").disabled = z <= ZOOM_MIN;
$("zoomIn").disabled = z >= ZOOM_MAX;
}
function setZoom(z) { uiState.zoom = z; applyZoom(); saveUi(); }
feat(theseus/translate): new sidebar add-on — right-click Translate selection Adds a bundled add-on `translate` with a sidebar panel + a right-click "Translate selection" menu item. Two swappable backends: - LibreTranslate (default) — free MIT engine; the panel's Settings tab lets the user point at any instance (public or self-hosted) and drop in an API key if one's required. - Google (unofficial free endpoint at translate.googleapis.com/ translate_a/single) — no key, wide coverage, but unofficial and Google can break it any time. Opt-in fallback. Flow: user selects text on a page, right-clicks -> "Translate selection". Add-on's context-menu handler stashes the selection under storage.__pending and calls api.revealSidebar("main"); the panel loads, drains __pending on first paint, and translates. Ctrl/Cmd+Enter in the input textarea also translates. Source + target language choices, browser-language default target, swap button, copy-to- clipboard on the output, settings gear. Depends on a new "context-menu-item" capability + api.revealSidebar hook in addons-host.js / main.js. Those wiring changes are prepared but not committed here — a parallel session is refactoring the same functions concurrently, so the safe path is to land translate/ first and let the wiring go in alongside the next host-facing commit. Until the wiring lands, the manifest's "context-menu-item" cap is silently dropped (per validateManifest's unknown-caps policy) and the sidebar panel + the panel's translation UI still work standalone — the right-click entry point is what's gated.
2026-09-20 17:50:06 +02:00
async function loadState() {
if (!S) return;
try {
const saved = await S.get("settings", null);
if (saved && typeof saved === "object") Object.assign(settingsState, saved);
const ui = await S.get("ui", null);
if (ui && typeof ui === "object") Object.assign(uiState, ui);
} catch (e) { console.warn("load state failed:", e); }
// One-shot migration off a dead default URL.
if (DEAD_LT_URLS.has(settingsState.ltUrl)) {
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();
feat(theseus/translate): new sidebar add-on — right-click Translate selection Adds a bundled add-on `translate` with a sidebar panel + a right-click "Translate selection" menu item. Two swappable backends: - LibreTranslate (default) — free MIT engine; the panel's Settings tab lets the user point at any instance (public or self-hosted) and drop in an API key if one's required. - Google (unofficial free endpoint at translate.googleapis.com/ translate_a/single) — no key, wide coverage, but unofficial and Google can break it any time. Opt-in fallback. Flow: user selects text on a page, right-clicks -> "Translate selection". Add-on's context-menu handler stashes the selection under storage.__pending and calls api.revealSidebar("main"); the panel loads, drains __pending on first paint, and translates. Ctrl/Cmd+Enter in the input textarea also translates. Source + target language choices, browser-language default target, swap button, copy-to- clipboard on the output, settings gear. Depends on a new "context-menu-item" capability + api.revealSidebar hook in addons-host.js / main.js. Those wiring changes are prepared but not committed here — a parallel session is refactoring the same functions concurrently, so the safe path is to land translate/ first and let the wiring go in alongside the next host-facing commit. Until the wiring lands, the manifest's "context-menu-item" cap is silently dropped (per validateManifest's unknown-caps policy) and the sidebar panel + the panel's translation UI still work standalone — the right-click entry point is what's gated.
2026-09-20 17:50:06 +02:00
backendSel.value = settingsState.backend;
ltUrl.value = settingsState.ltUrl;
ltKey.value = settingsState.ltKey;
srcSel.value = uiState.src;
tgtSel.value = uiState.tgt;
translate 0.1.2 → 0.1.3: real icon, bigger text, zoom, auto-translate on language change Auto-translate: changing either language only called saveUi(), so the pane below kept showing the previous language's result with nothing to say it was stale — you had to notice and press Translate. Both selects now re-run the translation, guarded on empty input and on source === target (which would only echo the input back). Text size: the panes were 13.5px, small for reading a paragraph in a language you don't know well, which is the entire job. Base is now 15px, driven by a --tsize custom property so both panes stay matched. Zoom: −/+ either side of a percentage in the header, 70–220% in steps of 10, clamped with the buttons disabling at each end. Click the percentage to reset. Ctrl/Cmd with +, - or 0 does the same from either pane. Persists per-machine alongside the language choice. Icon: a globe with an A tile and a 文 tile, replacing the 🌐 emoji that was indistinguishable from every other globe in the dock. Drawn against the 16px and 18px rasterizations rather than at a comfortable size — the first pass used a font glyph for 文 and it turned to grey mush at 16px, so the strokes are hand-drawn paths thick enough to survive. Also drops the hardcoded icon in registerSidebarPanel, which would otherwise shadow the manifest's mark. Verified in a harness with a stubbed host API: language change fires exactly one request, the two guards fire none, zoom clamps and persists, and the layout holds at sidebar width.
2026-09-27 18:00:46 +02:00
applyZoom();
feat(theseus/translate): new sidebar add-on — right-click Translate selection Adds a bundled add-on `translate` with a sidebar panel + a right-click "Translate selection" menu item. Two swappable backends: - LibreTranslate (default) — free MIT engine; the panel's Settings tab lets the user point at any instance (public or self-hosted) and drop in an API key if one's required. - Google (unofficial free endpoint at translate.googleapis.com/ translate_a/single) — no key, wide coverage, but unofficial and Google can break it any time. Opt-in fallback. Flow: user selects text on a page, right-clicks -> "Translate selection". Add-on's context-menu handler stashes the selection under storage.__pending and calls api.revealSidebar("main"); the panel loads, drains __pending on first paint, and translates. Ctrl/Cmd+Enter in the input textarea also translates. Source + target language choices, browser-language default target, swap button, copy-to- clipboard on the output, settings gear. Depends on a new "context-menu-item" capability + api.revealSidebar hook in addons-host.js / main.js. Those wiring changes are prepared but not committed here — a parallel session is refactoring the same functions concurrently, so the safe path is to land translate/ first and let the wiring go in alongside the next host-facing commit. Until the wiring lands, the manifest's "context-menu-item" cap is silently dropped (per validateManifest's unknown-caps policy) and the sidebar panel + the panel's translation UI still work standalone — the right-click entry point is what's gated.
2026-09-20 17:50:06 +02:00
onBackendChange();
}
async function saveSettings() {
settingsState.backend = backendSel.value;
settingsState.ltUrl = ltUrl.value.trim().replace(/\/+$/, "") || DEFAULT_LT_URL;
feat(theseus/translate): new sidebar add-on — right-click Translate selection Adds a bundled add-on `translate` with a sidebar panel + a right-click "Translate selection" menu item. Two swappable backends: - LibreTranslate (default) — free MIT engine; the panel's Settings tab lets the user point at any instance (public or self-hosted) and drop in an API key if one's required. - Google (unofficial free endpoint at translate.googleapis.com/ translate_a/single) — no key, wide coverage, but unofficial and Google can break it any time. Opt-in fallback. Flow: user selects text on a page, right-clicks -> "Translate selection". Add-on's context-menu handler stashes the selection under storage.__pending and calls api.revealSidebar("main"); the panel loads, drains __pending on first paint, and translates. Ctrl/Cmd+Enter in the input textarea also translates. Source + target language choices, browser-language default target, swap button, copy-to- clipboard on the output, settings gear. Depends on a new "context-menu-item" capability + api.revealSidebar hook in addons-host.js / main.js. Those wiring changes are prepared but not committed here — a parallel session is refactoring the same functions concurrently, so the safe path is to land translate/ first and let the wiring go in alongside the next host-facing commit. Until the wiring lands, the manifest's "context-menu-item" cap is silently dropped (per validateManifest's unknown-caps policy) and the sidebar panel + the panel's translation UI still work standalone — the right-click entry point is what's gated.
2026-09-20 17:50:06 +02:00
settingsState.ltKey = ltKey.value.trim();
if (S) try { await S.set("settings", settingsState); } catch {}
}
async function saveUi() {
uiState.src = srcSel.value;
uiState.tgt = tgtSel.value;
if (S) try { await S.set("ui", uiState); } catch {}
}
function onBackendChange() {
const isLt = backendSel.value === "libretranslate";
ltUrlLabel.style.display = isLt ? "" : "none";
ltKeyLabel.style.display = isLt ? "" : "none";
}
translate 0.1.5 → 0.1.6: name the detected language in the source select Auto-detect already worked — the backend returns detectedLanguage:{language,confidence} and the panel put it in the status line. But that only appeared after a translation had already run, in small text, away from the control that raised the question. You could not tell what "Auto-detect" had decided before committing to it. The first row of the source select now says "Auto-detect · German", and it says so while you are still typing. Detection runs on its own via LibreTranslate's /detect, debounced 700 ms and gated at 12 characters, because a detector given two words is guessing and firing per keystroke would pound a public mirror for nothing. It chains the same mirror fallback as translation, so a dead primary does not make detection look broken while translating still works. Confidence below 60 renders as "German?" rather than silently asserting a coin-flip. The Google backend has no detect-only route, so there doDetect returns null instead of burning a request, and the label is filled from the translation response — which every backend returns anyway, so a skipped or failed detect is never worse than before. Detection retires when a source is named explicitly, comes back on returning to Auto-detect, and is wiped by clear. A right-click selection schedules one too, since that text arrives with no keystroke. Verified against the real mirror (de/fr/ja at 100/100/90%) and in the harness: short text fires nothing, long text fires once, four rapid edits debounce to one call, and every transition above lands. Also adds the xray removal script used to take the old engine off all three exits now that they run sing-box.
2026-09-28 21:50:00 +02:00
// Detection runs on the text as it settles, so the source language is named
// before Translate is pressed. Debounced and length-gated: a couple of words
// is not enough for a detector to be right, and firing per keystroke would
// hammer a public mirror for no benefit. Nothing here blocks translating —
// a failed or slow detect just leaves the label alone.
const DETECT_DEBOUNCE_MS = 700, DETECT_MIN_CHARS = 12;
let detectTimer = null, detectSeq = 0;
function setDetected(code, confidence) {
if (code === detectedCode && confidence === detectedConfidence) return;
detectedCode = code || null;
detectedConfidence = typeof confidence === "number" ? confidence : null;
paintAutoLabel();
}
function clearDetected() { setDetected(null, null); }
async function runDetect() {
if (srcSel.value !== "auto") return;
const text = inp.value.trim();
if (text.length < DETECT_MIN_CHARS) { clearDetected(); return; }
const seq = ++detectSeq;
try {
const r = await window.silentmode.invoke("detect", {
text,
backend: settingsState.backend,
ltUrl: settingsState.ltUrl,
ltKey: settingsState.ltKey,
});
if (seq !== detectSeq) return; // a newer edit already superseded this
if (r && r.language) setDetected(r.language, r.confidence);
} catch { /* detection is a nicety; never surface it as an error */ }
}
function scheduleDetect() {
clearTimeout(detectTimer);
if (srcSel.value !== "auto") { clearDetected(); return; }
if (inp.value.trim().length < DETECT_MIN_CHARS) { clearDetected(); return; }
detectTimer = setTimeout(runDetect, DETECT_DEBOUNCE_MS);
}
feat(theseus/translate): new sidebar add-on — right-click Translate selection Adds a bundled add-on `translate` with a sidebar panel + a right-click "Translate selection" menu item. Two swappable backends: - LibreTranslate (default) — free MIT engine; the panel's Settings tab lets the user point at any instance (public or self-hosted) and drop in an API key if one's required. - Google (unofficial free endpoint at translate.googleapis.com/ translate_a/single) — no key, wide coverage, but unofficial and Google can break it any time. Opt-in fallback. Flow: user selects text on a page, right-clicks -> "Translate selection". Add-on's context-menu handler stashes the selection under storage.__pending and calls api.revealSidebar("main"); the panel loads, drains __pending on first paint, and translates. Ctrl/Cmd+Enter in the input textarea also translates. Source + target language choices, browser-language default target, swap button, copy-to- clipboard on the output, settings gear. Depends on a new "context-menu-item" capability + api.revealSidebar hook in addons-host.js / main.js. Those wiring changes are prepared but not committed here — a parallel session is refactoring the same functions concurrently, so the safe path is to land translate/ first and let the wiring go in alongside the next host-facing commit. Until the wiring lands, the manifest's "context-menu-item" cap is silently dropped (per validateManifest's unknown-caps policy) and the sidebar panel + the panel's translation UI still work standalone — the right-click entry point is what's gated.
2026-09-20 17:50:06 +02:00
function setStatus(text, cls = "") {
status.textContent = text;
status.className = "m" + (cls ? " " + cls : "");
}
// The panel used to fetch the translation server directly, but a public
// LibreTranslate mirror sitting behind a Cloudflare-style anti-bot page
// returns HTML for a POST from `Origin: file://`, and JSON.parse chokes on
// "<!doctype …". Doing the fetch server-side (via the add-on's index.js)
// sidesteps that whole class of problem and lets index.js fall through a
// small mirror list if the user's URL is down.
feat(theseus/translate): new sidebar add-on — right-click Translate selection Adds a bundled add-on `translate` with a sidebar panel + a right-click "Translate selection" menu item. Two swappable backends: - LibreTranslate (default) — free MIT engine; the panel's Settings tab lets the user point at any instance (public or self-hosted) and drop in an API key if one's required. - Google (unofficial free endpoint at translate.googleapis.com/ translate_a/single) — no key, wide coverage, but unofficial and Google can break it any time. Opt-in fallback. Flow: user selects text on a page, right-clicks -> "Translate selection". Add-on's context-menu handler stashes the selection under storage.__pending and calls api.revealSidebar("main"); the panel loads, drains __pending on first paint, and translates. Ctrl/Cmd+Enter in the input textarea also translates. Source + target language choices, browser-language default target, swap button, copy-to- clipboard on the output, settings gear. Depends on a new "context-menu-item" capability + api.revealSidebar hook in addons-host.js / main.js. Those wiring changes are prepared but not committed here — a parallel session is refactoring the same functions concurrently, so the safe path is to land translate/ first and let the wiring go in alongside the next host-facing commit. Until the wiring lands, the manifest's "context-menu-item" cap is silently dropped (per validateManifest's unknown-caps policy) and the sidebar panel + the panel's translation UI still work standalone — the right-click entry point is what's gated.
2026-09-20 17:50:06 +02:00
async function doTranslate() {
const text = inp.value.trim();
if (!text) { setStatus("nothing to translate"); return; }
const source = srcSel.value, target = tgtSel.value;
if (source && source !== "auto" && source === target) {
setStatus("source = target", "err");
out.textContent = text;
out.classList.remove("empty");
return;
}
goBtn.disabled = true; const orig = goBtn.textContent;
goBtn.textContent = "Translating…";
setStatus("translating…");
out.classList.remove("empty");
try {
const res = await window.silentmode.invoke("translate", {
text, source, target,
backend: settingsState.backend,
ltUrl: settingsState.ltUrl,
ltKey: settingsState.ltKey,
});
feat(theseus/translate): new sidebar add-on — right-click Translate selection Adds a bundled add-on `translate` with a sidebar panel + a right-click "Translate selection" menu item. Two swappable backends: - LibreTranslate (default) — free MIT engine; the panel's Settings tab lets the user point at any instance (public or self-hosted) and drop in an API key if one's required. - Google (unofficial free endpoint at translate.googleapis.com/ translate_a/single) — no key, wide coverage, but unofficial and Google can break it any time. Opt-in fallback. Flow: user selects text on a page, right-clicks -> "Translate selection". Add-on's context-menu handler stashes the selection under storage.__pending and calls api.revealSidebar("main"); the panel loads, drains __pending on first paint, and translates. Ctrl/Cmd+Enter in the input textarea also translates. Source + target language choices, browser-language default target, swap button, copy-to- clipboard on the output, settings gear. Depends on a new "context-menu-item" capability + api.revealSidebar hook in addons-host.js / main.js. Those wiring changes are prepared but not committed here — a parallel session is refactoring the same functions concurrently, so the safe path is to land translate/ first and let the wiring go in alongside the next host-facing commit. Until the wiring lands, the manifest's "context-menu-item" cap is silently dropped (per validateManifest's unknown-caps policy) and the sidebar panel + the panel's translation UI still work standalone — the right-click entry point is what's gated.
2026-09-20 17:50:06 +02:00
out.textContent = res.text || "(empty response)";
const via = res.via || settingsState.backend;
feat(theseus/translate): new sidebar add-on — right-click Translate selection Adds a bundled add-on `translate` with a sidebar panel + a right-click "Translate selection" menu item. Two swappable backends: - LibreTranslate (default) — free MIT engine; the panel's Settings tab lets the user point at any instance (public or self-hosted) and drop in an API key if one's required. - Google (unofficial free endpoint at translate.googleapis.com/ translate_a/single) — no key, wide coverage, but unofficial and Google can break it any time. Opt-in fallback. Flow: user selects text on a page, right-clicks -> "Translate selection". Add-on's context-menu handler stashes the selection under storage.__pending and calls api.revealSidebar("main"); the panel loads, drains __pending on first paint, and translates. Ctrl/Cmd+Enter in the input textarea also translates. Source + target language choices, browser-language default target, swap button, copy-to- clipboard on the output, settings gear. Depends on a new "context-menu-item" capability + api.revealSidebar hook in addons-host.js / main.js. Those wiring changes are prepared but not committed here — a parallel session is refactoring the same functions concurrently, so the safe path is to land translate/ first and let the wiring go in alongside the next host-facing commit. Until the wiring lands, the manifest's "context-menu-item" cap is silently dropped (per validateManifest's unknown-caps policy) and the sidebar panel + the panel's translation UI still work standalone — the right-click entry point is what's gated.
2026-09-20 17:50:06 +02:00
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.
translate 0.1.5 → 0.1.6: name the detected language in the source select Auto-detect already worked — the backend returns detectedLanguage:{language,confidence} and the panel put it in the status line. But that only appeared after a translation had already run, in small text, away from the control that raised the question. You could not tell what "Auto-detect" had decided before committing to it. The first row of the source select now says "Auto-detect · German", and it says so while you are still typing. Detection runs on its own via LibreTranslate's /detect, debounced 700 ms and gated at 12 characters, because a detector given two words is guessing and firing per keystroke would pound a public mirror for nothing. It chains the same mirror fallback as translation, so a dead primary does not make detection look broken while translating still works. Confidence below 60 renders as "German?" rather than silently asserting a coin-flip. The Google backend has no detect-only route, so there doDetect returns null instead of burning a request, and the label is filled from the translation response — which every backend returns anyway, so a skipped or failed detect is never worse than before. Detection retires when a source is named explicitly, comes back on returning to Auto-detect, and is wiped by clear. A right-click selection schedules one too, since that text arrives with no keystroke. Verified against the real mirror (de/fr/ja at 100/100/90%) and in the harness: short text fires nothing, long text fires once, four rapid edits debounce to one call, and every transition above lands. Also adds the xray removal script used to take the old engine off all three exits now that they run sing-box.
2026-09-28 21:50:00 +02:00
// The translation response carries the detection for free — take it,
// so the label is right even when the standalone detect was skipped
// (short text, Google backend, or a mirror that refused /detect).
if (source === "auto" && res.detected) setDetected(res.detected, res.confidence);
const before = topLangs().join(",");
noteLangUse(target);
noteLangUse(source === "auto" ? res.detected : source);
if (topLangs().join(",") !== before) refillLangSelects();
feat(theseus/translate): new sidebar add-on — right-click Translate selection Adds a bundled add-on `translate` with a sidebar panel + a right-click "Translate selection" menu item. Two swappable backends: - LibreTranslate (default) — free MIT engine; the panel's Settings tab lets the user point at any instance (public or self-hosted) and drop in an API key if one's required. - Google (unofficial free endpoint at translate.googleapis.com/ translate_a/single) — no key, wide coverage, but unofficial and Google can break it any time. Opt-in fallback. Flow: user selects text on a page, right-clicks -> "Translate selection". Add-on's context-menu handler stashes the selection under storage.__pending and calls api.revealSidebar("main"); the panel loads, drains __pending on first paint, and translates. Ctrl/Cmd+Enter in the input textarea also translates. Source + target language choices, browser-language default target, swap button, copy-to- clipboard on the output, settings gear. Depends on a new "context-menu-item" capability + api.revealSidebar hook in addons-host.js / main.js. Those wiring changes are prepared but not committed here — a parallel session is refactoring the same functions concurrently, so the safe path is to land translate/ first and let the wiring go in alongside the next host-facing commit. Until the wiring lands, the manifest's "context-menu-item" cap is silently dropped (per validateManifest's unknown-caps policy) and the sidebar panel + the panel's translation UI still work standalone — the right-click entry point is what's gated.
2026-09-20 17:50:06 +02:00
} catch (e) {
out.textContent = "";
out.classList.add("empty");
out.textContent = "Failed: " + (e?.message || String(e));
setStatus("failed", "err");
} finally {
goBtn.disabled = false; goBtn.textContent = orig;
}
saveUi();
}
// Buttons ---------------------------------------------------------------
goBtn.addEventListener("click", doTranslate);
swapBtn.addEventListener("click", () => {
if (srcSel.value === "auto") return;
const a = srcSel.value, b = tgtSel.value;
srcSel.value = b; tgtSel.value = a;
// Swap the text too if there's already output.
if (out.textContent && !out.classList.contains("empty")) {
inp.value = out.textContent;
out.textContent = ""; out.classList.add("empty");
}
saveUi();
});
$("clearIn").addEventListener("click", () => {
inp.value = ""; out.textContent = "The translation will appear here.";
out.classList.add("empty");
setStatus("ready");
translate 0.1.5 → 0.1.6: name the detected language in the source select Auto-detect already worked — the backend returns detectedLanguage:{language,confidence} and the panel put it in the status line. But that only appeared after a translation had already run, in small text, away from the control that raised the question. You could not tell what "Auto-detect" had decided before committing to it. The first row of the source select now says "Auto-detect · German", and it says so while you are still typing. Detection runs on its own via LibreTranslate's /detect, debounced 700 ms and gated at 12 characters, because a detector given two words is guessing and firing per keystroke would pound a public mirror for nothing. It chains the same mirror fallback as translation, so a dead primary does not make detection look broken while translating still works. Confidence below 60 renders as "German?" rather than silently asserting a coin-flip. The Google backend has no detect-only route, so there doDetect returns null instead of burning a request, and the label is filled from the translation response — which every backend returns anyway, so a skipped or failed detect is never worse than before. Detection retires when a source is named explicitly, comes back on returning to Auto-detect, and is wiped by clear. A right-click selection schedules one too, since that text arrives with no keystroke. Verified against the real mirror (de/fr/ja at 100/100/90%) and in the harness: short text fires nothing, long text fires once, four rapid edits debounce to one call, and every transition above lands. Also adds the xray removal script used to take the old engine off all three exits now that they run sing-box.
2026-09-28 21:50:00 +02:00
clearTimeout(detectTimer);
clearDetected();
feat(theseus/translate): new sidebar add-on — right-click Translate selection Adds a bundled add-on `translate` with a sidebar panel + a right-click "Translate selection" menu item. Two swappable backends: - LibreTranslate (default) — free MIT engine; the panel's Settings tab lets the user point at any instance (public or self-hosted) and drop in an API key if one's required. - Google (unofficial free endpoint at translate.googleapis.com/ translate_a/single) — no key, wide coverage, but unofficial and Google can break it any time. Opt-in fallback. Flow: user selects text on a page, right-clicks -> "Translate selection". Add-on's context-menu handler stashes the selection under storage.__pending and calls api.revealSidebar("main"); the panel loads, drains __pending on first paint, and translates. Ctrl/Cmd+Enter in the input textarea also translates. Source + target language choices, browser-language default target, swap button, copy-to- clipboard on the output, settings gear. Depends on a new "context-menu-item" capability + api.revealSidebar hook in addons-host.js / main.js. Those wiring changes are prepared but not committed here — a parallel session is refactoring the same functions concurrently, so the safe path is to land translate/ first and let the wiring go in alongside the next host-facing commit. Until the wiring lands, the manifest's "context-menu-item" cap is silently dropped (per validateManifest's unknown-caps policy) and the sidebar panel + the panel's translation UI still work standalone — the right-click entry point is what's gated.
2026-09-20 17:50:06 +02:00
inp.focus();
});
translate 0.1.5 → 0.1.6: name the detected language in the source select Auto-detect already worked — the backend returns detectedLanguage:{language,confidence} and the panel put it in the status line. But that only appeared after a translation had already run, in small text, away from the control that raised the question. You could not tell what "Auto-detect" had decided before committing to it. The first row of the source select now says "Auto-detect · German", and it says so while you are still typing. Detection runs on its own via LibreTranslate's /detect, debounced 700 ms and gated at 12 characters, because a detector given two words is guessing and firing per keystroke would pound a public mirror for nothing. It chains the same mirror fallback as translation, so a dead primary does not make detection look broken while translating still works. Confidence below 60 renders as "German?" rather than silently asserting a coin-flip. The Google backend has no detect-only route, so there doDetect returns null instead of burning a request, and the label is filled from the translation response — which every backend returns anyway, so a skipped or failed detect is never worse than before. Detection retires when a source is named explicitly, comes back on returning to Auto-detect, and is wiped by clear. A right-click selection schedules one too, since that text arrives with no keystroke. Verified against the real mirror (de/fr/ja at 100/100/90%) and in the harness: short text fires nothing, long text fires once, four rapid edits debounce to one call, and every transition above lands. Also adds the xray removal script used to take the old engine off all three exits now that they run sing-box.
2026-09-28 21:50:00 +02:00
// Detect as the text settles, and on paste (which fires input too, but the
// value is only current after the event loop turn).
inp.addEventListener("input", scheduleDetect);
inp.addEventListener("paste", () => setTimeout(scheduleDetect, 0));
feat(theseus/translate): new sidebar add-on — right-click Translate selection Adds a bundled add-on `translate` with a sidebar panel + a right-click "Translate selection" menu item. Two swappable backends: - LibreTranslate (default) — free MIT engine; the panel's Settings tab lets the user point at any instance (public or self-hosted) and drop in an API key if one's required. - Google (unofficial free endpoint at translate.googleapis.com/ translate_a/single) — no key, wide coverage, but unofficial and Google can break it any time. Opt-in fallback. Flow: user selects text on a page, right-clicks -> "Translate selection". Add-on's context-menu handler stashes the selection under storage.__pending and calls api.revealSidebar("main"); the panel loads, drains __pending on first paint, and translates. Ctrl/Cmd+Enter in the input textarea also translates. Source + target language choices, browser-language default target, swap button, copy-to- clipboard on the output, settings gear. Depends on a new "context-menu-item" capability + api.revealSidebar hook in addons-host.js / main.js. Those wiring changes are prepared but not committed here — a parallel session is refactoring the same functions concurrently, so the safe path is to land translate/ first and let the wiring go in alongside the next host-facing commit. Until the wiring lands, the manifest's "context-menu-item" cap is silently dropped (per validateManifest's unknown-caps policy) and the sidebar panel + the panel's translation UI still work standalone — the right-click entry point is what's gated.
2026-09-20 17:50:06 +02:00
$("copyOut").addEventListener("click", async () => {
if (out.classList.contains("empty") || !out.textContent) return;
try {
await navigator.clipboard.writeText(out.textContent);
setStatus("copied", "ok");
setTimeout(() => setStatus("ready"), 1200);
} catch (e) { setStatus("copy failed", "err"); }
});
gearBtn.addEventListener("click", () => settings.classList.toggle("open"));
backendSel.addEventListener("change", async () => { onBackendChange(); await saveSettings(); });
ltUrl.addEventListener("change", saveSettings);
ltKey.addEventListener("change", saveSettings);
translate 0.1.2 → 0.1.3: real icon, bigger text, zoom, auto-translate on language change Auto-translate: changing either language only called saveUi(), so the pane below kept showing the previous language's result with nothing to say it was stale — you had to notice and press Translate. Both selects now re-run the translation, guarded on empty input and on source === target (which would only echo the input back). Text size: the panes were 13.5px, small for reading a paragraph in a language you don't know well, which is the entire job. Base is now 15px, driven by a --tsize custom property so both panes stay matched. Zoom: −/+ either side of a percentage in the header, 70–220% in steps of 10, clamped with the buttons disabling at each end. Click the percentage to reset. Ctrl/Cmd with +, - or 0 does the same from either pane. Persists per-machine alongside the language choice. Icon: a globe with an A tile and a 文 tile, replacing the 🌐 emoji that was indistinguishable from every other globe in the dock. Drawn against the 16px and 18px rasterizations rather than at a comfortable size — the first pass used a font glyph for 文 and it turned to grey mush at 16px, so the strokes are hand-drawn paths thick enough to survive. Also drops the hardcoded icon in registerSidebarPanel, which would otherwise shadow the manifest's mark. Verified in a harness with a stubbed host API: language change fires exactly one request, the two guards fire none, zoom clamps and persists, and the layout holds at sidebar width.
2026-09-27 18:00:46 +02:00
// Changing either language re-runs the translation straight away. Picking a
// new target and then having to hit Translate was the single most confusing
// thing about this panel: the old handler saved the choice and stopped
// there, so the pane below kept showing the previous language's text with
// no sign it was stale.
async function onLangChange() {
translate 0.1.5 → 0.1.6: name the detected language in the source select Auto-detect already worked — the backend returns detectedLanguage:{language,confidence} and the panel put it in the status line. But that only appeared after a translation had already run, in small text, away from the control that raised the question. You could not tell what "Auto-detect" had decided before committing to it. The first row of the source select now says "Auto-detect · German", and it says so while you are still typing. Detection runs on its own via LibreTranslate's /detect, debounced 700 ms and gated at 12 characters, because a detector given two words is guessing and firing per keystroke would pound a public mirror for nothing. It chains the same mirror fallback as translation, so a dead primary does not make detection look broken while translating still works. Confidence below 60 renders as "German?" rather than silently asserting a coin-flip. The Google backend has no detect-only route, so there doDetect returns null instead of burning a request, and the label is filled from the translation response — which every backend returns anyway, so a skipped or failed detect is never worse than before. Detection retires when a source is named explicitly, comes back on returning to Auto-detect, and is wiped by clear. A right-click selection schedules one too, since that text arrives with no keystroke. Verified against the real mirror (de/fr/ja at 100/100/90%) and in the harness: short text fires nothing, long text fires once, four rapid edits debounce to one call, and every transition above lands. Also adds the xray removal script used to take the old engine off all three exits now that they run sing-box.
2026-09-28 21:50:00 +02:00
// Naming a source explicitly retires the detection; going back to
// Auto-detect re-runs it against whatever is in the box.
if (srcSel.value !== "auto") clearDetected(); else scheduleDetect();
translate 0.1.2 → 0.1.3: real icon, bigger text, zoom, auto-translate on language change Auto-translate: changing either language only called saveUi(), so the pane below kept showing the previous language's result with nothing to say it was stale — you had to notice and press Translate. Both selects now re-run the translation, guarded on empty input and on source === target (which would only echo the input back). Text size: the panes were 13.5px, small for reading a paragraph in a language you don't know well, which is the entire job. Base is now 15px, driven by a --tsize custom property so both panes stay matched. Zoom: −/+ either side of a percentage in the header, 70–220% in steps of 10, clamped with the buttons disabling at each end. Click the percentage to reset. Ctrl/Cmd with +, - or 0 does the same from either pane. Persists per-machine alongside the language choice. Icon: a globe with an A tile and a 文 tile, replacing the 🌐 emoji that was indistinguishable from every other globe in the dock. Drawn against the 16px and 18px rasterizations rather than at a comfortable size — the first pass used a font glyph for 文 and it turned to grey mush at 16px, so the strokes are hand-drawn paths thick enough to survive. Also drops the hardcoded icon in registerSidebarPanel, which would otherwise shadow the manifest's mark. Verified in a harness with a stubbed host API: language change fires exactly one request, the two guards fire none, zoom clamps and persists, and the layout holds at sidebar width.
2026-09-27 18:00:46 +02:00
await saveUi();
if (!inp.value.trim()) return; // nothing to re-translate
const src = srcSel.value, tgt = tgtSel.value;
if (src !== "auto" && src === tgt) return; // doTranslate would just echo
doTranslate();
}
srcSel.addEventListener("change", onLangChange);
tgtSel.addEventListener("change", onLangChange);
// Zoom controls.
$("zoomIn").addEventListener("click", () => setZoom((uiState.zoom || 100) + ZOOM_STEP));
$("zoomOut").addEventListener("click", () => setZoom((uiState.zoom || 100) - ZOOM_STEP));
$("zoomPct").addEventListener("click", () => setZoom(100));
$("zoomPct").addEventListener("keydown", (e) => {
if (e.key === "Enter" || e.key === " ") { e.preventDefault(); setZoom(100); }
});
feat(theseus/translate): new sidebar add-on — right-click Translate selection Adds a bundled add-on `translate` with a sidebar panel + a right-click "Translate selection" menu item. Two swappable backends: - LibreTranslate (default) — free MIT engine; the panel's Settings tab lets the user point at any instance (public or self-hosted) and drop in an API key if one's required. - Google (unofficial free endpoint at translate.googleapis.com/ translate_a/single) — no key, wide coverage, but unofficial and Google can break it any time. Opt-in fallback. Flow: user selects text on a page, right-clicks -> "Translate selection". Add-on's context-menu handler stashes the selection under storage.__pending and calls api.revealSidebar("main"); the panel loads, drains __pending on first paint, and translates. Ctrl/Cmd+Enter in the input textarea also translates. Source + target language choices, browser-language default target, swap button, copy-to- clipboard on the output, settings gear. Depends on a new "context-menu-item" capability + api.revealSidebar hook in addons-host.js / main.js. Those wiring changes are prepared but not committed here — a parallel session is refactoring the same functions concurrently, so the safe path is to land translate/ first and let the wiring go in alongside the next host-facing commit. Until the wiring lands, the manifest's "context-menu-item" cap is silently dropped (per validateManifest's unknown-caps policy) and the sidebar panel + the panel's translation UI still work standalone — the right-click entry point is what's gated.
2026-09-20 17:50:06 +02:00
translate 0.1.2 → 0.1.3: real icon, bigger text, zoom, auto-translate on language change Auto-translate: changing either language only called saveUi(), so the pane below kept showing the previous language's result with nothing to say it was stale — you had to notice and press Translate. Both selects now re-run the translation, guarded on empty input and on source === target (which would only echo the input back). Text size: the panes were 13.5px, small for reading a paragraph in a language you don't know well, which is the entire job. Base is now 15px, driven by a --tsize custom property so both panes stay matched. Zoom: −/+ either side of a percentage in the header, 70–220% in steps of 10, clamped with the buttons disabling at each end. Click the percentage to reset. Ctrl/Cmd with +, - or 0 does the same from either pane. Persists per-machine alongside the language choice. Icon: a globe with an A tile and a 文 tile, replacing the 🌐 emoji that was indistinguishable from every other globe in the dock. Drawn against the 16px and 18px rasterizations rather than at a comfortable size — the first pass used a font glyph for 文 and it turned to grey mush at 16px, so the strokes are hand-drawn paths thick enough to survive. Also drops the hardcoded icon in registerSidebarPanel, which would otherwise shadow the manifest's mark. Verified in a harness with a stubbed host API: language change fires exactly one request, the two guards fire none, zoom clamps and persists, and the layout holds at sidebar width.
2026-09-27 18:00:46 +02:00
// Ctrl/Cmd+Enter to translate; Ctrl/Cmd +/-/0 to zoom the text panes.
// Bound on the document so it works whichever pane has focus.
document.addEventListener("keydown", (e) => {
if (!(e.ctrlKey || e.metaKey)) return;
if (e.key === "Enter" && e.target === inp) { e.preventDefault(); doTranslate(); return; }
// "=" is the unshifted "+" on most layouts; accept both.
if (e.key === "+" || e.key === "=") { e.preventDefault(); setZoom((uiState.zoom || 100) + ZOOM_STEP); }
else if (e.key === "-") { e.preventDefault(); setZoom((uiState.zoom || 100) - ZOOM_STEP); }
else if (e.key === "0") { e.preventDefault(); setZoom(100); }
feat(theseus/translate): new sidebar add-on — right-click Translate selection Adds a bundled add-on `translate` with a sidebar panel + a right-click "Translate selection" menu item. Two swappable backends: - LibreTranslate (default) — free MIT engine; the panel's Settings tab lets the user point at any instance (public or self-hosted) and drop in an API key if one's required. - Google (unofficial free endpoint at translate.googleapis.com/ translate_a/single) — no key, wide coverage, but unofficial and Google can break it any time. Opt-in fallback. Flow: user selects text on a page, right-clicks -> "Translate selection". Add-on's context-menu handler stashes the selection under storage.__pending and calls api.revealSidebar("main"); the panel loads, drains __pending on first paint, and translates. Ctrl/Cmd+Enter in the input textarea also translates. Source + target language choices, browser-language default target, swap button, copy-to- clipboard on the output, settings gear. Depends on a new "context-menu-item" capability + api.revealSidebar hook in addons-host.js / main.js. Those wiring changes are prepared but not committed here — a parallel session is refactoring the same functions concurrently, so the safe path is to land translate/ first and let the wiring go in alongside the next host-facing commit. Until the wiring lands, the manifest's "context-menu-item" cap is silently dropped (per validateManifest's unknown-caps policy) and the sidebar panel + the panel's translation UI still work standalone — the right-click entry point is what's gated.
2026-09-20 17:50:06 +02:00
});
// Pending selection from a right-click. When the addon dispatches
// "context-menu", the addon's index.js writes __pending to storage and
// reveals the sidebar; we pull it here.
async function drainPending() {
if (!S) return;
let pending = null;
try { pending = await S.get("__pending", null); } catch {}
if (!pending || !pending.text) return;
const age = Date.now() - (pending.at || 0);
if (age > 5 * 60_000) { try { await S.set("__pending", null); } catch {}; return; } // stale
inp.value = pending.text;
try { await S.set("__pending", null); } catch {}
setStatus(pending.host ? `from ${pending.host}` : "from page selection");
translate 0.1.5 → 0.1.6: name the detected language in the source select Auto-detect already worked — the backend returns detectedLanguage:{language,confidence} and the panel put it in the status line. But that only appeared after a translation had already run, in small text, away from the control that raised the question. You could not tell what "Auto-detect" had decided before committing to it. The first row of the source select now says "Auto-detect · German", and it says so while you are still typing. Detection runs on its own via LibreTranslate's /detect, debounced 700 ms and gated at 12 characters, because a detector given two words is guessing and firing per keystroke would pound a public mirror for nothing. It chains the same mirror fallback as translation, so a dead primary does not make detection look broken while translating still works. Confidence below 60 renders as "German?" rather than silently asserting a coin-flip. The Google backend has no detect-only route, so there doDetect returns null instead of burning a request, and the label is filled from the translation response — which every backend returns anyway, so a skipped or failed detect is never worse than before. Detection retires when a source is named explicitly, comes back on returning to Auto-detect, and is wiped by clear. A right-click selection schedules one too, since that text arrives with no keystroke. Verified against the real mirror (de/fr/ja at 100/100/90%) and in the harness: short text fires nothing, long text fires once, four rapid edits debounce to one call, and every transition above lands. Also adds the xray removal script used to take the old engine off all three exits now that they run sing-box.
2026-09-28 21:50:00 +02:00
// Text arrived without a keystroke, so nothing scheduled a detect. The
// translation below reports the language anyway, but kicking one off now
// names it while that request is still in flight.
scheduleDetect();
feat(theseus/translate): new sidebar add-on — right-click Translate selection Adds a bundled add-on `translate` with a sidebar panel + a right-click "Translate selection" menu item. Two swappable backends: - LibreTranslate (default) — free MIT engine; the panel's Settings tab lets the user point at any instance (public or self-hosted) and drop in an API key if one's required. - Google (unofficial free endpoint at translate.googleapis.com/ translate_a/single) — no key, wide coverage, but unofficial and Google can break it any time. Opt-in fallback. Flow: user selects text on a page, right-clicks -> "Translate selection". Add-on's context-menu handler stashes the selection under storage.__pending and calls api.revealSidebar("main"); the panel loads, drains __pending on first paint, and translates. Ctrl/Cmd+Enter in the input textarea also translates. Source + target language choices, browser-language default target, swap button, copy-to- clipboard on the output, settings gear. Depends on a new "context-menu-item" capability + api.revealSidebar hook in addons-host.js / main.js. Those wiring changes are prepared but not committed here — a parallel session is refactoring the same functions concurrently, so the safe path is to land translate/ first and let the wiring go in alongside the next host-facing commit. Until the wiring lands, the manifest's "context-menu-item" cap is silently dropped (per validateManifest's unknown-caps policy) and the sidebar panel + the panel's translation UI still work standalone — the right-click entry point is what's gated.
2026-09-20 17:50:06 +02:00
doTranslate();
}
// First paint. loadState → drainPending, and re-drain whenever the panel
// becomes visible again (the sidebar preload dispatches a "sidebar-
// visibility" event on its window).
(async () => {
translate 0.1.2 → 0.1.3: real icon, bigger text, zoom, auto-translate on language change Auto-translate: changing either language only called saveUi(), so the pane below kept showing the previous language's result with nothing to say it was stale — you had to notice and press Translate. Both selects now re-run the translation, guarded on empty input and on source === target (which would only echo the input back). Text size: the panes were 13.5px, small for reading a paragraph in a language you don't know well, which is the entire job. Base is now 15px, driven by a --tsize custom property so both panes stay matched. Zoom: −/+ either side of a percentage in the header, 70–220% in steps of 10, clamped with the buttons disabling at each end. Click the percentage to reset. Ctrl/Cmd with +, - or 0 does the same from either pane. Persists per-machine alongside the language choice. Icon: a globe with an A tile and a 文 tile, replacing the 🌐 emoji that was indistinguishable from every other globe in the dock. Drawn against the 16px and 18px rasterizations rather than at a comfortable size — the first pass used a font glyph for 文 and it turned to grey mush at 16px, so the strokes are hand-drawn paths thick enough to survive. Also drops the hardcoded icon in registerSidebarPanel, which would otherwise shadow the manifest's mark. Verified in a harness with a stubbed host API: language change fires exactly one request, the two guards fire none, zoom clamps and persists, and the layout holds at sidebar width.
2026-09-27 18:00:46 +02:00
applyZoom(); // paint the control before storage answers (or if it never does)
await loadState(); // re-applies with the saved zoom once it's read
feat(theseus/translate): new sidebar add-on — right-click Translate selection Adds a bundled add-on `translate` with a sidebar panel + a right-click "Translate selection" menu item. Two swappable backends: - LibreTranslate (default) — free MIT engine; the panel's Settings tab lets the user point at any instance (public or self-hosted) and drop in an API key if one's required. - Google (unofficial free endpoint at translate.googleapis.com/ translate_a/single) — no key, wide coverage, but unofficial and Google can break it any time. Opt-in fallback. Flow: user selects text on a page, right-clicks -> "Translate selection". Add-on's context-menu handler stashes the selection under storage.__pending and calls api.revealSidebar("main"); the panel loads, drains __pending on first paint, and translates. Ctrl/Cmd+Enter in the input textarea also translates. Source + target language choices, browser-language default target, swap button, copy-to- clipboard on the output, settings gear. Depends on a new "context-menu-item" capability + api.revealSidebar hook in addons-host.js / main.js. Those wiring changes are prepared but not committed here — a parallel session is refactoring the same functions concurrently, so the safe path is to land translate/ first and let the wiring go in alongside the next host-facing commit. Until the wiring lands, the manifest's "context-menu-item" cap is silently dropped (per validateManifest's unknown-caps policy) and the sidebar panel + the panel's translation UI still work standalone — the right-click entry point is what's gated.
2026-09-20 17:50:06 +02:00
await drainPending();
inp.focus();
})();
window.addEventListener("visibilitychange", () => { if (!document.hidden) drainPending(); });
// Sidebar preload emits this custom event on the panel's window when the
// sidebar is shown/hidden — see main.js's send("sidebar-visibility").
// Not all preload versions expose it as an event; if not, visibilitychange
// above covers most cases.
</script>
</body>
</html>