From 501c7cf2730d917541797ff09b4ca47aa71926a7 Mon Sep 17 00:00:00 2001 From: Local Dev Date: Sun, 20 Sep 2026 17:50:06 +0200 Subject: [PATCH] =?UTF-8?q?feat(theseus/translate):=20new=20sidebar=20add-?= =?UTF-8?q?on=20=E2=80=94=20right-click=20Translate=20selection?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- bundled-addons/translate/addon.json | 19 ++ bundled-addons/translate/index.js | 32 +++ bundled-addons/translate/panel.html | 350 ++++++++++++++++++++++++++++ 3 files changed, 401 insertions(+) create mode 100644 bundled-addons/translate/addon.json create mode 100644 bundled-addons/translate/index.js create mode 100644 bundled-addons/translate/panel.html diff --git a/bundled-addons/translate/addon.json b/bundled-addons/translate/addon.json new file mode 100644 index 0000000..cd8ae17 --- /dev/null +++ b/bundled-addons/translate/addon.json @@ -0,0 +1,19 @@ +{ + "id": "translate", + "name": "Translate", + "version": "0.1.0", + "description": "Right-click a selection to translate it. Sidebar panel with LibreTranslate or Google as the backend.", + "author": "Silent Mode", + "icon": "🌐", + "main": "index.js", + "capabilities": ["sidebar-panel", "context-menu-item"], + "context-menu-items": [ + { + "id": "translate-selection", + "label": "Translate selection", + "when": "selectionText", + "icon": "🌐" + } + ], + "updateURL": "https://navigate.st/bns/theseus.x/extensions/translate/updates.json" +} diff --git a/bundled-addons/translate/index.js b/bundled-addons/translate/index.js new file mode 100644 index 0000000..f6cf9b8 --- /dev/null +++ b/bundled-addons/translate/index.js @@ -0,0 +1,32 @@ +// Translate — right-click a selection, get a translation in the sidebar. +// +// One sidebar panel; the panel HTML does the actual translation (LibreTranslate +// or Google unofficial free endpoint, user's pick). When the user picks +// "Translate selection" from a page's right-click menu we stash the selection +// under storage.__pending and reveal our sidebar; the panel reads __pending on +// load / on visibility change and translates immediately. + +module.exports = { + activate(api) { + api.registerSidebarPanel({ + id: "main", + title: "Translate", + icon: "🌐", + page: "panel.html", + }); + + api.onMessage("context-menu", async (payload) => { + const text = String(payload && payload.selectionText || "").trim(); + if (!text) { api.log("context-menu fired with no selection"); return; } + // Cap what we stash to keep storage tiny; the address bar and menu already + // truncate visually, but the raw selection can be huge. + const clip = text.length > 12_000 ? text.slice(0, 12_000) : text; + api.storage.set("__pending", { text: clip, host: payload.host || "", at: Date.now() }); + api.revealSidebar("main"); + api.log(`context-menu → translate ${clip.length} chars from ${payload.host || "?"}`); + return { ok: true }; + }); + + api.log("registered translate panel + context-menu item"); + }, +}; diff --git a/bundled-addons/translate/panel.html b/bundled-addons/translate/panel.html new file mode 100644 index 0000000..9e696e5 --- /dev/null +++ b/bundled-addons/translate/panel.html @@ -0,0 +1,350 @@ + + + + +Translate + + + +
+
🌐 Translate
+
ready
+
+ +
+ + + +
+ +
+
+ + +
+
+ +
The translation will appear here.
+
+
+ +
+ + +
+ +
+ + + +
+ LibreTranslate is a free MIT-licensed engine. Public instances rate-limit; if you + hit "too many requests", pick another URL or self-host. Google (unofficial) uses + translate.googleapis.com/translate_a/single — free, no key, but + unofficial and Google can break it at any time. +
+
+ + + +