translations and pickers

This commit is contained in:
Mateusz Gruszczyński
2026-09-05 08:56:45 +02:00
parent e68ad69bc5
commit bf34a7cab1
18 changed files with 606 additions and 63 deletions
+47 -11
View File
@@ -7,6 +7,8 @@
*/
const STORAGE_KEY = "rustpad:language";
const GUEST_STORAGE_KEY = "rustpad:guest-language";
const AUTH_STATE_KEY = "rustpad:auth-state";
const FALLBACK_LANGUAGE = "en";
const TRANSLATABLE_ATTRIBUTES = ["placeholder", "title", "aria-label", "aria-description"];
const SKIP_SELECTOR = [
@@ -285,21 +287,50 @@ async function loadBundle(code) {
return bundle;
}
function hasAccountSession() {
try { return localStorage.getItem(AUTH_STATE_KEY) === "1"; } catch { return false; }
}
function preferredCode() {
// English is always the default. A cached language is only used while an
// authenticated profile is expected; the server session remains the source
// of truth and will re-apply its saved preference after validation.
let hasAccountSession = false;
let saved = "";
try {
hasAccountSession = localStorage.getItem("rustpad:auth-state") === "1";
if (hasAccountSession) saved = localStorage.getItem(STORAGE_KEY) || "";
saved = hasAccountSession()
? (localStorage.getItem(STORAGE_KEY) || "")
: (localStorage.getItem(GUEST_STORAGE_KEY) || localStorage.getItem(STORAGE_KEY) || "");
} catch { /* storage may be blocked */ }
if (!hasAccountSession) return FALLBACK_LANGUAGE;
const available = new Set(catalog.map(item => item.code));
return available.has(saved) ? saved : FALLBACK_LANGUAGE;
}
function persistLanguage(code, scope = "auto") {
try {
localStorage.setItem(STORAGE_KEY, code);
const guestScope = scope === "guest" || (scope === "auto" && !hasAccountSession());
if (guestScope) localStorage.setItem(GUEST_STORAGE_KEY, code);
} catch { /* storage may be blocked */ }
}
export function rememberGuestLanguage(code = activeCode) {
try {
const available = new Set(catalog.map(item => item.code));
const saved = localStorage.getItem(GUEST_STORAGE_KEY);
if (available.has(saved)) return;
const value = available.has(code) ? code : FALLBACK_LANGUAGE;
localStorage.setItem(GUEST_STORAGE_KEY, value);
} catch { /* storage may be blocked */ }
}
export async function restoreGuestLanguage() {
let code = FALLBACK_LANGUAGE;
try { code = localStorage.getItem(GUEST_STORAGE_KEY) || FALLBACK_LANGUAGE; } catch { /* storage may be blocked */ }
const applied = await setLanguage(code, { persist: false });
try {
localStorage.setItem(STORAGE_KEY, applied);
localStorage.setItem(GUEST_STORAGE_KEY, applied);
} catch { /* storage may be blocked */ }
return applied;
}
function applyDocumentLanguage() {
const meta = activeBundle?.meta || fallbackBundle?.meta;
if (!meta) return;
@@ -307,16 +338,14 @@ function applyDocumentLanguage() {
document.documentElement.dataset.locale = meta.locale;
}
export async function setLanguage(code, { persist = true } = {}) {
export async function setLanguage(code, { persist = true, scope = "auto" } = {}) {
await initI18n();
const available = catalog.find(item => item.code === code);
const target = available ? available.code : FALLBACK_LANGUAGE;
activeBundle = await loadBundle(target);
activeCode = activeBundle.meta.code;
rebuildSourceIndex();
if (persist) {
try { localStorage.setItem(STORAGE_KEY, activeCode); } catch { /* storage may be blocked */ }
}
if (persist) persistLanguage(activeCode, scope);
applyDocumentLanguage();
translateTree(document, true);
document.dispatchEvent(new CustomEvent("rustpad:languagechange", {
@@ -473,6 +502,13 @@ export function populateLanguageSelect(input, selectedCode = getLanguage()) {
}));
}
window.addEventListener("storage", event => {
if (event.key !== STORAGE_KEY && event.key !== GUEST_STORAGE_KEY && event.key !== AUTH_STATE_KEY) return;
if (event.key === GUEST_STORAGE_KEY && hasAccountSession()) return;
const next = preferredCode();
if (next !== activeCode) void setLanguage(next, { persist: false });
});
export function formatDateTime(value, options) {
const date = value instanceof Date ? value : new Date(value);
if (Number.isNaN(date.getTime())) return String(value ?? "");