diff options
Diffstat (limited to '')
| -rw-r--r-- | src/i18n.ts | 308 | ||||
| -rw-r--r-- | src/i18n/coverage.test.ts | 61 | ||||
| -rw-r--r-- | src/i18n/da.ts | 488 | ||||
| -rw-r--r-- | src/i18n/en.ts | 101 | ||||
| -rw-r--r-- | src/i18n/index.ts | 28 | ||||
| -rw-r--r-- | src/i18n/lesson-text.ts | 22 | ||||
| -rw-r--r-- | src/i18n/narrate.ts | 41 | ||||
| -rw-r--r-- | src/i18n/present.ts | 76 |
8 files changed, 228 insertions, 897 deletions
diff --git a/src/i18n.ts b/src/i18n.ts deleted file mode 100644 index 2fd3dca..0000000 --- a/src/i18n.ts +++ /dev/null @@ -1,308 +0,0 @@ -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; diff --git a/src/i18n/coverage.test.ts b/src/i18n/coverage.test.ts deleted file mode 100644 index f12178d..0000000 --- a/src/i18n/coverage.test.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { describe, expect, it, afterEach } from 'vitest'; -import { defaults, experiments, type Experiment } from '../experiments'; -import { daMeasurementLabels, daWords, setLanguage } from '../i18n'; -import { testLessons, type TestLesson } from '../test4'; -import { danishLessons } from './da'; -import { presentLesson } from './present'; - -const lessons: Experiment[] = [...testLessons, ...experiments]; - -afterEach(() => setLanguage('en')); - -describe('Danish catalog', () => { - it('translates every lesson, control and answer while keeping the mathematics', () => { - setLanguage('da'); - for (const lesson of lessons) { - const copy = danishLessons[lesson.id]; - const view = presentLesson(lesson); - expect(view.title).toBe(copy.title); - expect(view.title).not.toBe(lesson.title); - expect(view.shortTitle).toBe(copy.shortTitle); - expect(view.legend.map(([, text]) => text)).toEqual(copy.legend); - expect(view.controls.map(control => control.id)).toEqual(lesson.controls.map(control => control.id)); - for (const control of view.controls) { - const source = lesson.controls.find(item => item.id === control.id)!; - expect(control.label).toBe(copy.controls[control.id].label); - expect(control.label).not.toBe(source.label); - if (control.kind === 'select' && source.kind === 'select') { - expect(control.options.map(option => option.value)).toEqual(source.options.map(option => option.value)); - expect(control.options.map(option => option.label)).toEqual(source.options.map(option => copy.controls[control.id].options?.[option.value])); - } - } - if (!('questions' in lesson) || !('questions' in view)) continue; - const source = lesson as TestLesson; - const translated = view as TestLesson; - expect(translated.questions).toHaveLength(source.questions.length); - expect(translated.answers).toHaveLength(source.answers.length); - translated.answers.forEach((part, index) => { - expect(part.steps).toHaveLength(source.answers[index].steps.length); - part.steps.forEach((step, stepIndex) => { - expect(step.math).toBe(source.answers[index].steps[stepIndex].math); - expect(step.why).not.toBe(source.answers[index].steps[stepIndex].why); - }); - }); - expect(translated.observe(defaults(lesson))).not.toBe(source.observe(defaults(lesson))); - } - }); - - it('has a Danish label for every live measurement and status word', () => { - const labels = lessons.flatMap(lesson => lesson.measure(defaults(lesson)).map(measurement => measurement.label)); - for (const label of labels) expect(daMeasurementLabels[label], label).toBeTruthy(); - for (const status of ['Repulsive', 'Attractive', 'Inside', 'Outside', 'On surface', 'Undefined', 'pairs']) { - expect(daWords[status], status).toBeTruthy(); - } - setLanguage('da'); - const field = presentLesson(experiments[0]); - expect(field.measure({ ...defaults(field), configuration: 'like' }).map(measurement => measurement.value)).toContain('Frastødende'); - expect(presentLesson(lessons[0]).title).toBe('Arbejde langs en bane'); - setLanguage('en'); - expect(presentLesson(lessons[0]).title).toBe('Work along a path'); - }); -}); diff --git a/src/i18n/da.ts b/src/i18n/da.ts index dbdbc2e..2eea50e 100644 --- a/src/i18n/da.ts +++ b/src/i18n/da.ts @@ -1,391 +1,101 @@ -import type { ExperimentId } from '../experiments'; -import type { LessonText, TestCopy } from './lesson-text'; +import type { MessageKey } from './en'; -const openstax = 'OpenStax · Universitetsfysik'; -const original = 'Oprindeligt spørgsmål · Test_4.md'; -const chargeLegend = ['Positiv ladning', 'Negativ ladning', 'Elektrisk felt E', 'Bane / kraft / energi']; - -const lab: Record<Exclude<ExperimentId, `test-${string}`>, LessonText> = { - field: { - title: 'Elektrisk ladning, kraft og felt', - shortTitle: 'Ladning og felt', - description: 'Se hvordan ladninger med fortegn skaber et vektorfelt og udøver kræfter på hinanden.', - explanation: 'En positiv prøveladning følger det elektriske felt; en negativ ladning mærker en kraft i modsat retning. Den store guld-pil viser kraften på den højre kilde, og kun den, der skyldes den venstre kilde.', - challenge: 'Fordobl afstanden. Hvad sker der med kraften? Skift til ens ladninger, og se tiltrækning blive til frastødning.', - limits: String.raw`Punktladninger i vakuum. x–z-planen samples; punkter inden for $0.18\,\mathrm m$ fra en kilde udelades. Pilenes længder er komprimeret, og kraftpilen er normaliseret.`, - sceneNote: String.raw`Gitter: store kvadrater på $1\,\mathrm m$ · små pile: $E$ · guld-pil: kraft på højre ladning`, - legend: ['Positiv ladning', 'Negativ ladning', 'Elektrisk felt', 'Kraft'], - sourceTitle: openstax, - controls: { - charge: { label: 'Kildeladningens størrelse' }, - separation: { label: 'Ladningsafstand' }, - configuration: { label: 'Ladningstegn', options: { opposite: 'Modsat (+ / −)', like: 'Ens (+ / +)' } }, - }, - }, - flux: { - title: 'Elektrisk flux og ladning', - shortTitle: 'Flux og Gauss’ lov', - description: 'Flyt en ladning gennem en gaussisk flade, og sammenlign det lokale felt med nettofluxen.', - explanation: String.raw`Den gennemsigtige kugle er en tænkt lukket flade, ikke en leder. Pilene viser $E$ i fladens prøvepunkter: mint betyder udadgående flux, og koral betyder indadgående flux. En ladning udenfor skaber stadig et felt, men dens samlede flux gennem den lukkede flade er nul.`, - challenge: 'Hold ladningen indenfor, og gør kuglen større. Flyt derefter ladningen udenfor. Kan der være et felt forskelligt fra nul, når nettofluxen er nul?', - limits: 'Fluxvisningen bruger Gauss’ lov eksakt; pilene er diskrete feltprøver, ikke et numerisk integral. En punktladning præcis på fladen gør fluxen udefineret.', - sceneNote: String.raw`Tænkt gaussisk flade · mint: $\vec E\cdot\hat n>0$ · koral: $\vec E\cdot\hat n<0$`, - legend: ['Udadgående flux', 'Indadgående flux', 'Gaussisk flade'], - sourceTitle: openstax, - controls: { - charge: { label: 'Kildeladning med fortegn' }, - radius: { label: 'Gaussisk kugles radius' }, - position: { label: 'Ladningens x-position' }, - }, - }, - energy: { - title: 'Elektrisk potentiel energi', - shortTitle: 'Potentiel energi', - description: 'Adskil to ladninger, og forbind vekselvirkningsenergien med det arbejde, feltet udfører.', - explanation: 'Kilden bliver til venstre. Den mintfarvede kurve viser vekselvirkningsenergien som funktion af afstanden; guld-markøren følger den aktuelle indstilling. Modsatte ladninger har negativ energi i forhold til uendelig adskillelse, så det kræver ydre arbejde at trække dem fra hinanden.', - challenge: String.raw`Flyt en modsat ladning tættere på. Stiger eller falder $U$? Gør derefter den bevægelige ladning positiv, og sammenlign fortegnet for feltets arbejde.`, - limits: String.raw`To foreskrevne punktladninger med $U=0$ i det uendelige. Kurven bruger en fast visningsskala på 20 nJ pr. lodret sceneenhed. Betjeningen sætter positioner; den integrerer ikke bevægelse.`, - sceneNote: String.raw`Kurve: $U$ som funktion af afstanden · guld-punkt: aktuel $U$ · vandret akse for nulenergi`, - legend: ['Fast +3 nC', 'Negativ ladning', 'Energikurve', 'Aktuel energi'], - sourceTitle: openstax, - controls: { - testCharge: { label: 'Bevægelig ladning' }, - distance: { label: 'Afstand fra +3 nC-kilden' }, - }, - }, - potential: { - title: 'Elektrisk potential og gradient', - shortTitle: 'Potential og gradient', - description: 'Aflæs det elektriske potential som et landskab, og forbind nedadbakke-retningen med feltet.', - explanation: String.raw`Varme toppe er positivt potential; blå dale er negativt. Mint-pile viser det elektriske felt projiceret i x–z-planen. Guld-sonden måler $V$ og x-komponenten af $E$. Feltpilene peger mod aftagende potential.`, - challenge: String.raw`Brug modsatte ladninger: potentialet i midtpunktet kan være nul, mens feltet er forskelligt fra nul. Sammenlign $V$ og $E_x$, mens du flytter sonden.`, - limits: String.raw`Højde $=2\tanh\left(\frac{V}{15\,\mathrm V}\right)$, en komprimeret potentialskala. Huller udelader kildernes singulariteter. Pilenes længder er komprimeret. Sonden er altid i $z=1\,\mathrm m$, væk fra kilderne; $V=0$ i det uendelige.`, - sceneNote: String.raw`Højde og farve koder potentialet · guld-sonde i $z=1\,\mathrm m$ · pile: $-\nabla V$`, - legend: ['Positivt potential', 'Negativt potential', 'Sonde'], - sourceTitle: openstax, - controls: { - charge: { label: 'Kildeladningens størrelse' }, - separation: { label: 'Ladningsafstand' }, - configuration: { label: 'Ladningstegn', options: { opposite: 'Modsat (+ / −)', like: 'Ens (+ / +)' } }, - probe: { label: 'Sondens x-position (z = 1 m)' }, - }, - }, - capacitance: { - title: 'Kapacitans', - shortTitle: 'Kapacitans', - description: 'Byg en pladekondensator, og se hvordan geometrien styrer ladningslagringen.', - explanation: 'Den venstre plade er positiv, og den højre er negativ. Ved en fast påtrykt spænding lagrer større plader mere ladning, mens et større gab mindsker kapacitansen. Pilene viser tilnærmelsen med et homogent felt mellem pladerne.', - challenge: 'Fordobl arealet, og fordobl derefter gabet. Hvilke målinger vender tilbage til deres oprindelige værdier, og hvilke gør ikke?', - limits: 'Ideelle parallelle plader i vakuum, forbundet til en fast spændingskilde. Randeffekter ignoreres. Pladeafstanden er forstørret 50× i forhold til de laterale mål, så den kan ses; ladningsmarkørerne er illustrative. Pilene spænder over gabet for at vise retning, ikke størrelse.', - sceneNote: 'Positiv plade → felt → negativ plade · gab forstørret 50×', - legend: ['Positiv plade', 'Negativ plade', 'Homogent E'], - sourceTitle: openstax, - controls: { - area: { label: 'Pladeareal' }, - gap: { label: 'Pladeafstand' }, - voltage: { label: 'Opladningsspænding' }, - }, - }, - dielectric: { - title: 'Dielektriske materialer', - shortTitle: 'Dielektrika', - description: 'Polariser en isolator, og sammenlign indsættelse med tilsluttet eller afbrudt forsyning.', - explanation: String.raw`Bundne dipoler retter sig ind efter feltet: deres negative ender vender mod den positive plade. De bundne ladninger modvirker feltet fra pladernes frie ladninger. Med et batteri tilsluttet strømmer ekstra fri ladning til, og $V$ forbliver fast. Når kondensatoren er isoleret, forbliver $Q$ fast, og $V$ falder.`, - challenge: String.raw`Skift til isoleret, og sammenlign $\kappa$ = 1 med $\kappa$ = 4. Tilslut derefter forsyningen igen, og gentag: hvilke størrelser holdes konstante?`, - limits: String.raw`Et homogent, lineært dielektrikum udfylder hele gabet. Isoleret $Q$ er defineret ved at oplade denne geometri i vakuum ved den valgte opladningsspænding, derefter afbryde og indsætte materialet. Hver indstilling er en ny forberedelse; gennemslag og randeffekter er udeladt. Gab forstørret 50×; feltpile viser retning, ikke størrelse.`, - sceneNote: String.raw`Guld-volumen: dielektrikum · blå/røde par: bundne dipoler · $\kappa$ = 1: ingen polarisering`, - legend: ['Dielektrikum', 'Bundet −', 'Bundet +', 'Nettofelt'], - sourceTitle: openstax, - controls: { - permittivity: { label: 'Relativ permittivitet κ' }, - connection: { label: 'Elektrisk tilstand', options: { connected: 'Tilsluttet · spænding holdes fast', isolated: 'Isoleret · fri ladning holdes fast' } }, - area: { label: 'Pladeareal' }, - gap: { label: 'Pladeafstand' }, - voltage: { label: 'Opladningsspænding' }, - }, - equations: [ - String.raw`C = \frac{\kappa\varepsilon_0 A}{d},\qquad \vec P = \varepsilon_0(\kappa-1)\vec E`, - String.raw`\begin{aligned}\text{Fast }V:&\quad Q\propto\kappa\\\text{Fast }Q:&\quad V,E\propto\frac1\kappa\end{aligned}`, - ], - }, - current: { - title: 'Strøm', - shortTitle: 'Strøm og drift', - description: 'Driv elektroner gennem en kobbertråd, og sammenlign deres drift med den konventionelle strøm.', - explanation: 'Mint viser den konventionelle strøm, defineret som strøm af positiv ladning. Blå elektroner driver i modsat retning. Højere spænding øger driften; en længere tråd øger resistansen. En bredere tråd fører mere strøm ved samme spænding.', - challenge: 'Vend spændingen. Sæt den derefter til nul. Øg trådens areal ved fast spænding: stiger strømmen, og ændrer elektronernes driftshastighed sig?', - limits: String.raw`Ohmisk kobber ved fast temperatur: $\rho=1.68\times10^{-8}\,\Omega\,\mathrm m$, $n=8.5\times10^{28}\,\mathrm m^{-3}$. Driften vises 600× hurtigere, og trådens radius er overdrevet. Termisk spredning, opvarmning og signaludbredelse simuleres ikke; $P$ er afgiven effekt.`, - sceneNote: 'Blå: elektrondrift · mint: konventionel strøm · hastighed ×600 · radius overdrevet', - legend: ['Elektroner', 'Konventionel strøm', 'Kobberioner'], - sourceTitle: openstax, - controls: { - voltage: { label: 'Spænding over tråden' }, - length: { label: 'Trådlængde' }, - area: { label: 'Tværsnitsareal' }, - }, - }, - semiconductor: { - title: 'Halvledermaterialer', - shortTitle: 'Halvledere', - description: String.raw`Undersøg intrinsic, $n$-type og $p$-type silicium gennem ladningsbærere og et energibånddiagram.`, - explanation: String.raw`Elektroner optager ledningsbåndet; huller er manglende elektroner i valensbåndet. Donorer hæver elektrontætheden og Ferminiveauet; acceptorer hæver hulletætheden og sænker det. Guld-linjen markerer $E_F$. Bærermarkørerne viser populationer på en komprimeret skala, ikke enkelte atomer.`, - challenge: String.raw`Skift fra intrinsic til $n$-type og derefter til $p$-type ved samme dotering. Følg majoritetsbæreren og Ferminiveauets forskydning. Hvorfor er ledningsevnerne forskellige?`, - limits: String.raw`Illustrative referenceværdier for silicium ved $300\,\mathrm K$: $E_g=1.12\,\mathrm{eV}$, $n_i\approx10^{16}\,\mathrm m^{-3}$, $\mu_n=0.135\,\mathrm{m^2/(V\,s)}$ og $\mu_p=0.048\,\mathrm{m^2/(V\,s)}$. Fuldt ioniseret, ikke-degenereret ligevægt med faste mobiliteter. $E_i$ er tegnet omtrent ved båndmidten. Bevægelsen er skematisk termisk bevægelse uden påtrykt felt eller nettodrift; tæthedsbetjeningen er inaktiv for intrinsic silicium.`, - sceneNote: String.raw`Øvre blå bånd: ledning · nedre koralbånd: valens · guld: $E_F$ · ingen nettostrøm`, - legend: ['Ledningselektroner', 'Valenshuller', 'Ferminiveau'], - sourceTitle: 'OpenStax · Båndteori for faste stoffer', - controls: { - doping: { label: 'Siliciumprøve ved 300 K', options: { intrinsic: 'Intrinsic · udopet', n: 'n-type · donorurenheder', p: 'p-type · acceptorurenheder' } }, - concentration: { label: 'Doteringstæthed · log₁₀(N / m⁻³)' }, - }, - }, -}; - -const test: Record<Extract<ExperimentId, `test-${string}`>, TestCopy> = { - 'test-work': { - title: 'Arbejde langs en bane', - shortTitle: 'Arbejde og baner', - description: 'Følg en ladning fra a til b. Kun den del af kraften, der ligger langs hvert lille skridt, bidrager til arbejdet.', - questions: [String.raw`Hvilket arbejde udfører den elektriske kraft, når $q_0$ flyttes fra a til b?`], - controls: { - q0: { label: 'Bevægelig ladning q₀' }, - field: { label: 'Elektrisk felt E' }, - distance: { label: 'Forskydning s' }, - path: { label: 'Baneform', options: { straight: 'Ret bane', curved: 'Krum bane · samme endepunkter' } }, - progress: { label: 'Rejseforløb' }, - }, - explanation: 'Skalarproduktet måler, hvor meget den elektriske kraft hjælper eller modvirker hvert skridt. Elektrostatisk arbejde afhænger af endepunkterne. Positivt feltarbejde betyder et fald i elektrisk potentiel energi.', - challenge: 'Skift mellem de to baner. Forudsig det endelige arbejde, før du ændrer den bevægelige ladning fra positiv til negativ.', - prediction: 'Kræver den længere, krumme rute mere arbejde af det elektriske felt?', - predictionAnswer: String.raw`Nej. I et elektrostatisk felt er $W=U_a-U_b$. Begge ruter starter og slutter de samme steder. Skridt på tværs er vinkelrette på $E$ og bidrager ikke til arbejdet. Ekstra bevægelse langs $E$ ophæver bevægelsen tilbage mod $E$.`, - sceneNote: String.raw`Eksempel: homogent $E$ langs +x. Grønne pile er $E$; guld-pilen er den elektriske kraft. Begge baner deler a og b.`, - limits: String.raw`Elektrostatisk vakuummodel; foreskreven bane, ikke en fri partikelbane. Geometrien er i meter; felt- og kraftpile viser retning med komprimerede længder. Guld-sonden styres af rejseforløbet; stiplede endepunkter bliver stående. Vælg $U_a=0$.`, - legend: chargeLegend, - sourceTitle: original, - answers: [{ - title: 'Spørgsmål 1 · Arbejde udført af den elektriske kraft · 30p', - steps: [ - 'Start med definitionen af arbejde: læg den del af kraften sammen, der følger hver lille forskydning.', - String.raw`Indsæt den elektriske kraft på den bevægelige ladning, $\vec F_e=q_0\vec E$.`, - 'Den elektrostatiske kraft er konservativ, så feltets arbejde er faldet i potentiel energi.', - ], - }], - }, - 'test-conservation': { - title: 'Hvor bliver energien af?', - shortTitle: 'Bevarelse', - description: 'En positiv ladning bevæger sig væk fra en fast positiv kilde. Se potentiel energi blive til kinetisk energi.', - questions: ['Skriv betingelsen for bevarelse af den samlede energi.'], - controls: { - q0: { label: 'Bevægelig ladning q₀' }, - kinetic: { label: 'Begyndelseskinetisk energi Kₐ' }, - progress: { label: 'Rejseforløb' }, - }, - equations: [String.raw`\Delta E_{\mathrm{total}} = 0\quad\text{(isoleret system)}`, String.raw`K_a+U_a = K_b+U_b`, String.raw`\Delta K = W_{ab} = -\Delta U`], - explanation: 'Den samlede energi er bevaret for et isoleret system. Hvis kun den elektriske kraft udfører arbejde på den bevægelige ladning, er summen af kinetisk energi og elektrisk potentiel energi konstant. Kilden er fast; dens ophæng udfører intet arbejde.', - challenge: String.raw`Flyt rejseforløbet fra 0 % til 100 %. Kontrollér, at den samlede søjle holder samme højde, mens $K$ stiger og $U$ falder.`, - prediction: 'Hvis partiklen starter i hvile, kan den så få fart uden en energitilførsel?', - predictionAnswer: String.raw`Ja. Dens begyndelsesværdi af elektrisk potentiel energi leverer stigningen i kinetisk energi. Energien skifter form, mens $K+U$ forbliver konstant. Partiklen og kilden udgør tilsammen det elektrostatiske system; den bevægelige partikel alene har ikke konstant kinetisk energi.`, - sceneNote: String.raw`Fri radial bevægelse væk fra +3 nC. Søjlerne viser $U$, $K$ og deres konstante sum; massen er 1 mg.`, - limits: String.raw`Kilden er fast, vakuum, ikke-relativistisk punktpartikel med massen 1 mg. $U(\infty)=0$. Rejseforløbet vælger en position, ikke forløbet tid; hastigheden beregnes ud fra energien, ikke ud fra skyderens hastighed. Søjlehøjder er normaliseret til den samlede energi; aflæsningerne er i nJ.`, - legend: chargeLegend, - sourceTitle: original, - answers: [{ - title: 'Spørgsmål 2 · Bevarelsesbetingelse · 15p', - steps: [ - 'For et isoleret system krydser der ingen nettoenergi systemets grænse.', - 'Hvis den elektriske kraft er den eneste kraft, der udfører arbejde, er den mekaniske energi bevaret.', - String.raw`Indsæt den kinetiske energi for en partikel med massen $m$. Arbejde-energi-sætningen forbinder de to ændringer.`, - ], - }], - }, - 'test-central': { - title: 'En punktladnings felt', - shortTitle: 'Centralt felt', - description: String.raw`Flyt $q_0$ radialt gennem et centralt felt. Forbind den afstandskvadrerede kraft med en potentiel energi, der aftager som én over afstanden.`, - questions: [ - String.raw`Når $q_0$ bevæger sig fra a til b i et $E$-felt fra en punktladning $q$ (“centralt felt”).`, - 'a. (30p) Udled det arbejde, den elektriske kraft udfører.', - String.raw`b. (25p) Arbejdet ovenfor er lig med faldet $U_a$ − $U_b$. Hvad er $U_a$ og $U_b$, hvis $b\to\infty$?`, - ], - controls: { - source: { label: 'Kildeladning q' }, - q0: { label: 'Bevægelig ladning q₀' }, - ra: { label: 'Begyndelsesafstand rₐ' }, - rb: { label: 'Slutafstand rᵦ' }, - destination: { label: 'Endepunkt b', options: { finite: 'Endelig slutafstand', infinity: 'Uendelighed · slutenergien er nul' } }, - progress: { label: 'Rejseforløb' }, - }, - explanation: String.raw`Kun radial forskydning bidrager til arbejdet i et centralt felt. Integration af $\frac1{r^2}$ giver $-\frac1r$. Nulpunktet i det uendelige fastlægger energireferencen; det er ikke en ekstra kraftlov.`, - challenge: String.raw`Sammenlign ens og modsatte ladninger. Send b til uendelig, og kontrollér at $W_{a\infty}=U_a$, inklusive fortegnet.`, - prediction: String.raw`Er $U$ altid positiv, når vi vælger $U(\infty)=0$?`, - predictionAnswer: String.raw`Nej. $U=\frac{kqq_0}{r}$ er positiv for ens ladninger og negativ for modsatte ladninger. En negativ værdi betyder, at konfigurationen har mindre energi end de adskilte ladninger i det uendelige. Feltets arbejde ved at adskille modsatte ladninger er negativt.`, - sceneNote: String.raw`Radial bane foroven; $U(r)$-kurve forneden. Uendelighedstilstanden viser en fortsættelse udad, ikke en endelig stedfortræder for uendelighed.`, - limits: String.raw`Fast punktkilde; Coulombs konstant i vakuum $k=\frac1{4\pi\varepsilon_0}$. Afstande i meter. $U$-graf: 30 nJ pr. lodret sceneenhed, klippet uden for ±2,5 enheder. Uendelighed har analytisk præcis $U_b=0$; den synlige bane stopper ved diagrammets kant og placerer ikke b i en endelig radius.`, - legend: chargeLegend, - sourceTitle: original, - answers: [ - { - title: '3a · Udled det radiale arbejde · 30p', - steps: [ - 'Skriv det radiale felt og kraften fra den faste punktladning.', - String.raw`Den radiale komponent af forskydningen er $\hat r\cdot\mathrm d\vec r=\mathrm dr$. Tangentiale skridt udfører intet arbejde.`, - 'Integrer, og indsæt endepunkterne. Resultatet afhænger kun af deres afstande.', - ], - }, - { - title: '3b · Sæt nulpunktet i det uendelige · 25p', - steps: [ - String.raw`Vælg $U(\infty)=0$. Når $r_b\to\infty$, går den reciprokke afstand mod nul.`, - String.raw`Brug $W_{a\infty}=U_a-U_\infty$ til at identificere de to endepunktsenergier.`, - String.raw`Det giver den generelle vekselvirkningsenergi. Dens fortegn er fortegnet af $qq_0$.`, - ], - }, - ], - }, - 'test-assembly': { - title: 'Opbyg et ladningssystem', - shortTitle: 'Ladningssamling', - description: 'Bring ladningerne ind én ad gangen. Hver forbindende kant er én vekselvirkningsenergi, talt præcis én gang.', - questions: [ - 'Ladninger bringes sammen.', - String.raw`a. (15p) Hvad er den samlede elektriske potentielle energi, hvis $q_1$, $q_2$ og $q_3$ bringes sammen fra det uendelige?`, - String.raw`b. (10p) Hvad er den ekstra elektriske potentielle energi, hvis $q_0$ bringes sammen med $q_1$, $q_2$ og $q_3$?`, - ], - controls: { - stage: { - label: 'Samlingstrin', - options: { '1': '1 · Placer første ladning', '2': '2 · Tilføj anden ladning', '3': '3 · Tilføj tredje ladning', '4': '4 · Tilføj prøveladning' }, - }, - spacing: { label: 'Konfigurationens størrelse' }, - q0: { label: 'Tilføjet ladning q₀' }, - }, - explanation: String.raw`Placer $q_1$ gratis. Tilføjelse af $q_2$ skaber ét par; tilføjelse af $q_3$ skaber to mere. Den fjerde ladning skaber tre nye par uden at ændre energien af de eksisterende tre. Ved kvasistatisk samling er det ydre arbejde lig med stigningen i $U$.`, - challenge: String.raw`Gå igennem alle fire samlingstrin. Vend derefter $q_0$, og se fortegnet af dens tre nye vekselvirkninger vende.`, - prediction: String.raw`Skal den samlede energi indeholde både $\frac{q_1q_2}{r_{12}}$ og $\frac{q_2q_1}{r_{21}}$?`, - predictionAnswer: String.raw`Nej. De beskriver det samme par. Sum kun over $i<j$. Tre ladninger har tre forskellige par; fire ladninger har seks. Den ekstra energi fra $q_0$ omfatter kun dens tre nye par, $\Delta U_0=q_0V_{123}$.`, - sceneNote: 'Et tetraederarrangement. Mint-kanter: eksisterende par. Guld-kanter: par indført af den nyeste ladning. Stiplede markører: endnu ikke samlet.', - limits: String.raw`Punktladninger fastholdt i forskellige positioner. $q_1$ = +3 nC, $q_2$ = −2 nC, $q_3$ = +2 nC. Alle koordinater skaleres med konfigurationens størrelse i meter. Reference: alle ladninger er gensidigt adskilt i det uendelige. Selvenergier er udeladt. Totaler for senere trin vises i aflæsningerne, også før de ladninger er placeret.`, - legend: chargeLegend, - sourceTitle: original, - answers: [ - { - title: '4a · Saml q₁, q₂, q₃ · 15p', - steps: [ - String.raw`Bring først $q_1$ ind fra det uendelige. Når ingen andre ladninger er til stede, er dens vekselvirkningsenergi nul.`, - String.raw`Bring $q_2$ ind mod potentialet fra $q_1$; bring derefter $q_3$ ind mod potentialet fra $q_1$ og $q_2$.`, - 'Læg bidragene sammen, og tæl hvert par én gang.', - ], - }, - { - title: '4b · Tilføj q₀ til det faste system · 10p', - steps: [ - String.raw`De eksisterende tre ladninger holdes fast. Tilføj kun de tre nye vekselvirkninger, der involverer $q_0$.`, - String.raw`Sæt $q_0$ uden for parentesen. Den resterende sum er potentialet fra de oprindelige tre ladninger i den nye position.`, - ], - }, - ], - }, - 'test-uniform': { - title: 'Arbejde i et homogent felt', - shortTitle: 'Homogent felt', - description: String.raw`Drej forskydningen i forhold til feltet. Projektionen langs $E$ bestemmer arbejdet, ikke hele banens længde.`, - questions: [ - String.raw`Når $q_0$ bevæger sig fra a til b i et homogent $E$-felt.`, - 'a. (15p) Udled det arbejde, den elektriske kraft udfører.', - String.raw`b. (10p) Hvad er $U_{\mathrm{pot},a}$ og $U_{\mathrm{pot},b}$?`, - ], - controls: { - q0: { label: 'Bevægelig ladning q₀' }, - field: { label: 'Elektrisk felt E' }, - distance: { label: 'Forskydning s' }, - angle: { label: 'Forskydningsvinkel α' }, - progress: { label: 'Rejseforløb' }, - }, - explanation: String.raw`Et konstant felt kan tages uden for arbejdsintegralet. Med x langs $E$ ændrer den potentielle energi sig kun med x. Energinulpunktet er vilkårligt; her er a referencen, så $U_a=0$.`, - challenge: String.raw`Sæt $\alpha$ til $90^\circ$. Ladningen tilbagelægger en afstand forskellig fra nul, men $E$ er vinkelret på hvert skridt: arbejdet er nul.`, - prediction: String.raw`Hvis $\alpha$ er over $90^\circ$ for en positiv ladning, afgiver feltet så energi til ladningen?`, - predictionAnswer: String.raw`Feltet udfører negativt arbejde, fordi $\cos\alpha<0$. $U$ stiger, når ladningen bevæger sig mod feltet. En ydre aktør måtte tilføre den energi for at flytte den langsomt, eller en frit bevægelig partikel ville miste kinetisk energi.`, - sceneNote: String.raw`Forskydningen ligger i xy-planen; $E$ peger langs +x. Det blå stykke er $\Delta x=s\cos\alpha$. Vælg $U_a=0$.`, - limits: String.raw`Ideelt konstant felt uden randeffekter. Geometri i meter; pile viser retning med komprimerede længder. Foreskreven bevægelse, ikke en fri partikelbane. $U_a=0$ fastlægger $C=q_0Ex_a$; $x_a$ vises eksplicit af markøren a.`, - legend: chargeLegend, - sourceTitle: original, - answers: [ - { - title: '5a · Udled arbejdet i et homogent felt · 15p', - steps: [ - String.raw`Start med arbejdsintegralet. Feltet er konstant, så tag $\vec E$ uden for integralet.`, - String.raw`Skriv skalarproduktet med forskydningens længde og vinklen $\alpha$.`, - 'Vælg x-aksen langs feltet. Kun x-forskydningen bidrager.', - ], - }, - { - title: '5b · Potentiel energi i endepunkterne · 10p', - steps: [ - 'Ændringen i potentiel energi er minus feltets arbejde.', - String.raw`Integration med hensyn til x efterlader en vilkårlig referencekonstant $C$.`, - String.raw`Et bekvemt valg er $U_a=0$. Det ændrer ingen kræfter eller energiforskelle.`, - ], - }, - ], - }, - 'test-dipole': { - title: 'Drej en dipol i et felt', - shortTitle: 'Dipolrotation', - description: 'To lige store og modsat rettede kræfter ophæver hinanden ved translation, men danner et kraftmoment. Drej dipolen for at undersøge arbejde og stabilitet.', - questions: [ - String.raw`Den elektriske dipol i et homogent $E$-felt.`, - String.raw`a. (10p) Udled kraftmomentvektoren, og bevis $\tau=p_eE\sin\theta$, hvor $\theta=\angle(\vec p_e,\vec E)$.`, - String.raw`b. (30p) Udled $W_{ab}$, når dipolen drejer fra $\theta_a$ til $\theta_b$. Hvad er $U_a$ og $U_b$? Du behøver ikke udlede $\mathrm dW=-\tau\,\mathrm d\theta$.`, - String.raw`c. (10p) Forklar, hvorfor $\theta_a$ = $90^\circ$ kan være en reference, angiv $U_a$, og find det generelle $U$ også som et skalarprodukt.`, - ], - controls: { - charge: { label: 'Dipolladningens størrelse q' }, - length: { label: 'Ladningsafstand d' }, - field: { label: 'Elektrisk felt E' }, - thetaA: { label: 'Begyndelsesvinkel θₐ' }, - thetaB: { label: 'Slutvinkel θᵦ' }, - progress: { label: 'Rotationsforløb' }, - }, - explanation: String.raw`Dipolmomentet peger fra $-q$ mod $+q$. De lige store og modsat rettede elektriske kræfter danner et kraftpar, der søger at rette $\vec p_e$ ind efter $\vec E$. Vi bruger $U(90^\circ)=0$; indretning er tilstanden med mindst energi.`, - challenge: String.raw`Sæt slutvinklen til $0^\circ$ og derefter til $180^\circ$, med rotationsforløbet på 100 %. Begge har kraftmoment nul. Sammenlign deres energi og stabilitet.`, - prediction: String.raw`Hvis kraftmomentet er nul ved både $0^\circ$ og $180^\circ$, er begge orienteringer så stabile?`, - predictionAnswer: String.raw`Nej. Ved $0^\circ$ er $U=-p_eE$ et minimum: en lille vinkeldrejning giver et tilbageførende kraftmoment. Ved $180^\circ$ er $U=+p_eE$ et maksimum: en lille drejning fører dipolen længere væk. Ved $90^\circ$ er $U=0$ per konvention, men kraftmomentet er størst, så det er ikke ligevægt. Uden dæmpning kan en frit roterende dipol svinge om indretningen.`, - sceneNote: String.raw`$E$ langs $+x$; $\vec p_e$ peger fra − mod +. Guld-pile viser kræfter; den blå z-pil viser kraftmomentet med fortegn. $U(\theta)$ forneden.`, - limits: String.raw`Ideel stiv dipol med fast centrum i et homogent felt. Geometri i meter. Ingen nettokraft. Vinklen måles mod uret i xy, så $\tau_z=-p_eE\sin\theta$. Skyderen foreskriver orienteringen, ikke en dæmpet dynamisk bane. Kraftmomentets pillængde og grafhøjden er normaliseret; aflæsningerne giver størrelser i SI-afledte enheder. $U(90^\circ)=0$.`, - legend: chargeLegend, - sourceTitle: original, - answers: [ - { - title: '6a · Kraftmomentets vektor og størrelse · 10p', - steps: [ - String.raw`Lad $\vec d$ pege fra den negative ladning mod den positive. Definér dipolmomentet.`, - String.raw`Tag momenter om midtpunktet. De to momentarme er $\pm\frac{\vec d}{2}$.`, - 'Krydsproduktet giver vektorens retning og størrelse. De to kræfter danner et kraftpar.', - ], - }, - { - title: '6b · Rotationsarbejde og energi i endepunkterne · 30p', - steps: [ - String.raw`Kraftmomentet virker mod aftagende $\theta$. Med $\tau$ som den positive størrelse bruges den givne relation.`, - 'Integrer fra begyndelsesvinklen til slutvinklen.', - String.raw`Brug $W_{ab}=U_a-U_b$. Referencekonstanten er den samme i begge endepunkter.`, - ], - }, - { - title: '6c · Reference ved 90° og skalarprodukt · 10p', - steps: [ - String.raw`Nulpunktet for potentiel energi er vilkårligt. Ved $90^\circ$ er de to ladninger på samme elektriske potential, så $+qV$ og $-qV$ ophæver hinanden.`, - String.raw`Da $\cos90^\circ=0$, sætter denne reference $C=0$.`, - 'Indrettet er et stabilt energiminimum; modsat er et ustabilt maksimum. Den vinkelrette reference er ikke en ligevægt.', - ], - }, - ], - }, +export const da: Record<MessageKey, string> = { + 'app.title': 'Fieldwork · Elektrisk fluks og ladning', + 'app.brand': 'fieldwork', + 'app.tagline': 'Et laboratorium for elektrofysik', + 'app.skip': 'Gå til eksperimentet', + 'app.navigation': 'Hovednavigation', + 'app.experiment': 'Eksperiment', + 'app.principle': 'Fysikken', + 'app.module': 'MODEL {number}', + 'app.footer': 'Lidt nysgerrighed. En klarere forståelse.', + 'app.footerNote': 'Elektrofysik / Interaktiv læring', + 'app.loadError': 'Modellen kunne ikke indlæses. Genindlæs siden for at prøve igen.', + 'app.reload': 'Genindlæs siden', + 'flux.eyebrow': 'ELEKTRICITET GJORT SYNLIG', + 'flux.title': 'Elektrisk fluks og ladning', + 'flux.subtitle': 'Flyt en ladning. Ændr dens omgivelser. Find ud af, hvad der virkelig bestemmer den elektriske fluks.', + 'flux.topic': 'Gauss’ lov', + 'flux.live': 'INTERAKTIV 3D-MODEL', + 'flux.sceneTitle': 'Én ladning. Én lukket flade.', + 'flux.sceneLabel': '3D-model af elektrisk fluks. Træk for at dreje rundt og scroll for at zoome. Når modellen er i fokus, brug piletasterne til at dreje, plus eller minus til at zoome og Home til at nulstille visningen.', + 'flux.sceneHint': 'Træk for at dreje · Scroll for at zoome', + 'flux.keyboardHint': 'Tastatur: piletaster for at dreje · + / − for at zoome · Home for at nulstille', + 'flux.resetView': 'Nulstil visning', + 'flux.frontView': 'Set forfra', + 'flux.topView': 'Set ovenfra', + 'flux.cameraViews': 'Kameravisninger', + 'flux.legend.positive': 'Positivt felt', + 'flux.legend.negative': 'Negativt felt', + 'flux.legend.zero': 'Intet elektrisk felt', + 'flux.legend.surface': 'Gaussisk flade', + 'flux.legend.normal': 'Udadrettet normal', + 'flux.marker': 'q = {value}', + 'flux.status.inside': 'Ladning indeni', + 'flux.status.outside': 'Ladning udenfor', + 'flux.status.boundary': 'Ladning på fladen', + 'flux.status.neutral': 'Ingen ladning', + 'flux.controls': 'Gør det til dit eget eksperiment', + 'flux.controlsHint': 'Ændr én ting. Se, hvad der sker.', + 'flux.charge': 'Ladning', + 'flux.position': 'Ladningens position', + 'flux.positionHint': 'Flyt den langs x-aksen gennem fladen.', + 'flux.surface': 'Gaussisk flade', + 'flux.sphere': 'Kugle', + 'flux.cube': 'Terning', + 'flux.radius': 'Fladens radius', + 'flux.halfSide': 'Halv sidelængde', + 'flux.surfaceHint': 'En tænkt grænse, ikke et fysisk skal.', + 'flux.layers': 'Synlige lag', + 'flux.fieldLines': 'Elektriske feltlinjer', + 'flux.surfaceLayer': 'Gaussisk flade', + 'flux.normals': 'Fladens normaler', + 'flux.reset': 'Nulstil eksperiment', + 'flux.presets': 'Start med et scenarie', + 'flux.preset.positive': 'Positiv ladning', + 'flux.preset.negative': 'Negativ ladning', + 'flux.preset.outside': 'Udenfor fladen', + 'flux.preset.custom': 'Eget eksperiment', + 'flux.readings': 'Målinger i realtid', + 'flux.enclosed': 'Omsluttet ladning', + 'flux.area': 'Fladens areal', + 'flux.netFlux': 'Samlet elektrisk fluks', + 'flux.exact': 'Beregnet ud fra Gauss’ lov', + 'flux.undefined': 'Udefineret', + 'flux.result.insidePositive': 'Mere går ud, end der kommer ind.', + 'flux.result.insideNegative': 'Mere kommer ind, end der går ud.', + 'flux.result.outside': 'Det, der kommer ind, går også ud igen.', + 'flux.result.neutral': 'Ingen ladning. Intet felt. Ingen fluks.', + 'flux.result.boundary': 'Ladningen ligger på selve fladen.', + 'flux.explanation.insidePositive': 'Fladen omslutter en positiv ladning, så den samlede fluks går udad. Prøv at ændre fladens størrelse: Den samlede fluks forbliver den samme, så længe ladningen er indeni.', + 'flux.explanation.insideNegative': 'Fladen omslutter en negativ ladning, så den samlede fluks går indad. Det negative fortegn fortæller dig retningen i forhold til fladens udadrettede normal.', + 'flux.explanation.outside': 'Det elektriske felt er der stadig, men den omsluttede ladning er nul. Hver feltlinje, der går ind i den lukkede flade, går også ud af den igen, så den samlede fluks bliver nul.', + 'flux.explanation.neutral': 'Når kildeladningen er sat til nul, er der intet elektrisk felt nogen steder i modellen. Fluksen gennem den lukkede flade er derfor også nul.', + 'flux.explanation.boundary': 'En ideel punktladning på fladen gør feltet singulært netop dér. Flyt ladningen en smule ind eller ud for at få en veldefineret omsluttet ladning og fluks.', + 'flux.summary': '{status}. Omsluttet ladning: {charge}. Samlet elektrisk fluks: {flux}.', + 'flux.webglError': 'Den 3D-visningen kræver WebGL 2. Prøv en browser med hardwareacceleration slået til. Kontrollerne, beregningerne og forklaringerne virker stadig.', + 'flux.webglLost': 'Forbindelsen til 3D-grafikken blev afbrudt. Genindlæs siden for at gendanne den. Beregningerne virker stadig.', + 'flux.physicsEyebrow': 'FRA MODEL TIL IDÉ', + 'flux.physicsTitle': 'En flade fortæller en historie.', + 'flux.physicsSubtitle': 'Fluks måler, hvor meget elektrisk felt der går igennem en flade. Sådan læser du den.', + 'flux.lesson.one.number': '01', + 'flux.lesson.one.title': 'Retningen betyder noget', + 'flux.lesson.one.body': 'Et lille fladestykke har en arealvektor vinkelret på sig. Et felt, der peger samme vej som normalen, giver positiv fluks; et felt, der peger imod, giver negativ fluks. Et felt langs fladen bidrager med nul.', + 'flux.lesson.two.number': '02', + 'flux.lesson.two.title': 'Læg hele fladen sammen', + 'flux.lesson.two.body': 'For en lukket flade peger alle arealvektorer udad. Læg de fortegnsbestemte bidrag fra alle fladestykkerne sammen for at finde den samlede fluks. En samlet sum på nul betyder ikke, at feltet er nul.', + 'flux.lesson.three.number': '03', + 'flux.lesson.three.title': 'Kun det indeni tæller', + 'flux.lesson.three.body': 'Gauss’ lov forbinder den samlede fluks med den omsluttede ladning. Størrelse og form kan ændre feltet henover fladen, men de kan ikke ændre den samlede fluks, så længe den samme ladning er omsluttet.', + 'flux.try': 'PRØV SELV', + 'flux.tryText': 'Gør fladen større. Ændrer den samlede fluks sig?', + 'flux.assumptions.title': 'Om denne model', + 'flux.assumptions.body': 'Én stationær, ideel punktladning i vakuum; ingen andre ladninger eller materialer. Fladen er tænkt og har centrum i origo. Én enhed i scenen svarer til én meter. Ladningsmarkøren har kun en synlig størrelse, så du kan finde den; om ladningen er indeni afgøres ud fra dens centrum. Feltlinjerne er illustrative eksempler, og pilenes længde angiver ikke feltstyrken. Den viste samlede værdi bruger det eksakte resultat fra Gauss’ lov og er ikke en optælling af de tegnede linjer.', + 'flux.reference': 'Baseret på kursusreferencen: Elektrisk fluks og ladning, afsnit 3.2–3.4.', + 'flux.constant': 'Vakuumpermittivitet: ε₀ = {value} F/m.', + 'units.charge': '{value} nC', + 'units.distance': '{value} m', + 'units.area': '{value} m²', + 'units.flux': '{value} N·m²/C', }; - -export const danishLessons: Record<ExperimentId, LessonText> = { ...lab, ...test }; - -export function danishTest(id: Extract<ExperimentId, `test-${string}`>): TestCopy { - return test[id]; -} diff --git a/src/i18n/en.ts b/src/i18n/en.ts new file mode 100644 index 0000000..ff8d1b2 --- /dev/null +++ b/src/i18n/en.ts @@ -0,0 +1,101 @@ +export const en = { + 'app.title': 'Fieldwork · Electric flux and charge', + 'app.brand': 'fieldwork', + 'app.tagline': 'An electrophysics lab', + 'app.skip': 'Skip to the experiment', + 'app.navigation': 'Main navigation', + 'app.experiment': 'Experiment', + 'app.principle': 'The physics', + 'app.module': 'MODEL {number}', + 'app.footer': 'A little curiosity. A clearer understanding.', + 'app.footerNote': 'Electrophysics / Interactive learning', + 'app.loadError': 'The model could not be loaded. Reload the page to try again.', + 'app.reload': 'Reload page', + 'flux.eyebrow': 'ELECTRICITY, MADE VISIBLE', + 'flux.title': 'Electric flux and charge', + 'flux.subtitle': 'Move a charge. Reshape its surroundings. Discover what really determines electric flux.', + 'flux.topic': 'Gauss’s law', + 'flux.live': 'INTERACTIVE 3D MODEL', + 'flux.sceneTitle': 'One charge. One closed surface.', + 'flux.sceneLabel': '3D electric flux model. Drag to orbit and scroll to zoom. When focused, use arrow keys to orbit, plus or minus to zoom, and Home to reset the view.', + 'flux.sceneHint': 'Drag to orbit · Scroll to zoom', + 'flux.keyboardHint': 'Keyboard: arrows to orbit · + / − to zoom · Home to reset', + 'flux.resetView': 'Reset view', + 'flux.frontView': 'Front view', + 'flux.topView': 'Top view', + 'flux.cameraViews': 'Camera views', + 'flux.legend.positive': 'Positive field', + 'flux.legend.negative': 'Negative field', + 'flux.legend.zero': 'No electric field', + 'flux.legend.surface': 'Gaussian surface', + 'flux.legend.normal': 'Outward normal', + 'flux.marker': 'q = {value}', + 'flux.status.inside': 'Charge inside', + 'flux.status.outside': 'Charge outside', + 'flux.status.boundary': 'Charge on surface', + 'flux.status.neutral': 'Zero charge', + 'flux.controls': 'Make it your experiment', + 'flux.controlsHint': 'Change one thing. Watch what follows.', + 'flux.charge': 'Charge', + 'flux.position': 'Charge position', + 'flux.positionHint': 'Move along the x-axis through the surface.', + 'flux.surface': 'Gaussian surface', + 'flux.sphere': 'Sphere', + 'flux.cube': 'Cube', + 'flux.radius': 'Surface radius', + 'flux.halfSide': 'Half side length', + 'flux.surfaceHint': 'An imaginary boundary, not a physical shell.', + 'flux.layers': 'Visible layers', + 'flux.fieldLines': 'Electric field lines', + 'flux.surfaceLayer': 'Gaussian surface', + 'flux.normals': 'Surface normals', + 'flux.reset': 'Reset experiment', + 'flux.presets': 'Start with a scenario', + 'flux.preset.positive': 'Positive charge', + 'flux.preset.negative': 'Negative charge', + 'flux.preset.outside': 'Outside the surface', + 'flux.preset.custom': 'Custom experiment', + 'flux.readings': 'Live measurements', + 'flux.enclosed': 'Enclosed charge', + 'flux.area': 'Surface area', + 'flux.netFlux': 'Net electric flux', + 'flux.exact': 'Calculated from Gauss’s law', + 'flux.undefined': 'Undefined', + 'flux.result.insidePositive': 'More leaves than enters.', + 'flux.result.insideNegative': 'More enters than leaves.', + 'flux.result.outside': 'What enters also leaves.', + 'flux.result.neutral': 'No charge. No field. No flux.', + 'flux.result.boundary': 'The charge is on the boundary.', + 'flux.explanation.insidePositive': 'The surface encloses a positive charge, so the net flux is outward. Change the surface size: the total flux stays the same while the charge remains inside.', + 'flux.explanation.insideNegative': 'The surface encloses a negative charge, so the net flux is inward. The negative sign tells you the direction relative to the outward surface normal.', + 'flux.explanation.outside': 'The electric field is still present, but the enclosed charge is zero. Every field line that enters the closed surface also exits it, giving zero net flux.', + 'flux.explanation.neutral': 'With the source charge set to zero, this model has no electric field anywhere. The flux through the closed surface is also zero.', + 'flux.explanation.boundary': 'An ideal point charge on the surface makes the field singular there. Move the charge slightly inside or outside to get a well-defined enclosed charge and flux.', + 'flux.summary': '{status}. Enclosed charge: {charge}. Net electric flux: {flux}.', + 'flux.webglError': 'The 3D view needs WebGL 2. Try a browser with hardware acceleration enabled. The controls, calculations, and explanations still work.', + 'flux.webglLost': 'The 3D graphics connection was lost. Reload the page to restore it. Calculations still work.', + 'flux.physicsEyebrow': 'FROM THE MODEL TO THE IDEA', + 'flux.physicsTitle': 'A surface tells a story.', + 'flux.physicsSubtitle': 'Flux measures how much electric field passes through a surface. Here is how to read it.', + 'flux.lesson.one.number': '01', + 'flux.lesson.one.title': 'Direction matters', + 'flux.lesson.one.body': 'A small patch has an area vector perpendicular to it. Field pointing with this normal gives positive flux; field pointing against it gives negative flux. A tangent field contributes zero.', + 'flux.lesson.two.number': '02', + 'flux.lesson.two.title': 'Add up the whole surface', + 'flux.lesson.two.body': 'For a closed surface, every area vector points outward. Add the signed contributions from all its patches to find the net flux. A zero total does not mean the field is zero.', + 'flux.lesson.three.number': '03', + 'flux.lesson.three.title': 'Only what is inside counts', + 'flux.lesson.three.body': 'Gauss’s law connects that total to the enclosed charge. Size and shape can change the field across the surface, but they cannot change the total flux while the same charge stays enclosed.', + 'flux.try': 'TRY IT', + 'flux.tryText': 'Make the surface larger. Does the net flux change?', + 'flux.assumptions.title': 'About this model', + 'flux.assumptions.body': 'One stationary ideal point charge in vacuum; no other charges or materials. The surface is imaginary and centered at the origin. One scene unit represents one metre. The charge marker has a visible size only so you can find it; membership uses its center. Field lines are illustrative samples and arrow lengths do not measure field strength. The displayed total uses the exact Gauss’s-law result, not a count of the drawn lines.', + 'flux.reference': 'Based on the course reference: Electric flux and charge, sections 3.2–3.4.', + 'flux.constant': 'Vacuum permittivity: ε₀ = {value} F/m.', + 'units.charge': '{value} nC', + 'units.distance': '{value} m', + 'units.area': '{value} m²', + 'units.flux': '{value} N·m²/C', +} as const; + +export type MessageKey = keyof typeof en; diff --git a/src/i18n/index.ts b/src/i18n/index.ts new file mode 100644 index 0000000..2f46dd0 --- /dev/null +++ b/src/i18n/index.ts @@ -0,0 +1,28 @@ +import { en, type MessageKey } from './en'; +import { da } from './da'; + +// Add a catalog and register its BCP 47 locale here to support another language. +// Missing messages always fall back to English. Locale selection belongs to the shell. +const catalogs: Record<string, Partial<Record<MessageKey, string>>> = { en, da }; +export const defaultLocale = 'en'; +export type Parameters = Record<string, string | number>; + +export function createI18n(requestedLocale = defaultLocale) { + const locale = Object.hasOwn(catalogs, requestedLocale) ? requestedLocale : defaultLocale; + const catalog = catalogs[locale]; + return { + locale, + t(key: MessageKey, parameters: Parameters = {}): string { + const message = catalog[key] ?? en[key]; + return message.replace(/\{(\w+)\}/g, (_, name: string) => String(parameters[name] ?? `{${name}}`)); + }, + number(value: number, options: Intl.NumberFormatOptions = {}): string { + return new Intl.NumberFormat(locale, { maximumFractionDigits: 2, ...options }).format( + Object.is(value, -0) ? 0 : value, + ); + }, + }; +} + +export type I18n = ReturnType<typeof createI18n>; +export type { MessageKey }; diff --git a/src/i18n/lesson-text.ts b/src/i18n/lesson-text.ts deleted file mode 100644 index 0ed290c..0000000 --- a/src/i18n/lesson-text.ts +++ /dev/null @@ -1,22 +0,0 @@ -export interface ControlText { label: string; options?: Record<string, string> } - -export interface LessonText { - title: string; - shortTitle: string; - description: string; - explanation: string; - challenge: string; - limits: string; - sceneNote: string; - legend: string[]; - sourceTitle: string; - controls: Record<string, ControlText>; - equations?: string[]; -} - -export interface TestCopy extends LessonText { - questions: string[]; - answers: { title: string; steps: string[] }[]; - prediction: string; - predictionAnswer: string; -} diff --git a/src/i18n/narrate.ts b/src/i18n/narrate.ts deleted file mode 100644 index e112b42..0000000 --- a/src/i18n/narrate.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { formatDisplay, number, type Parameters, type TestId } from '../experiments'; -import { assemblyState, centralState, conservationState, motionState, rotationState } from '../test4'; - -const n = (value: number) => formatDisplay(value); - -export function narrate(id: TestId, p: Parameters): string { - if (id === 'test-work') { - const work = n(motionState(p).work * 1e9); - const route = p.path === 'curved' - ? 'Den krumme rute tilføjer bevægelse på tværs, men det endelige arbejde forbliver det samme.' - : 'Den rette rute gør skalarproduktet mellem kraft og forskydning let at se.'; - return `${route} Ved b udfører feltet ${work} nJ arbejde; $U$ falder med det beløb.`; - } - if (id === 'test-conservation') { - const s = conservationState(p); - return String.raw`Ved $r$ = ${n(s.r)} m er $U$ = ${n(s.u * 1e9)} nJ og $K$ = ${n(s.kinetic * 1e9)} nJ. Summen forbliver ${n(s.total * 1e9)} nJ. Det hidtidige arbejde er stigningen i $K$.`; - } - if (id === 'test-central') { - const s = centralState(p); - return s.rb === Infinity - ? String.raw`I det uendelige er $U_b=0$ eksakt. $W_{a\infty}=U_a$ = ${n(s.work * 1e9)} nJ. Diagrammets kant er kun en visuel fortsættelse mod uendelighed.` - : String.raw`Feltet udfører ${n(s.work * 1e9)} nJ arbejde fra $r_a$ = ${s.ra} m til $r_b$ = ${s.rb} m. Skift af fortegn på en af ladningerne vender $U$ og $W$.`; - } - if (id === 'test-assembly') { - const s = assemblyState(p); - return String.raw`Trin ${s.stage}: ${s.stage * (s.stage - 1) / 2} forskellige par er til stede. $U_{123}$ = ${n(s.base * 1e9)} nJ; tilføjelse af $q_0$ ændrer den med ${n(s.extra * 1e9)} nJ. Negativ parenergi betyder modsatte fortegn.`; - } - if (id === 'test-uniform') { - const m = motionState(p); - return String.raw`$\alpha = ${number(p, 'angle')}^\circ$ projicerer $s$ = ${m.s} m over på $\Delta x$ = ${n(m.dx)} m. $W$ = ${n(m.work * 1e9)} nJ og $U_b$ = ${n(-m.work * 1e9)} nJ med $U_a=0$.`; - } - const s = rotationState(p); - const degrees = s.theta * 180 / Math.PI; - if (s.field === 0) return String.raw`$E=0$: begge kræfter, kraftmomentet, rotationsarbejdet og dipolens vekselvirkningsenergi forsvinder. Der er ingen foretrukken orientering.`; - if (degrees < 0.01) return String.raw`Indrettet ved $0^\circ$: $U$ er minimum, og kraftmomentet er nul. Det er stabil ligevægt; en lille drejning giver et tilbageførende kraftmoment.`; - if (degrees > 179.99) return String.raw`Modsat ved $180^\circ$: $U$ er maksimum, og kraftmomentet er nul. Det er ustabil ligevægt; en lille drejning fører den væk fra denne position.`; - const follow = Math.abs(degrees - 90) < 0.01 - ? String.raw`Ved $90^\circ$ er $U=0$ per valg, men kraftmomentet er størst: det er ikke ligevægt.` - : 'Følg punktet på energikurven, mens du drejer.'; - return String.raw`Ved $\theta = ${n(degrees)}^\circ$ er $\tau_z$ = ${n(s.torqueZ * 1e9)} nN·m. Den negative z-retning mindsker $\theta$ mod indretning. ${follow}`; -} diff --git a/src/i18n/present.ts b/src/i18n/present.ts deleted file mode 100644 index d734efd..0000000 --- a/src/i18n/present.ts +++ /dev/null @@ -1,76 +0,0 @@ -import type { Choice, Experiment, ExperimentId, Measurement, Parameters, Slider, TestId } from '../experiments'; -import { measurementLabel, word, getLanguage, type Language } from '../i18n'; -import type { TestLesson } from '../test4'; -import { danishLessons, danishTest } from './da'; -import type { LessonText, TestCopy } from './lesson-text'; -import { narrate } from './narrate'; - -interface Catalog { - lessons: Record<ExperimentId, LessonText>; - test: (id: TestId) => TestCopy; - narrate: (id: TestId, p: Parameters) => string; -} - -const catalogs: Record<Exclude<Language, 'en'>, Catalog> = { - da: { lessons: danishLessons, test: danishTest, narrate }, -}; - -function localizeMeasurements(measurements: Measurement[]): Measurement[] { - return measurements.map(measurement => ({ - label: measurementLabel(measurement.label), - value: word(measurement.value), - unit: word(measurement.unit), - })); -} - -function localizeControls(experiment: Experiment, lessons: Record<ExperimentId, LessonText>): (Slider | Choice)[] { - const copy = lessons[experiment.id].controls; - return experiment.controls.map(control => { - const text = copy[control.id]; - if (!text) return control; - if (control.kind === 'range') return { ...control, label: text.label }; - return { - ...control, - label: text.label, - options: control.options.map(option => ({ ...option, label: text.options?.[option.value] ?? option.label })), - }; - }); -} - -export function presentLesson<T extends Experiment>(experiment: T): T { - const measure = (p: Parameters) => localizeMeasurements(experiment.measure(p)); - const language = getLanguage(); - if (language === 'en') return { ...experiment, measure }; - const catalog = catalogs[language]; - const text = catalog.lessons[experiment.id]; - const localized = { - ...experiment, - title: text.title, - shortTitle: text.shortTitle, - description: text.description, - explanation: text.explanation, - challenge: text.challenge, - limits: text.limits, - sceneNote: text.sceneNote, - legend: experiment.legend.map(([colour], index) => [colour, text.legend[index] ?? experiment.legend[index][1]] as [string, string]), - source: { ...experiment.source, title: text.sourceTitle }, - controls: localizeControls(experiment, catalog.lessons), - equations: text.equations ?? experiment.equations, - measure, - }; - if (!('questions' in experiment)) return localized as T; - const lesson = experiment as unknown as TestLesson; - const test = catalog.test(lesson.id); - return { - ...localized, - questions: test.questions, - prediction: test.prediction, - predictionAnswer: test.predictionAnswer, - answers: lesson.answers.map((part, index) => ({ - ...part, - title: test.answers[index].title, - steps: part.steps.map((step, stepIndex) => ({ ...step, why: test.answers[index].steps[stepIndex] })), - })), - observe: (p: Parameters) => catalog.narrate(lesson.id, p), - } as T; -} |