feat(sirius-x): load the site in the reader's language, with a dropdown to switch
Visitors should not have to read English to register a name. The pages
stay authored in English; js/i18n.js picks the language from the saved
choice, ?lang=, or the browser, fetches i18n/<lang>.json (exact English
text -> translation) and rewrites text nodes, placeholders, titles and the
page title. A MutationObserver keeps script-built content (search rows,
register modal, portal rows, footer, profile menu) translated, so no page
script had to change. A dropdown in the top nav switches languages and
back to English losslessly. Code, hashes, addresses and record keys are
never touched. Ships German, Greek, Russian, Spanish, French and
Portuguese; the docs page keeps its English prose and only gains the
translated chrome.
2026-09-16 23:30:16 +02:00
|
|
|
// Sirius.X runtime translation.
|
|
|
|
|
//
|
|
|
|
|
// The pages are written in English. This script picks the reader's language
|
|
|
|
|
// (saved choice → ?lang= → browser languages → English), fetches
|
|
|
|
|
// i18n/<lang>.json — a flat map of English text → translation — and swaps
|
|
|
|
|
// every matching text node, placeholder, title and aria-label. A
|
|
|
|
|
// MutationObserver keeps script-built content (search rows, the register
|
|
|
|
|
// modal, portal rows, footer, profile menu) translated as it appears, so
|
|
|
|
|
// page scripts never need to know about languages.
|
|
|
|
|
//
|
|
|
|
|
// Rules of the road:
|
|
|
|
|
// • Keys are the exact English text of a node (whitespace collapsed).
|
|
|
|
|
// • <code>, <pre>, <kbd>, <textarea>, .mono and anything under
|
|
|
|
|
// [translate="no"] / [data-i18n-skip] is left alone — names, hashes,
|
|
|
|
|
// record keys and addresses must never change.
|
|
|
|
|
// • English is the source of truth: every node remembers its English
|
|
|
|
|
// text, so switching languages (or back to English) is lossless.
|
|
|
|
|
// • Adding a language = one JSON file in i18n/ + one line in LANGS.
|
|
|
|
|
//
|
|
|
|
|
// Exposed as window.siriusI18n = { lang, langs, t(text), setLang(code) }.
|
|
|
|
|
|
|
|
|
|
(function () {
|
|
|
|
|
var STORE = "sirius-lang";
|
|
|
|
|
var CACHE = "sirius-i18n:";
|
feat(portal): the name's settings show what its subdomains serve, and let you change it
The per-name tab was a DNS record table: eight record types, of which the
stack reads exactly one, and then only as a last resort when a name carries
no content record. Everything else was signed, stored and read by nobody —
furniture that looked like configuration. Meanwhile the thing a name owner
actually has, a folder tree that already answers on matching subdomains, was
not visible anywhere and could not be changed.
The tab now leads with what answers today: the apex, every folder in the
name's storage next to the subdomain it serves on, and which of those a rule
overrides. Below it, the rules — point a subdomain at a different folder,
redirect it, or switch it off.
Classic DNS keeps its table, behind a disclosure that says plainly what
those records do today and what they are waiting for. Nobody loses a record
they published; they just stop being the first thing a name owner is asked
to think about. The table gains a Host column, so records can belong to a
subdomain rather than only the apex.
Both halves are one signed manifest and one publish button — they always
were one file, and two buttons for it only ever invited a half-saved state.
Labels and values are separated in the generated rows so the words
translate and the owner's own folder and URL do not.
The signer accepted v1 manifests only, which would have rejected every
publish from this screen.
2026-09-28 01:21:47 +02:00
|
|
|
var VERSION = "20260928routing"; // bump when dictionaries change
|
feat(sirius-x): load the site in the reader's language, with a dropdown to switch
Visitors should not have to read English to register a name. The pages
stay authored in English; js/i18n.js picks the language from the saved
choice, ?lang=, or the browser, fetches i18n/<lang>.json (exact English
text -> translation) and rewrites text nodes, placeholders, titles and the
page title. A MutationObserver keeps script-built content (search rows,
register modal, portal rows, footer, profile menu) translated, so no page
script had to change. A dropdown in the top nav switches languages and
back to English losslessly. Code, hashes, addresses and record keys are
never touched. Ships German, Greek, Russian, Spanish, French and
Portuguese; the docs page keeps its English prose and only gains the
translated chrome.
2026-09-16 23:30:16 +02:00
|
|
|
var LANGS = {
|
|
|
|
|
en: "English",
|
|
|
|
|
de: "Deutsch",
|
|
|
|
|
el: "Ελληνικά",
|
|
|
|
|
ru: "Русский",
|
|
|
|
|
es: "Español",
|
|
|
|
|
fr: "Français",
|
|
|
|
|
pt: "Português",
|
|
|
|
|
};
|
|
|
|
|
var SKIP_TAGS = { SCRIPT: 1, STYLE: 1, CODE: 1, PRE: 1, KBD: 1, TEXTAREA: 1, NOSCRIPT: 1, SVG: 1 };
|
|
|
|
|
var ATTRS = ["placeholder", "title", "aria-label"];
|
|
|
|
|
|
|
|
|
|
var base = (function () {
|
|
|
|
|
try { return new URL("../i18n/", document.currentScript.src).href; }
|
|
|
|
|
catch (e) { return "./i18n/"; }
|
|
|
|
|
})();
|
|
|
|
|
|
|
|
|
|
var dict = null; // English -> translation for the active language
|
|
|
|
|
var lang = "en";
|
|
|
|
|
var applying = false;
|
|
|
|
|
|
|
|
|
|
// ---------- language choice ----------
|
|
|
|
|
function normalise(code) {
|
|
|
|
|
if (!code) return null;
|
|
|
|
|
var primary = String(code).toLowerCase().split(/[-_]/)[0];
|
|
|
|
|
return LANGS[primary] ? primary : null;
|
|
|
|
|
}
|
|
|
|
|
function detect() {
|
|
|
|
|
var fromQuery = null;
|
|
|
|
|
try { fromQuery = normalise(new URLSearchParams(location.search).get("lang")); } catch (e) {}
|
|
|
|
|
if (fromQuery) { save(fromQuery); return fromQuery; }
|
|
|
|
|
var saved = null;
|
|
|
|
|
try { saved = normalise(localStorage.getItem(STORE)); } catch (e) {}
|
|
|
|
|
if (saved) return saved;
|
|
|
|
|
var list = navigator.languages && navigator.languages.length ? navigator.languages : [navigator.language];
|
|
|
|
|
for (var i = 0; i < list.length; i++) { var n = normalise(list[i]); if (n) return n; }
|
|
|
|
|
return "en";
|
|
|
|
|
}
|
|
|
|
|
function save(code) { try { localStorage.setItem(STORE, code); } catch (e) {} }
|
|
|
|
|
|
|
|
|
|
// ---------- dictionary loading (cache first, network second) ----------
|
|
|
|
|
function readCache(code) {
|
|
|
|
|
try {
|
|
|
|
|
var raw = localStorage.getItem(CACHE + code);
|
|
|
|
|
if (!raw) return null;
|
|
|
|
|
var obj = JSON.parse(raw);
|
|
|
|
|
return obj && obj.v === VERSION ? obj.d : null;
|
|
|
|
|
} catch (e) { return null; }
|
|
|
|
|
}
|
|
|
|
|
function writeCache(code, d) {
|
|
|
|
|
try { localStorage.setItem(CACHE + code, JSON.stringify({ v: VERSION, d: d })); } catch (e) {}
|
|
|
|
|
}
|
|
|
|
|
function load(code) {
|
|
|
|
|
if (code === "en") return Promise.resolve({});
|
|
|
|
|
var cached = readCache(code);
|
|
|
|
|
var net = fetch(base + code + ".json?v=" + VERSION, { cache: "no-cache" })
|
|
|
|
|
.then(function (r) { if (!r.ok) throw new Error("i18n " + r.status); return r.json(); })
|
|
|
|
|
.then(function (d) { writeCache(code, d); return d; });
|
|
|
|
|
if (cached) { net.then(function (d) { if (lang === code) { dict = d; applyAll(); } }).catch(function () {}); return Promise.resolve(cached); }
|
|
|
|
|
return net;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---------- translation ----------
|
|
|
|
|
function t(text) {
|
|
|
|
|
if (!dict) return text;
|
|
|
|
|
var key = String(text).replace(/\s+/g, " ").trim();
|
|
|
|
|
// An empty string is a valid translation (the word is dropped in that
|
|
|
|
|
// language) — only a missing key falls back to English.
|
|
|
|
|
var out = Object.prototype.hasOwnProperty.call(dict, key) ? dict[key] : undefined;
|
|
|
|
|
return typeof out === "string" ? out : text;
|
|
|
|
|
}
|
|
|
|
|
function skip(el) {
|
|
|
|
|
for (var e = el; e && e.nodeType === 1; e = e.parentNode) {
|
|
|
|
|
if (SKIP_TAGS[e.tagName]) return true;
|
|
|
|
|
if (e.getAttribute) {
|
|
|
|
|
if (e.getAttribute("translate") === "no" || e.hasAttribute("data-i18n-skip")) return true;
|
|
|
|
|
var cls = e.getAttribute("class") || "";
|
|
|
|
|
if (/(^|\s)mono(\s|$)/.test(cls)) return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
function translateText(node) {
|
|
|
|
|
var cur = node.nodeValue;
|
|
|
|
|
if (!cur || !/\S/.test(cur)) return;
|
|
|
|
|
// A page script changed the text since we last touched it → new source.
|
|
|
|
|
if (node.__i18nOut !== cur) { node.__i18nSrc = cur; node.__i18nOut = null; }
|
|
|
|
|
var src = node.__i18nSrc != null ? node.__i18nSrc : cur;
|
|
|
|
|
var key = src.replace(/\s+/g, " ").trim();
|
|
|
|
|
var has = dict && key && Object.prototype.hasOwnProperty.call(dict, key) && typeof dict[key] === "string";
|
|
|
|
|
// Keep the node's leading/trailing whitespace (it separates inline
|
|
|
|
|
// siblings) but drop the source's internal line breaks — a multi-line
|
|
|
|
|
// paragraph in the HTML is one text node and must match its collapsed key.
|
|
|
|
|
var next = has
|
|
|
|
|
? src.match(/^\s*/)[0] + dict[key] + src.match(/\s*$/)[0]
|
|
|
|
|
: src;
|
|
|
|
|
if (next !== cur) node.nodeValue = next;
|
|
|
|
|
node.__i18nSrc = src;
|
|
|
|
|
node.__i18nOut = next;
|
|
|
|
|
}
|
|
|
|
|
function translateAttrs(el) {
|
|
|
|
|
for (var i = 0; i < ATTRS.length; i++) {
|
|
|
|
|
var a = ATTRS[i];
|
|
|
|
|
if (!el.hasAttribute(a)) continue;
|
|
|
|
|
var cur = el.getAttribute(a);
|
|
|
|
|
var orig = el.getAttribute("data-i18n-orig-" + a);
|
|
|
|
|
if (orig == null || el.getAttribute("data-i18n-out-" + a) !== cur) { orig = cur; el.setAttribute("data-i18n-orig-" + a, orig); }
|
|
|
|
|
var next = t(orig);
|
|
|
|
|
if (next !== cur) el.setAttribute(a, next);
|
|
|
|
|
el.setAttribute("data-i18n-out-" + a, next);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
function walk(root) {
|
|
|
|
|
if (!root) return;
|
|
|
|
|
if (root.nodeType === 3) { if (!skip(root.parentNode)) translateText(root); return; }
|
|
|
|
|
if (root.nodeType !== 1 && root.nodeType !== 11) return;
|
|
|
|
|
if (root.nodeType === 1) {
|
|
|
|
|
if (skip(root)) return;
|
|
|
|
|
translateAttrs(root);
|
|
|
|
|
}
|
|
|
|
|
var tw = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT, null);
|
|
|
|
|
var n;
|
|
|
|
|
while ((n = tw.nextNode())) {
|
|
|
|
|
if (n.nodeType === 1) { if (!skip(n)) translateAttrs(n); }
|
|
|
|
|
else if (!skip(n.parentNode)) translateText(n);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
function applyAll() {
|
|
|
|
|
applying = true;
|
|
|
|
|
try {
|
|
|
|
|
walk(document.body);
|
|
|
|
|
if (document.title) {
|
|
|
|
|
if (!document.__i18nTitle) document.__i18nTitle = document.title;
|
|
|
|
|
document.title = t(document.__i18nTitle);
|
|
|
|
|
}
|
|
|
|
|
document.documentElement.setAttribute("lang", lang);
|
|
|
|
|
} finally { applying = false; }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Keep script-built content translated. Mutations we cause ourselves are
|
|
|
|
|
// ignored (applying flag); everything else is walked as it lands.
|
|
|
|
|
var observer = null;
|
|
|
|
|
function observe() {
|
|
|
|
|
if (observer || !window.MutationObserver) return;
|
|
|
|
|
observer = new MutationObserver(function (records) {
|
|
|
|
|
if (applying || !dict) return;
|
|
|
|
|
applying = true;
|
|
|
|
|
try {
|
|
|
|
|
for (var i = 0; i < records.length; i++) {
|
|
|
|
|
var r = records[i];
|
|
|
|
|
if (r.type === "characterData") { if (!skip(r.target.parentNode)) translateText(r.target); }
|
|
|
|
|
else if (r.type === "attributes") { if (r.target.nodeType === 1 && !skip(r.target)) translateAttrs(r.target); }
|
|
|
|
|
else for (var j = 0; j < r.addedNodes.length; j++) walk(r.addedNodes[j]);
|
|
|
|
|
}
|
|
|
|
|
} finally { applying = false; }
|
|
|
|
|
});
|
|
|
|
|
observer.observe(document.documentElement, {
|
|
|
|
|
childList: true, subtree: true, characterData: true,
|
|
|
|
|
attributes: true, attributeFilter: ATTRS,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---------- switching ----------
|
|
|
|
|
function setLang(code) {
|
|
|
|
|
code = normalise(code) || "en";
|
|
|
|
|
lang = code;
|
|
|
|
|
save(code);
|
|
|
|
|
return load(code).then(function (d) {
|
|
|
|
|
if (lang !== code) return;
|
|
|
|
|
dict = code === "en" ? null : d;
|
|
|
|
|
applyAll();
|
|
|
|
|
if (!dict) { document.documentElement.setAttribute("lang", "en"); }
|
|
|
|
|
refreshPicker();
|
|
|
|
|
}).catch(function () {
|
|
|
|
|
// Dictionary unreachable → stay on English rather than half-translate.
|
|
|
|
|
dict = null; lang = "en"; applyAll(); refreshPicker();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---------- the dropdown ----------
|
2026-09-17 00:36:55 +02:00
|
|
|
// A custom menu rather than a <select>: a native select cannot show
|
|
|
|
|
// flags, and emoji flags render as bare letters on Windows. Flags are
|
|
|
|
|
// tiny inline SVGs (3:2, simplified) so no third-party image request
|
|
|
|
|
// ever leaves the visitor's browser. Sits on the right of the top nav,
|
|
|
|
|
// just before the wallet button.
|
|
|
|
|
var FLAGS = {
|
|
|
|
|
en: '<rect width="60" height="40" fill="#012169"/><path d="M0 0L60 40M60 0L0 40" stroke="#fff" stroke-width="8"/><path d="M0 0L60 40M60 0L0 40" stroke="#C8102E" stroke-width="3"/><path d="M30 0V40M0 20H60" stroke="#fff" stroke-width="10"/><path d="M30 0V40M0 20H60" stroke="#C8102E" stroke-width="6"/>',
|
|
|
|
|
de: '<rect width="60" height="40" fill="#000"/><rect y="13.33" width="60" height="13.34" fill="#D00"/><rect y="26.67" width="60" height="13.33" fill="#FFCE00"/>',
|
|
|
|
|
el: '<rect width="60" height="40" fill="#0D5EAF"/><rect y="4.44" width="60" height="4.45" fill="#fff"/><rect y="13.33" width="60" height="4.45" fill="#fff"/><rect y="22.22" width="60" height="4.45" fill="#fff"/><rect y="31.11" width="60" height="4.45" fill="#fff"/><rect width="22.22" height="22.22" fill="#0D5EAF"/><rect x="8.89" width="4.44" height="22.22" fill="#fff"/><rect y="8.89" width="22.22" height="4.44" fill="#fff"/>',
|
|
|
|
|
ru: '<rect width="60" height="40" fill="#fff"/><rect y="13.33" width="60" height="13.34" fill="#0039A6"/><rect y="26.67" width="60" height="13.33" fill="#D52B1E"/>',
|
|
|
|
|
es: '<rect width="60" height="40" fill="#AA151B"/><rect y="10" width="60" height="20" fill="#F1BF00"/>',
|
|
|
|
|
fr: '<rect width="20" height="40" fill="#0055A4"/><rect x="20" width="20" height="40" fill="#fff"/><rect x="40" width="20" height="40" fill="#EF4135"/>',
|
|
|
|
|
pt: '<rect width="60" height="40" fill="#F00"/><rect width="24" height="40" fill="#060"/><circle cx="24" cy="20" r="7" fill="#FFE000"/><circle cx="24" cy="20" r="3.5" fill="#F00"/>',
|
|
|
|
|
};
|
|
|
|
|
var GLOBE = '<svg class="lp-globe" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path d="M3 12h18M12 3c3 3.5 3 14.5 0 18M12 3c-3 3.5-3 14.5 0 18"/></svg>';
|
|
|
|
|
var CARET = '<svg class="lp-caret" viewBox="0 0 10 6" aria-hidden="true"><path d="M1 1l4 4 4-4" fill="none" stroke="currentColor" stroke-width="1.6"/></svg>';
|
|
|
|
|
function flagSvg(code) {
|
|
|
|
|
return '<svg class="lp-flag" viewBox="0 0 60 40" aria-hidden="true">' + (FLAGS[code] || "") + "</svg>";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var picker = null, pickerBtn = null, pickerMenu = null;
|
|
|
|
|
function closePicker() {
|
|
|
|
|
if (!picker) return;
|
|
|
|
|
picker.classList.remove("open");
|
|
|
|
|
pickerBtn.setAttribute("aria-expanded", "false");
|
|
|
|
|
}
|
|
|
|
|
function openPicker() {
|
|
|
|
|
if (!picker) return;
|
|
|
|
|
picker.classList.add("open");
|
|
|
|
|
pickerBtn.setAttribute("aria-expanded", "true");
|
|
|
|
|
var active = pickerMenu.querySelector('[aria-selected="true"]');
|
|
|
|
|
if (active) active.focus();
|
|
|
|
|
}
|
feat(sirius-x): load the site in the reader's language, with a dropdown to switch
Visitors should not have to read English to register a name. The pages
stay authored in English; js/i18n.js picks the language from the saved
choice, ?lang=, or the browser, fetches i18n/<lang>.json (exact English
text -> translation) and rewrites text nodes, placeholders, titles and the
page title. A MutationObserver keeps script-built content (search rows,
register modal, portal rows, footer, profile menu) translated, so no page
script had to change. A dropdown in the top nav switches languages and
back to English losslessly. Code, hashes, addresses and record keys are
never touched. Ships German, Greek, Russian, Spanish, French and
Portuguese; the docs page keeps its English prose and only gains the
translated chrome.
2026-09-16 23:30:16 +02:00
|
|
|
function paintPicker() {
|
|
|
|
|
if (picker) return;
|
|
|
|
|
var css = document.createElement("style");
|
|
|
|
|
css.textContent =
|
2026-09-17 00:36:55 +02:00
|
|
|
".lang-picker{position:relative;margin-left:auto;font-family:var(--sans,inherit)}" +
|
|
|
|
|
".topnav .lang-picker + .profile-wrap,.topnav .lang-picker + a.portal{margin-left:8px}" +
|
|
|
|
|
".lp-btn{display:inline-flex;align-items:center;gap:7px;padding:5px 10px 5px 9px;border-radius:999px;" +
|
|
|
|
|
"background:rgba(255,255,255,.05);border:1px solid rgba(255,255,255,.1);color:var(--mut,#b8c2d4);" +
|
|
|
|
|
"font:inherit;font-size:12.5px;line-height:1;cursor:pointer;transition:border-color .12s,color .12s,background .12s}" +
|
|
|
|
|
".lp-btn:hover,.lang-picker.open .lp-btn{color:var(--ink,#f1f4fa);border-color:rgba(214,255,61,.35);background:rgba(214,255,61,.08)}" +
|
|
|
|
|
".lp-globe{width:15px;height:15px;opacity:.85;flex:none}" +
|
|
|
|
|
".lp-flag{width:20px;height:14px;border-radius:2px;box-shadow:0 0 0 1px rgba(255,255,255,.14);flex:none}" +
|
|
|
|
|
".lp-caret{width:9px;height:9px;opacity:.7;transition:transform .15s;flex:none}" +
|
|
|
|
|
".lang-picker.open .lp-caret{transform:rotate(180deg)}" +
|
|
|
|
|
".lp-menu{position:absolute;right:0;top:calc(100% + 8px);min-width:200px;padding:6px;border-radius:12px;" +
|
|
|
|
|
"background:var(--panel,#141a24);border:1px solid rgba(255,255,255,.1);box-shadow:0 12px 32px rgba(0,0,0,.45);z-index:200;display:none}" +
|
|
|
|
|
".lang-picker.open .lp-menu{display:block}" +
|
|
|
|
|
".lp-opt{display:flex;align-items:center;gap:10px;width:100%;padding:8px 10px;border:0;border-radius:8px;" +
|
|
|
|
|
"background:transparent;color:var(--mut,#b8c2d4);font:inherit;font-size:13.5px;text-align:left;cursor:pointer}" +
|
|
|
|
|
".lp-opt:hover,.lp-opt:focus-visible{background:rgba(255,255,255,.06);color:var(--ink,#f1f4fa);outline:none}" +
|
|
|
|
|
'.lp-opt[aria-selected="true"]{color:var(--acid,#d6ff3d);background:rgba(214,255,61,.08)}' +
|
|
|
|
|
".lp-opt .lp-code{margin-left:auto;font-family:var(--mono,monospace);font-size:10.5px;letter-spacing:.08em;text-transform:uppercase;opacity:.55}" +
|
|
|
|
|
".lang-picker-float{position:fixed;bottom:20px;right:20px;z-index:99}" +
|
2026-09-20 15:56:29 +02:00
|
|
|
".lang-picker-inline{margin-left:0}" +
|
2026-09-17 00:36:55 +02:00
|
|
|
"@media (max-width:560px){.lp-name{display:none}}";
|
feat(sirius-x): load the site in the reader's language, with a dropdown to switch
Visitors should not have to read English to register a name. The pages
stay authored in English; js/i18n.js picks the language from the saved
choice, ?lang=, or the browser, fetches i18n/<lang>.json (exact English
text -> translation) and rewrites text nodes, placeholders, titles and the
page title. A MutationObserver keeps script-built content (search rows,
register modal, portal rows, footer, profile menu) translated, so no page
script had to change. A dropdown in the top nav switches languages and
back to English losslessly. Code, hashes, addresses and record keys are
never touched. Ships German, Greek, Russian, Spanish, French and
Portuguese; the docs page keeps its English prose and only gains the
translated chrome.
2026-09-16 23:30:16 +02:00
|
|
|
document.head.appendChild(css);
|
2026-09-17 00:36:55 +02:00
|
|
|
|
|
|
|
|
picker = document.createElement("div");
|
feat(sirius-x): load the site in the reader's language, with a dropdown to switch
Visitors should not have to read English to register a name. The pages
stay authored in English; js/i18n.js picks the language from the saved
choice, ?lang=, or the browser, fetches i18n/<lang>.json (exact English
text -> translation) and rewrites text nodes, placeholders, titles and the
page title. A MutationObserver keeps script-built content (search rows,
register modal, portal rows, footer, profile menu) translated, so no page
script had to change. A dropdown in the top nav switches languages and
back to English losslessly. Code, hashes, addresses and record keys are
never touched. Ships German, Greek, Russian, Spanish, French and
Portuguese; the docs page keeps its English prose and only gains the
translated chrome.
2026-09-16 23:30:16 +02:00
|
|
|
picker.className = "lang-picker";
|
|
|
|
|
picker.setAttribute("translate", "no");
|
2026-09-17 00:36:55 +02:00
|
|
|
pickerBtn = document.createElement("button");
|
|
|
|
|
pickerBtn.type = "button";
|
|
|
|
|
pickerBtn.className = "lp-btn";
|
|
|
|
|
pickerBtn.setAttribute("aria-haspopup", "listbox");
|
|
|
|
|
pickerBtn.setAttribute("aria-expanded", "false");
|
|
|
|
|
pickerBtn.setAttribute("aria-label", "Language");
|
|
|
|
|
pickerBtn.title = "Language";
|
|
|
|
|
pickerMenu = document.createElement("div");
|
|
|
|
|
pickerMenu.className = "lp-menu";
|
|
|
|
|
pickerMenu.setAttribute("role", "listbox");
|
|
|
|
|
pickerMenu.setAttribute("aria-label", "Language");
|
feat(sirius-x): load the site in the reader's language, with a dropdown to switch
Visitors should not have to read English to register a name. The pages
stay authored in English; js/i18n.js picks the language from the saved
choice, ?lang=, or the browser, fetches i18n/<lang>.json (exact English
text -> translation) and rewrites text nodes, placeholders, titles and the
page title. A MutationObserver keeps script-built content (search rows,
register modal, portal rows, footer, profile menu) translated, so no page
script had to change. A dropdown in the top nav switches languages and
back to English losslessly. Code, hashes, addresses and record keys are
never touched. Ships German, Greek, Russian, Spanish, French and
Portuguese; the docs page keeps its English prose and only gains the
translated chrome.
2026-09-16 23:30:16 +02:00
|
|
|
var html = "";
|
2026-09-17 00:36:55 +02:00
|
|
|
for (var code in LANGS) {
|
|
|
|
|
html += '<button type="button" class="lp-opt" role="option" data-lang="' + code + '" aria-selected="false" tabindex="-1">' +
|
|
|
|
|
flagSvg(code) + "<span>" + LANGS[code] + '</span><span class="lp-code">' + code + "</span></button>";
|
|
|
|
|
}
|
|
|
|
|
pickerMenu.innerHTML = html;
|
|
|
|
|
picker.appendChild(pickerBtn);
|
|
|
|
|
picker.appendChild(pickerMenu);
|
|
|
|
|
|
|
|
|
|
pickerBtn.addEventListener("click", function (e) {
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
if (picker.classList.contains("open")) closePicker(); else openPicker();
|
|
|
|
|
});
|
|
|
|
|
pickerMenu.addEventListener("click", function (e) {
|
|
|
|
|
var opt = e.target.closest ? e.target.closest("[data-lang]") : null;
|
|
|
|
|
if (!opt) return;
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
closePicker();
|
|
|
|
|
pickerBtn.focus();
|
|
|
|
|
setLang(opt.getAttribute("data-lang"));
|
|
|
|
|
});
|
|
|
|
|
picker.addEventListener("keydown", function (e) {
|
|
|
|
|
var opts = pickerMenu.querySelectorAll("[data-lang]");
|
|
|
|
|
var i = Array.prototype.indexOf.call(opts, document.activeElement);
|
|
|
|
|
if (e.key === "Escape") { closePicker(); pickerBtn.focus(); }
|
|
|
|
|
else if (e.key === "ArrowDown") { e.preventDefault(); if (!picker.classList.contains("open")) openPicker(); else opts[Math.min(i + 1, opts.length - 1)].focus(); }
|
|
|
|
|
else if (e.key === "ArrowUp") { e.preventDefault(); if (i > 0) opts[i - 1].focus(); }
|
|
|
|
|
});
|
|
|
|
|
document.addEventListener("click", closePicker);
|
|
|
|
|
|
2026-09-20 15:56:29 +02:00
|
|
|
var slot = document.querySelector("[data-lang-slot]");
|
feat(sirius-x): load the site in the reader's language, with a dropdown to switch
Visitors should not have to read English to register a name. The pages
stay authored in English; js/i18n.js picks the language from the saved
choice, ?lang=, or the browser, fetches i18n/<lang>.json (exact English
text -> translation) and rewrites text nodes, placeholders, titles and the
page title. A MutationObserver keeps script-built content (search rows,
register modal, portal rows, footer, profile menu) translated, so no page
script had to change. A dropdown in the top nav switches languages and
back to English losslessly. Code, hashes, addresses and record keys are
never touched. Ships German, Greek, Russian, Spanish, French and
Portuguese; the docs page keeps its English prose and only gains the
translated chrome.
2026-09-16 23:30:16 +02:00
|
|
|
var nav = document.querySelector(".topnav");
|
2026-09-20 15:56:29 +02:00
|
|
|
if (slot) { slot.replaceWith(picker); picker.classList.add("lang-picker-inline"); } else
|
feat(sirius-x): load the site in the reader's language, with a dropdown to switch
Visitors should not have to read English to register a name. The pages
stay authored in English; js/i18n.js picks the language from the saved
choice, ?lang=, or the browser, fetches i18n/<lang>.json (exact English
text -> translation) and rewrites text nodes, placeholders, titles and the
page title. A MutationObserver keeps script-built content (search rows,
register modal, portal rows, footer, profile menu) translated, so no page
script had to change. A dropdown in the top nav switches languages and
back to English losslessly. Code, hashes, addresses and record keys are
never touched. Ships German, Greek, Russian, Spanish, French and
Portuguese; the docs page keeps its English prose and only gains the
translated chrome.
2026-09-16 23:30:16 +02:00
|
|
|
if (nav) {
|
2026-09-17 00:36:55 +02:00
|
|
|
// Right side: directly before the wallet button (profile-wrap once
|
|
|
|
|
// profile-menu.js has wrapped the portal link, the bare link before
|
|
|
|
|
// that), after everything else.
|
2026-09-17 03:28:50 +02:00
|
|
|
// Order on the right: language · Dashboard (when signed in) · wallet.
|
|
|
|
|
var wallet = nav.querySelector(".dash-link") || nav.querySelector(".profile-wrap") || nav.querySelector("a.portal");
|
2026-09-17 00:36:55 +02:00
|
|
|
if (wallet) nav.insertBefore(picker, wallet); else nav.appendChild(picker);
|
feat(sirius-x): load the site in the reader's language, with a dropdown to switch
Visitors should not have to read English to register a name. The pages
stay authored in English; js/i18n.js picks the language from the saved
choice, ?lang=, or the browser, fetches i18n/<lang>.json (exact English
text -> translation) and rewrites text nodes, placeholders, titles and the
page title. A MutationObserver keeps script-built content (search rows,
register modal, portal rows, footer, profile menu) translated, so no page
script had to change. A dropdown in the top nav switches languages and
back to English losslessly. Code, hashes, addresses and record keys are
never touched. Ships German, Greek, Russian, Spanish, French and
Portuguese; the docs page keeps its English prose and only gains the
translated chrome.
2026-09-16 23:30:16 +02:00
|
|
|
} else {
|
|
|
|
|
picker.classList.add("lang-picker-float");
|
|
|
|
|
document.body.appendChild(picker);
|
|
|
|
|
}
|
|
|
|
|
refreshPicker();
|
|
|
|
|
}
|
2026-09-17 00:36:55 +02:00
|
|
|
function refreshPicker() {
|
|
|
|
|
if (!picker) return;
|
|
|
|
|
pickerBtn.innerHTML = GLOBE + flagSvg(lang) + '<span class="lp-name">' + (LANGS[lang] || lang) + "</span>" + CARET;
|
|
|
|
|
var opts = pickerMenu.querySelectorAll("[data-lang]");
|
|
|
|
|
for (var i = 0; i < opts.length; i++) {
|
|
|
|
|
opts[i].setAttribute("aria-selected", opts[i].getAttribute("data-lang") === lang ? "true" : "false");
|
|
|
|
|
}
|
|
|
|
|
}
|
feat(sirius-x): load the site in the reader's language, with a dropdown to switch
Visitors should not have to read English to register a name. The pages
stay authored in English; js/i18n.js picks the language from the saved
choice, ?lang=, or the browser, fetches i18n/<lang>.json (exact English
text -> translation) and rewrites text nodes, placeholders, titles and the
page title. A MutationObserver keeps script-built content (search rows,
register modal, portal rows, footer, profile menu) translated, so no page
script had to change. A dropdown in the top nav switches languages and
back to English losslessly. Code, hashes, addresses and record keys are
never touched. Ships German, Greek, Russian, Spanish, French and
Portuguese; the docs page keeps its English prose and only gains the
translated chrome.
2026-09-16 23:30:16 +02:00
|
|
|
|
|
|
|
|
// ---------- boot ----------
|
|
|
|
|
lang = detect();
|
|
|
|
|
var ready = lang === "en" ? Promise.resolve(null) : load(lang).then(function (d) { dict = d; return d; }).catch(function () { lang = "en"; return null; });
|
|
|
|
|
function boot() {
|
|
|
|
|
observe();
|
2026-09-17 00:36:55 +02:00
|
|
|
ready.then(function () { applyAll(); });
|
feat(sirius-x): load the site in the reader's language, with a dropdown to switch
Visitors should not have to read English to register a name. The pages
stay authored in English; js/i18n.js picks the language from the saved
choice, ?lang=, or the browser, fetches i18n/<lang>.json (exact English
text -> translation) and rewrites text nodes, placeholders, titles and the
page title. A MutationObserver keeps script-built content (search rows,
register modal, portal rows, footer, profile menu) translated, so no page
script had to change. A dropdown in the top nav switches languages and
back to English losslessly. Code, hashes, addresses and record keys are
never touched. Ships German, Greek, Russian, Spanish, French and
Portuguese; the docs page keeps its English prose and only gains the
translated chrome.
2026-09-16 23:30:16 +02:00
|
|
|
}
|
|
|
|
|
if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", boot);
|
|
|
|
|
else boot();
|
2026-09-17 00:36:55 +02:00
|
|
|
// The picker is painted on `load`, after the deferred nav scripts
|
|
|
|
|
// (theme.js font pill, profile-menu.js wallet button) have laid out the
|
|
|
|
|
// top nav, so it lands in a stable spot on the right.
|
|
|
|
|
if (document.readyState === "complete") paintPicker();
|
|
|
|
|
else window.addEventListener("load", paintPicker);
|
feat(sirius-x): load the site in the reader's language, with a dropdown to switch
Visitors should not have to read English to register a name. The pages
stay authored in English; js/i18n.js picks the language from the saved
choice, ?lang=, or the browser, fetches i18n/<lang>.json (exact English
text -> translation) and rewrites text nodes, placeholders, titles and the
page title. A MutationObserver keeps script-built content (search rows,
register modal, portal rows, footer, profile menu) translated, so no page
script had to change. A dropdown in the top nav switches languages and
back to English losslessly. Code, hashes, addresses and record keys are
never touched. Ships German, Greek, Russian, Spanish, French and
Portuguese; the docs page keeps its English prose and only gains the
translated chrome.
2026-09-16 23:30:16 +02:00
|
|
|
|
|
|
|
|
window.siriusI18n = {
|
|
|
|
|
get lang() { return lang; },
|
|
|
|
|
langs: LANGS,
|
|
|
|
|
t: t,
|
|
|
|
|
setLang: setLang,
|
|
|
|
|
apply: function (root) { if (dict) { applying = true; try { walk(root || document.body); } finally { applying = false; } } },
|
|
|
|
|
};
|
|
|
|
|
})();
|