aboutsummaryrefslogtreecommitdiff
path: root/src/i18n.ts
diff options
context:
space:
mode:
Diffstat (limited to 'src/i18n.ts')
-rw-r--r--src/i18n.ts308
1 files changed, 308 insertions, 0 deletions
diff --git a/src/i18n.ts b/src/i18n.ts
new file mode 100644
index 0000000..2fd3dca
--- /dev/null
+++ b/src/i18n.ts
@@ -0,0 +1,308 @@
+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<Language, Chrome> = {
+ 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<Language, Record<string, string>> = {
+ 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<Language, Record<string, string>> = {
+ 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<K extends keyof Chrome>(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;