theseus/bundled-addons/translate/panel.html

461 lines
21 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);
}
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 = "";
if (includeAuto) el.appendChild(new Option("Auto-detect", "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";
}
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.
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");
inp.focus();
});
$("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() {
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");
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>