feat(i18n): live language switching for chrome + annotations

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
BenStullsBets
2026-06-29 18:48:48 -07:00
parent 49b77e0700
commit 1977e679ee
+41 -3
View File
@@ -53,6 +53,8 @@ let currentClipId = null; // base media currently loaded (reset to force a relo
let busy = false; // true while a ring transition is playing
let morphByPair = {}; // "fromClip→toClip" -> directed morph file (built from the ring)
let lastOverlay = null; // {level, intensity} from the most-recent renderOverlay call; drives per-frame track animation
let activeLang = "en"; // session-only; no persistence (resets to en each load)
let lastAffect = null; // {strength, intensity, right} from the last renderAffect, for re-render on language switch
async function loadData() {
const clips = (await (await fetch("/api/clips")).json()).clips || [];
@@ -502,7 +504,7 @@ function renderOverlay(level, intensity) {
overlay.innerHTML = "";
if (!clip || level <= 0) { overlay.style.opacity = "0"; return; }
overlay.style.opacity = String(intensity);
const strings = (clip.strings && clip.strings.en) || {};
const strings = HEFi18n.resolveStrings(clip.strings, activeLang);
const t = loopT();
let shown = 0;
for (const a of clip.annotations) {
@@ -548,12 +550,13 @@ function renderOverlay(level, intensity) {
// scene points (no boxes — feelings are scene-level) and read softer than the
// clinical reticles — feeling, not measurement.
function renderAffect(strength, intensity, right) {
lastAffect = { strength, intensity, right };
const clip = activeClip();
if (!affectLayer) return; // tolerate a stale page missing the affect layer
affectLayer.innerHTML = "";
if (!clip || !clip.affect || strength <= 0) { affectLayer.style.opacity = "0"; return; }
affectLayer.style.opacity = String(intensity);
const strings = (clip.strings && clip.strings.en) || {};
const strings = HEFi18n.resolveStrings(clip.strings, activeLang);
for (const f of clip.affect) {
if (f.min_level > strength) continue;
const [x, y] = f.at.map((n) => n * 100);
@@ -572,7 +575,8 @@ function renderScaleReadout() {
const member = (activeClip() && activeClip().title) || s.title;
// scale id · the chosen pool member · position on the ring (pool size if >1)
const poolTag = poolN > 1 ? ` · pool ${poolN}` : "";
$("scale-name").textContent = `${s.id} · ${member} (${ringIndex + 1}/${ring.scales.length}${poolTag})`;
const scaleName = HEFi18n.pickUiString("scale." + s.id, activeLang);
$("scale-name").textContent = `${scaleName} · ${member} (${ringIndex + 1}/${ring.scales.length}${poolTag})`;
renderDial();
refreshDevClip(); // keep the Dev Mode pool picker + clip data in sync with the landing
}
@@ -1218,6 +1222,7 @@ async function main() {
await landScale(); // pick the initial scale's pool member before first render
buildDial(); // draw the altitude knob from the ring's scales
initDev(); // wire the Dev Mode toggle + pool picker (reads persisted state)
initLanguage(); // populate the language dropdown + wire live switching
renderScaleReadout();
// Sliders stream on "input"; the toggles fire on "change".
for (const id of ["left", "right", "mood"]) $(id).addEventListener("input", debounced);
@@ -1248,6 +1253,39 @@ async function main() {
autoStart(); // once loaded: video on + gentle audio (3/10), no gesture needed
}
// Populate the language dropdown from the registry and wire live switching.
// English default, session-only (no persistence). Switching swaps UI chrome
// and re-renders the current annotations/affect in place — no reload, no fetch.
function initLanguage() {
const sel = $("lang-select");
if (!sel) return;
for (const { code, nativeName } of HEFi18n.LANGUAGES) {
const o = document.createElement("option");
o.value = code;
o.textContent = nativeName;
sel.appendChild(o);
}
sel.value = activeLang;
applyUiStrings(activeLang);
sel.addEventListener("change", () => setLanguage(sel.value));
}
// Fill every [data-i18n] node with its catalog string for `lang`.
function applyUiStrings(lang) {
document.documentElement.lang = lang;
for (const el of document.querySelectorAll("[data-i18n]")) {
el.textContent = HEFi18n.pickUiString(el.getAttribute("data-i18n"), lang);
}
}
function setLanguage(lang) {
activeLang = lang;
applyUiStrings(lang);
renderScaleReadout();
if (lastOverlay) renderOverlay(lastOverlay.level, lastOverlay.intensity);
if (lastAffect) renderAffect(lastAffect.strength, lastAffect.intensity, lastAffect.right);
}
// Auto-start the experience the moment media is ready: bring Video on and the audio
// level to 3/10 (the same gentle default the first manual Video-on couples in). The
// audio <audio> elements ideally unlock inside a user gesture (Safari); this fires