diff --git a/simulator/static/app.js b/simulator/static/app.js index 8bf34a1..8c0b7e9 100644 --- a/simulator/static/app.js +++ b/simulator/static/app.js @@ -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