export type Language = 'en' | 'da'; export const languageOptions: { id: Language; label: string }[] = [ { id: 'en', label: 'English' }, { id: 'da', label: 'Dansk' }, ]; export interface Chrome { subtitle: string; title: string; documentTitle: string; metaDescription: string; home: string; collection: string; language: string; test: string; experiments: string; overview: string; overviewTitle: string; overviewText: string; score: string; subjects: string; lessons: string; reset: string; original: string; parameters: string; measurements: string; camera: string; pause: string; resume: string; hint: string; notice: string; connect: string; physics: string; learn: string; try: string; scales: string; build: string; reasoning: string; originalAnswers: string; predict: string; reveal: string; hide: string; navigation: string; previous: string; next: string; view: string; viewport: string; study: string; canvas: string; webglStart: string; webglLost: string; experimentKicker: (index: number, total: number) => string; questionKicker: (section: string, question: number, points: number) => string; } const pad = (value: number) => String(value).padStart(2, '0'); const chrome: Record = { en: { subtitle: 'Explore the invisible. Understand the physical.', title: 'Electrotechnique & Electrophysics', documentTitle: 'Test 4 in 3D · Electrophysics Lab', metaDescription: 'Learn electric work, potential energy and dipole rotation through six interactive 3D lessons from Test 4.', home: 'Field Lab home', collection: 'Learning collection', language: 'Language', test: 'Test 4 in 3D', experiments: 'All experiments', overview: 'FROM EQUATIONS TO INTUITION', overviewTitle: 'Electric energy, made visible.', overviewText: 'Explore the six questions from Test 4. Change the scene, make a prediction, then follow the derivation.', score: '6 questions · 3D study guide', subjects: 'Electrophysics subjects', lessons: 'Lessons', reset: 'Reset experiment', original: 'Original test question', parameters: 'Experiment parameters', measurements: 'Live measurements', camera: 'Reset camera', pause: 'Pause animation', resume: 'Resume animation', hint: 'Drag to orbit · Scroll to zoom · Right-drag to pan · Arrow keys to pan when focused', notice: 'NOTICE WHAT CHANGES', connect: 'Connect the scene to the answer', physics: 'THE PHYSICS', learn: 'Learn the physics', try: 'TRY THIS', scales: 'Model & visual scales', build: 'BUILD YOUR ANSWER', reasoning: 'Follow the reasoning', originalAnswers: 'Original answers ↗', predict: 'PAUSE & PREDICT', reveal: 'Reveal explanation', hide: 'Hide explanation', navigation: 'Question navigation', previous: '← Previous question', next: 'Next question →', view: '3D VIEW', viewport: '3D workbench', study: 'Test answers and prediction', canvas: '3D experiment: drag to orbit, scroll to zoom, right-drag to pan', webglStart: 'WebGL could not start. Enable browser hardware acceleration and reload to see the 3D scenes. Lessons, controls and measurements still work.', webglLost: 'WebGL context lost. Reload to restore the 3D view. Numeric experiments still work.', experimentKicker: (index, total) => `EXPERIMENT ${pad(index)} / ${pad(total)}`, questionKicker: (section, question, points) => `TEST ${section} · QUESTION ${question} · ${points} POINTS`, }, da: { subtitle: 'Udforsk det usynlige. Forstå fysikken.', title: 'Elektroteknik og elektrofysik', documentTitle: 'Test 4 i 3D · Elektrofysiklaboratoriet', metaDescription: 'Lær elektrisk arbejde, potentiel energi og dipolrotation gennem seks interaktive 3D-lektioner fra Test 4.', home: 'Feltlaboratoriets forside', collection: 'Læringssamling', language: 'Sprog', test: 'Test 4 i 3D', experiments: 'Alle eksperimenter', overview: 'FRA LIGNINGER TIL INTUITION', overviewTitle: 'Elektrisk energi gjort synlig.', overviewText: 'Udforsk de seks spørgsmål fra Test 4. Ændr scenen, forudsig resultatet, og følg derefter udledningen.', score: '6 spørgsmål · 3D-studievejledning', subjects: 'Elektrofysikkens emner', lessons: 'Lektioner', reset: 'Nulstil eksperiment', original: 'Oprindeligt eksamensspørgsmål', parameters: 'Eksperimentparametre', measurements: 'Målinger i realtid', camera: 'Nulstil kamera', pause: 'Sæt animation på pause', resume: 'Genoptag animation', hint: 'Træk for at dreje · Rul for at zoome · Højreklik og træk for at panorere · Piletaster panorerer, når feltet er valgt', notice: 'BEMÆRK, HVAD DER ÆNDRER SIG', connect: 'Forbind scenen med svaret', physics: 'FYSIKKEN', learn: 'Lær fysikken', try: 'PRØV DETTE', scales: 'Model- og visuelle skalaer', build: 'BYG DIT SVAR', reasoning: 'Følg ræsonnementet', originalAnswers: 'Oprindelige svar ↗', predict: 'PAUSE OG FORUDSIG', reveal: 'Vis forklaring', hide: 'Skjul forklaring', navigation: 'Spørgsmålsnavigation', previous: '← Forrige spørgsmål', next: 'Næste spørgsmål →', view: '3D-VISNING', viewport: '3D-arbejdsbord', study: 'Prøvesvar og forudsigelse', canvas: '3D-eksperiment: træk for at dreje, rul for at zoome, højreklik og træk for at panorere', webglStart: 'WebGL kunne ikke starte. Slå browserens hardwareacceleration til, og genindlæs for at se 3D-scenerne. Lektioner, betjening og målinger virker stadig.', webglLost: 'WebGL-konteksten gik tabt. Genindlæs for at gendanne 3D-visningen. De numeriske eksperimenter virker stadig.', experimentKicker: (index, total) => `EKSPERIMENT ${pad(index)} / ${pad(total)}`, questionKicker: (section, question, points) => `TEST ${section} · SPØRGSMÅL ${question} · ${points} POINT`, }, }; const scenePhrases = { en: { origin: 'Origin', onRightCharge: 'on right charge', gaussian: 'Gaussian surface', probe: 'Probe', isolated: 'isolated', connected: 'supply connected', noCurrent: 'No net current', conventional: 'Conventional current', drift: String.raw`Electron drift · displayed $\times600$`, conduction: 'Conduction band', valence: 'Valence band', midgap: 'midgap', gap: 'gap', energyConstant: 'Energy changes form · total stays constant', }, da: { origin: 'Origo', onRightCharge: 'på højre ladning', gaussian: 'Gaussisk flade', probe: 'Sonde', isolated: 'isoleret', connected: 'forsyning tilsluttet', noCurrent: 'Ingen nettostrøm', conventional: 'Konventionel strøm', drift: String.raw`Elektrondrift · vist $\times600$`, conduction: 'Ledningsbånd', valence: 'Valensbånd', midgap: 'båndmidte', gap: 'gab', energyConstant: 'Energien skifter form · summen er konstant', }, } as const; export type SceneKey = keyof typeof scenePhrases.en; const words: Record> = { en: {}, da: { Repulsive: 'Frastødende', Attractive: 'Tiltrækkende', 'On surface': 'På overfladen', Inside: 'Inden for', Outside: 'Uden for', Undefined: 'Udefineret', pairs: 'par', }, }; const measurementLabels: Record> = { en: {}, da: { '|E| at origin': '|E| i origo', '|Force on right charge|': '|Kraft på højre ladning|', Interaction: 'Vekselvirkning', 'Net electric flux': 'Netto elektrisk flux', 'Enclosed charge': 'Indesluttet ladning', 'Source location': 'Kildens placering', 'Interaction energy': 'Vekselvirkningsenergi', 'Work by field, from 6 m': 'Feltets arbejde fra 6 m', '|Force on moving charge|': '|Kraft på den bevægelige ladning|', 'Potential at probe': 'Potential ved sonden', 'Ex at probe': 'Ex ved sonden', '|E| at probe': '|E| ved sonden', Capacitance: 'Kapacitans', 'Stored free charge |Q|': 'Lagret fri ladning |Q|', 'Electric field': 'Elektrisk felt', 'Stored energy': 'Lagret energi', 'Plate voltage': 'Pladespænding', 'Free charge |Q|': 'Fri ladning |Q|', 'Net field': 'Nettofelt', 'Polarization |P|': 'Polarisering |P|', Resistance: 'Resistans', 'Conventional current': 'Konventionel strøm', 'Electron drift, x': 'Elektrondrift, x', 'Dissipated power': 'Afgivet effekt', 'Electron density n': 'Elektrontæthed n', 'Hole density p': 'Hulletæthed p', 'Conductivity σ': 'Ledningsevne σ', 'Fermi shift EF − Ei': 'Forskydning af Fermi EF − Ei', 'Field work a → b': 'Feltets arbejde a → b', 'Work to current position': 'Arbejde til nuværende position', 'Potential energy Uᵦ': 'Potentiel energi Uᵦ', 'Potential energy U': 'Potentiel energi U', 'Kinetic energy K': 'Kinetisk energi K', 'Total K + U': 'Samlet K + U', 'Particle speed': 'Partikelhastighed', 'Potential energy Uₐ': 'Potentiel energi Uₐ', 'Energy of current assembly': 'Energi af den aktuelle samling', 'Three-charge energy U₁₂₃': 'Tre ladningers energi U₁₂₃', 'Additional energy ΔU₀': 'Ekstra energi ΔU₀', 'Pairs currently assembled': 'Par samlet nu', 'Projection Δx': 'Projektion Δx', 'Current angle θ': 'Aktuel vinkel θ', 'Torque τz': 'Kraftmoment τz', 'Work to current angle': 'Arbejde til aktuel vinkel', }, }; function storedLanguage(): Language { try { const stored = globalThis.localStorage?.getItem('language'); if (stored === 'da' || stored === 'en') return stored; } catch { // Storage can be unavailable in private or non-browser contexts. } return 'en'; } let language: Language = storedLanguage(); const listeners = new Set<() => void>(); export function getLanguage(): Language { return language; } export function setLanguage(next: Language) { language = next; try { localStorage.setItem('language', next); } catch { // Ignore storage failures; the in-memory choice still applies. } if (typeof document !== 'undefined') document.documentElement.lang = next; for (const listener of listeners) listener(); } export function onLanguageChange(listener: () => void) { listeners.add(listener); return () => listeners.delete(listener); } export function t(key: K): Chrome[K] { return chrome[language][key]; } export function scene(key: SceneKey): string { return scenePhrases[language][key]; } export function word(english: string): string { return words[language][english] ?? english; } export function measurementLabel(english: string): string { return measurementLabels[language][english] ?? english; } export const daMeasurementLabels = measurementLabels.da; export const daWords = words.da;