Theseus: translator provider setting — LibreTranslate or Bergamot

A new row in Settings › Language, Translator engine: a radio between the
hosted LibreTranslate backend (the current default) and the on-device
Bergamot engine. The chip and auto-translate route through a single
dispatcher (translatorCall) that picks the provider by setting; adding
or removing an engine only touches the dispatcher.

Bergamot's WASM runtime and model manager are not bundled yet, so the
Bergamot provider falls back to LibreTranslate for now and prints a
one-time console notice. The setting itself is real today — a user can
declare their preference and the real engine lands in a later release
without the user touching Settings again. See bergamot.x for status.
This commit is contained in:
Silent Mode 2026-10-04 14:20:18 +02:00
parent b99d3a6e54
commit 0ce2ab95dd
2 changed files with 70 additions and 1 deletions

43
main.js
View file

@ -548,6 +548,18 @@ const SETTINGS_DEFAULTS = {
"https://libretranslate.com/translate",
],
translateApiKey: "",
// Which translator provider the chip and auto-translate use.
// "libretranslate" — POST text to a hosted LibreTranslate peer (the
// translateEndpoints list). Fast, no setup, but the peer sees the page.
// "bergamot" — Mozilla's WASM translator running on the user's own
// computer; the page's text never leaves the device. Models mirror
// from bergamot.x on first use. The WASM runtime + model manager are
// NOT bundled yet (2026-10-04); picking this today still routes
// through LibreTranslate with a one-time console notice, so the user
// can declare their preference now and get the real engine when it
// ships without changing the setting again.
// The setting lives in Settings › Language.
translateProvider: "libretranslate",
// Light up the URL-bar translate chip when the loaded page's language
// differs from the user's preferred one. Clicking the chip translates
// the page in place; clicking it again reverts.
@ -5067,6 +5079,35 @@ async function translatorLibreTranslate(texts, from, to) {
}
throw new Error(`all ${peers.length} translator peer(s) failed — ${errors[errors.length - 1] || "no detail"}`);
}
// Bergamot (on-device) provider. The runtime is Mozilla's WASM port of
// Marian NMT; models mirror from `bergamot.x` to the user's Theseus
// profile on first use. Neither the runtime nor any model is bundled in
// Theseus yet (2026-10-04) — the plumbing exists so the setting is real
// today and the engine can land in a later release without churning the
// chip, the picker or the chunking.
//
// Behaviour until the runtime lands:
// - Log a one-time per-session notice that we're falling back.
// - Delegate to translatorLibreTranslate so the user still gets a
// translated page, exactly as if they'd left the provider set to
// LibreTranslate. The setting carries forward.
let _bergamotFallbackNoticed = false;
async function translatorBergamot(texts, from, to) {
if (!_bergamotFallbackNoticed) {
console.warn("[translate] Bergamot provider picked, but the on-device runtime is not bundled yet — falling back to LibreTranslate. See bergamot.x for status.");
_bergamotFallbackNoticed = true;
}
return translatorLibreTranslate(texts, from, to);
}
// Pick the provider by setting. Keep this the only place that reads
// `settings.translateProvider` — `translateAll` and `translateActiveTab`
// stay provider-agnostic, so adding or removing engines only touches
// this dispatcher.
async function translatorCall(texts, from, to) {
const provider = String(settings.translateProvider || "libretranslate").toLowerCase();
if (provider === "bergamot") return translatorBergamot(texts, from, to);
return translatorLibreTranslate(texts, from, to);
}
// Chunk a texts array so each POST stays under a reasonable size — LibreTranslate
// instances vary (3-5 KB is a safe shared floor), and a monolithic 50-page POST
// is also slower to recover from an upstream drop than four 12-page POSTs.
@ -5088,7 +5129,7 @@ async function translateAll(texts, from, to) {
const chunks = chunkTexts(texts);
const out = [];
for (const chunk of chunks) {
const got = await translatorLibreTranslate(chunk, from, to);
const got = await translatorCall(chunk, from, to);
for (const s of got) out.push(s);
}
return out;

View file

@ -372,6 +372,14 @@
</div>
<h2 class="sub">Page translation</h2>
<p class="subd">When the page's declared language is different from yours, Theseus can translate its visible text in place. The source stays untouched — click the chip again to revert.</p>
<div class="row" style="flex-direction:column;align-items:stretch;gap:10px">
<div class="txt"><div class="t">Translator engine</div>
<div class="d">Where the translation runs. Both read the chip the same way; the difference is who sees the page's text. Details at <a href="https://bergamot.x" target="_blank" rel="noopener">bergamot.x</a>.</div></div>
<div id="translateProviderList" style="display:flex;flex-direction:column;gap:6px">
<label class="polrow"><input type="radio" name="translateProvider" value="libretranslate"><span><b>LibreTranslate — hosted</b> <span class="pmuted">— Silent Mode's server does the translation; the page's text is POSTed to <code>silentmode.st/libre</code> (or any peer you add). Fast and ready today.</span></span></label>
<label class="polrow"><input type="radio" name="translateProvider" value="bergamot"><span><b>Bergamot — on-device</b> <span class="pmuted">— WebAssembly engine on your computer; nothing leaves the device. Needs a one-time per-language model download. <b style="color:var(--warn)">Coming in a later release</b> — picking it today still routes through LibreTranslate with a one-time notice.</span></span></label>
</div>
</div>
<div class="row">
<div class="txt"><div class="t">Translate automatically</div><div class="d">When the page's declared language is different from yours (and both are supported by the translator), Theseus translates it in place as soon as it loads. Turn this off to leave pages in their original language — the globe chip's menu still offers a one-click translation.</div></div>
<label class="sw"><input type="checkbox" id="translateAutoOffer"><span class="track"><span class="knob"></span></span></label>
@ -1225,6 +1233,26 @@
};
if (C.onSettingsUpdate) C.onSettingsUpdate((next) => render(next.translateEndpoints || []));
})();
// ---- Translator engine (Language) — radio between the hosted
// LibreTranslate backend and the on-device Bergamot engine. Bergamot
// is wired as a provider but its WASM runtime is not bundled yet;
// picking it today still routes through LibreTranslate (see main.js
// translatorBergamot stub). Lives in the same Language section so
// the user sees the two choices beside each other and side effects
// (chip tooltip) stay in context.
(function () {
const radios = document.querySelectorAll('input[name="translateProvider"]');
if (!radios.length) return;
const current = s.translateProvider || "libretranslate";
radios.forEach((r) => {
r.checked = (r.value === current);
r.addEventListener("change", () => { if (r.checked) C.set("translateProvider", r.value); });
});
if (C.onSettingsUpdate) C.onSettingsUpdate((next) => {
const v = next.translateProvider || "libretranslate";
radios.forEach((r) => { r.checked = (r.value === v); });
});
})();
// ---- website language (General page) — friendly wrapper over the
// same languageMode/languageValue setting the Anti-fingerprinting Language
// row edits. "Auto" = languageMode="show" (follow OS); anything else