Theseus: Settings grows its own Language page

Website language, offer-to-translate and the translator peers list used
to sit as three sub-sections inside General, and Privacy › Anti-
fingerprinting duplicated the language picker on top of them — three
places to edit the one languageMode/languageValue setting. Settings
grows its own Language entry in the sidebar now; everything about
language — the preferred-language picker, the auto-offer toggle, the
peer list, the API key — lives there, and the Privacy duplicate is
gone. The chip in the URL bar still edits the same setting, so the
toolbar surface is unchanged.

The copy for the picker ("Your language") now says what the setting
actually drives: it's sent as Accept-Language and it's the translator's
target. Legacy "hide"/"spoof" language modes migrate to Automatic on
first open of Settings (the picker only has Automatic / a tag / Other),
so a profile that still carries one of those from an older release
lands on a valid state the first time Settings opens.

Greek (el-GR) was already in the picker; this release's silentmode.st/
libre backend added `el` to --load-only so the translator now has real
en↔el support — the picker and the backend are in step.
This commit is contained in:
Silent Mode 2026-10-03 19:05:25 +02:00
parent 4ad95b3c7a
commit 88044952e3

View file

@ -258,6 +258,7 @@
<nav class="side">
<div class="brand">⛓ Theseus</div>
<a data-sec="general" class="active">General</a>
<a data-sec="language">Language</a>
<a data-sec="search">Search</a>
<a data-sec="passwords">Passwords</a>
<a data-sec="performance">Performance</a>
@ -270,34 +271,7 @@
<section id="general">
<h1>General</h1>
<p class="lede">Changes apply immediately and are saved for next time.</p>
<h2 class="sub" style="border-top:0;padding-top:0;margin-top:0">Website language</h2>
<div class="row">
<div class="txt"><div class="t">Preferred language</div><div class="d" id="webLangHint">Also switchable from the globe icon in the address bar.</div></div>
<div class="ctl">
<select id="webLangPick"></select>
<input id="webLangOther" type="text" placeholder="BCP-47, e.g. cs-CZ" hidden style="width:150px">
</div>
</div>
<h2 class="sub">Translate pages</h2>
<p class="subd">Translate a page's visible text in place to your preferred language. The <b>Accept-Language</b> row above only asks the server for a translated body — many static sites (including names on BCDN) serve only one language, and translation turns them readable without a reload.</p>
<div class="row">
<div class="txt"><div class="t">Offer to translate</div><div class="d">Light up a translate chip in the address bar when the page's declared language is different from the one you picked above.</div></div>
<label class="sw"><input type="checkbox" id="translateAutoOffer"><span class="track"><span class="knob"></span></span></label>
</div>
<div class="row" style="flex-direction:column;align-items:stretch;gap:8px">
<div class="txt"><div class="t">Translation peers</div>
<div class="d">Ordered list of LibreTranslate-compatible endpoints. The first one that answers wins; a dead or rate-limited peer is skipped and the next is tried. Keep Silent Mode's own instances at the top; add your own self-hosted LibreTranslate for privacy or unlimited use. On-device Bergamot (WASM) is coming in a later release.</div></div>
<div id="translateEndpointList" style="display:flex;flex-direction:column;gap:6px"></div>
<div style="display:flex;gap:6px;flex-wrap:wrap">
<input id="translatePeerUrl" type="text" placeholder="https://your-libretranslate/translate" style="flex:1;min-width:220px;background:#1b2330;color:var(--ink);border:1px solid var(--line);border-radius:8px;padding:7px 10px;font-size:13px">
<button id="translatePeerAdd" class="btn">Add peer</button>
</div>
</div>
<div class="row">
<div class="txt"><div class="t">API key (optional)</div><div class="d">Shared across all peers in the list. Some LibreTranslate instances need a key for the paid tier or to raise the rate limit; leave blank for peers that don't.</div></div>
<input id="translateApiKey" type="text" placeholder="(none)" style="min-width:200px;background:#1b2330;color:var(--ink);border:1px solid var(--line);border-radius:8px;padding:7px 10px;font-size:13px">
</div>
<h2 class="sub">Startup</h2>
<h2 class="sub" style="border-top:0;padding-top:0;margin-top:0">Startup</h2>
<div class="row">
<div class="txt"><div class="t">Open previous windows and tabs</div><div class="d">Restore the tabs from your last session when Theseus starts.</div></div>
<label class="sw"><input type="checkbox" id="restoreSession"><span class="track"><span class="knob"></span></span></label>
@ -380,6 +354,42 @@
The names and marks are reserved: <a href="https://code.silentmode.st/silentmode/theseus/src/branch/master/TRADEMARKS.md" target="_blank" rel="noopener">trademarks</a>.</div></div>
</div>
</section>
<!-- LANGUAGE -->
<!-- One language setting for the whole browser: it's what we send to sites
as Accept-Language (so a server that has your language serves you in
it), and it's the target for the in-page translator (so a server that
doesn't is still readable). The translate chip in the address bar
lights up when the two don't match. -->
<section id="language" hidden>
<h1>Language</h1>
<p class="lede">Your language. Websites that have it serve you in it; pages that don't can be translated in place.</p>
<div class="row" style="border-top:0;padding-top:0;margin-top:0">
<div class="txt"><div class="t">Your language</div><div class="d" id="webLangHint">Sent to every site in the <b>Accept-Language</b> header; the translate chip's target. Also switchable from the globe icon in the address bar.</div></div>
<div class="ctl">
<select id="webLangPick"></select>
<input id="webLangOther" type="text" placeholder="BCP-47, e.g. cs-CZ" hidden style="width:150px">
</div>
</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">
<div class="txt"><div class="t">Offer to translate</div><div class="d">Light up a translate chip in the address bar when the page's language differs from yours.</div></div>
<label class="sw"><input type="checkbox" id="translateAutoOffer"><span class="track"><span class="knob"></span></span></label>
</div>
<div class="row" style="flex-direction:column;align-items:stretch;gap:8px">
<div class="txt"><div class="t">Translation peers</div>
<div class="d">Ordered list of LibreTranslate-compatible endpoints. The first one that answers wins; a dead or rate-limited peer is skipped and the next is tried. Keep Silent Mode's own instances at the top; add your own self-hosted LibreTranslate for privacy or unlimited use. On-device Bergamot (WASM) is coming in a later release.</div></div>
<div id="translateEndpointList" style="display:flex;flex-direction:column;gap:6px"></div>
<div style="display:flex;gap:6px;flex-wrap:wrap">
<input id="translatePeerUrl" type="text" placeholder="https://your-libretranslate/translate" style="flex:1;min-width:220px;background:#1b2330;color:var(--ink);border:1px solid var(--line);border-radius:8px;padding:7px 10px;font-size:13px">
<button id="translatePeerAdd" class="btn">Add peer</button>
</div>
</div>
<div class="row">
<div class="txt"><div class="t">API key (optional)</div><div class="d">Shared across all peers in the list. Some LibreTranslate instances need a key for the paid tier or to raise the rate limit; leave blank for peers that don't.</div></div>
<input id="translateApiKey" type="text" placeholder="(none)" style="min-width:200px;background:#1b2330;color:var(--ink);border:1px solid var(--line);border-radius:8px;padding:7px 10px;font-size:13px">
</div>
</section>
<!-- PLUG-INS -->
<!-- Compact main page: one row per plug-in with title / status / update
controls on the left and the on/off toggle on the right. Clicking
@ -712,18 +722,6 @@
<input id="timezoneValueOther" type="text" placeholder="IANA zone, e.g. America/Anchorage" hidden>
</div>
</div>
<div class="row">
<div class="txt"><div class="t">Language</div><div class="d">Same control as <b>General › Website language</b> and the globe chip in the URL bar.</div></div>
<div class="ctl">
<select id="languageMode">
<option value="show">Automatic (system language)</option>
<option value="manual">Manual…</option>
</select>
<!-- Populated in JS from WEB_LANG_QUICK (same list the General row + globe menu use). -->
<select id="languageValue" hidden></select>
<input id="languageValueOther" type="text" placeholder="BCP-47 tag, e.g. cs-CZ" hidden>
</div>
</div>
<div class="row">
<div class="txt"><div class="t">Location</div><div class="d"><b>Hide</b> denies geolocation entirely; <b>Manual</b> reports the coordinates of a country you pick, so a page's <code>navigator.geolocation</code> answers with a plausible position instead of your real one.</div></div>
<!-- Column layout so the country dropdown always lands on its own line
@ -1481,52 +1479,12 @@
// "Manual" mode reveals both the select AND the Other input (if Other was picked).
const applyTzVis = (manual) => { tzV.hidden = !manual; tzOther.hidden = !manual || tzV.value !== "__other__"; };
bind("timezoneMode", null, applyTzVis);
// Language — same languageMode/languageValue setting that General ›
// Website language and the globe chip edit. Simplified to two modes:
// Automatic (follow OS) and Manual (pick a language). Legacy "hide" and
// "spoof" values migrate to Automatic on first open so the dropdown
// always has a valid pick. The shared WEB_LANG_LIST populates the
// language select so the picker here matches the General row's.
const lngMode = document.getElementById("languageMode");
const lgV = document.getElementById("languageValue");
const lgOther = document.getElementById("languageValueOther");
lgV.innerHTML = WEB_LANG_LIST.map((L) => `<option value="${L.tag}">${L.label}</option>`).join("")
+ `<option value="__other__">Other…</option>`;
// Migrate legacy modes to the new two-option set on load.
let initMode = s.languageMode || "show";
if (initMode === "hide" || initMode === "spoof") {
C.set("languageMode", "show");
initMode = "show";
}
const savedTag = s.languageValue || "en-GB";
const known = new Set(WEB_LANG_LIST.map((L) => L.tag));
if (known.has(savedTag)) { lgV.value = savedTag; lgOther.hidden = true; }
else { lgV.value = "__other__"; lgOther.value = savedTag; }
const applyLng = () => {
lgV.hidden = lngMode.value !== "manual";
lgOther.hidden = lngMode.value !== "manual" || lgV.value !== "__other__";
};
lngMode.value = initMode;
applyLng();
lngMode.addEventListener("change", () => { C.set("languageMode", lngMode.value); applyLng(); });
lgV.addEventListener("change", () => {
if (lgV.value === "__other__") { applyLng(); setTimeout(() => lgOther.focus(), 0); return; }
C.set("languageValue", lgV.value);
applyLng();
});
lgOther.addEventListener("change", () => {
const v = String(lgOther.value).trim();
if (v) C.set("languageValue", v);
});
// React when the globe chip or the General row writes to the same setting.
if (C.onSettingsUpdate) C.onSettingsUpdate((next) => {
const m = (next.languageMode === "hide" || next.languageMode === "spoof") ? "show" : (next.languageMode || "show");
lngMode.value = m;
const t = next.languageValue || "en-GB";
if (known.has(t)) { lgV.value = t; lgOther.hidden = true; }
else { lgV.value = "__other__"; lgOther.value = t; }
applyLng();
});
// Language row moved out of Privacy › Anti-fingerprinting: the setting
// (languageMode / languageValue) still drives anti-fingerprinting, but
// the only control for it lives under Settings › Language now. Legacy
// "hide" and "spoof" values migrate to "show" (Automatic) on load so
// the new picker always has a valid state.
if (s.languageMode === "hide" || s.languageMode === "spoof") C.set("languageMode", "show");
// Location: three modes — Show real / Hide / Manual (pick a country).
// Manual writes the country's representative lat/lon into locationLat +
// locationLon, which is what navigator.geolocation returns to pages via