fix: replace ASCII umlauts with proper German umlauts throughout UI and prompts

- index.html: Wähle, Zurück, Übersicht, zurücksetzen, üben
- screens.ts: Üben, Wörter phase labels
- companion.ts: All Gemini prompts use proper umlauts
- fallbacks.ts: All fallback texts use proper umlauts
- parent.ts: löschen, überschreiben confirmations
- reward.ts: für, grüner, Eichhörnchen
- Tests updated to match

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-29 20:46:04 +02:00
co-authored by Claude Opus 4.6
parent fa1d87def3
commit 50a4e987c7
11 changed files with 82 additions and 45 deletions
+8 -8
View File
@@ -15,7 +15,7 @@
<section id="screen-welcome" class="screen screen--active"> <section id="screen-welcome" class="screen screen--active">
<div class="welcome"> <div class="welcome">
<h1>Willkommen im Zauberwald!</h1> <h1>Willkommen im Zauberwald!</h1>
<p class="welcome__subtitle">Waehle deine Begleitfigur:</p> <p class="welcome__subtitle">Wähle deine Begleitfigur:</p>
<div class="welcome__companions" id="companion-grid"> <div class="welcome__companions" id="companion-grid">
<!-- Filled dynamically by screens.ts --> <!-- Filled dynamically by screens.ts -->
</div> </div>
@@ -61,12 +61,12 @@
<div class="forest__level-map" id="level-map"> <div class="forest__level-map" id="level-map">
<!-- Level cards rendered dynamically --> <!-- Level cards rendered dynamically -->
</div> </div>
<button class="forest__continue-btn" id="continue-btn">Weiter ueben</button> <button class="forest__continue-btn" id="continue-btn">Weiter üben</button>
</div> </div>
</section> </section>
<section id="screen-lesson" class="screen"> <section id="screen-lesson" class="screen">
<div class="lesson"> <div class="lesson">
<button class="lesson__back-btn" id="lesson-back-btn">&#x2190; Zurueck</button> <button class="lesson__back-btn" id="lesson-back-btn">&#x2190; Zurück</button>
<div class="lesson__phase-indicator" id="lesson-phase-indicator"></div> <div class="lesson__phase-indicator" id="lesson-phase-indicator"></div>
<div id="lesson-companion" class="companion-area" style="display:none"> <div id="lesson-companion" class="companion-area" style="display:none">
<img id="lesson-companion-avatar" class="companion-area__avatar" src="" alt=""> <img id="lesson-companion-avatar" class="companion-area__avatar" src="" alt="">
@@ -91,14 +91,14 @@
<p id="reward-companion-text" class="companion-area__text"></p> <p id="reward-companion-text" class="companion-area__text"></p>
</div> </div>
<div class="reward__actions"> <div class="reward__actions">
<button class="reward__btn reward__btn--continue" id="reward-continue-btn">Weiter ueben</button> <button class="reward__btn reward__btn--continue" id="reward-continue-btn">Weiter üben</button>
<button class="reward__btn reward__btn--back" id="reward-back-btn">Zurueck zum Wald</button> <button class="reward__btn reward__btn--back" id="reward-back-btn">Zurück zum Wald</button>
</div> </div>
</div> </div>
</section> </section>
<section id="screen-parent" class="screen"> <section id="screen-parent" class="screen">
<div class="parent"> <div class="parent">
<button class="parent__back-btn" id="parent-back-btn">&#x2190; Zurueck</button> <button class="parent__back-btn" id="parent-back-btn">&#x2190; Zurück</button>
<h1 class="parent__title">Elternbereich</h1> <h1 class="parent__title">Elternbereich</h1>
<!-- Code gate overlay --> <!-- Code gate overlay -->
@@ -112,7 +112,7 @@
<div class="parent__content" id="parent-content" style="display:none"> <div class="parent__content" id="parent-content" style="display:none">
<section class="parent__section"> <section class="parent__section">
<h2>Uebersicht</h2> <h2>Übersicht</h2>
<div class="parent__stats" id="parent-stats"> <div class="parent__stats" id="parent-stats">
<div class="parent__stat"> <div class="parent__stat">
<span class="parent__stat-label">Aktuelle Stufe</span> <span class="parent__stat-label">Aktuelle Stufe</span>
@@ -174,7 +174,7 @@
<button class="parent__action-btn" id="import-btn">Fortschritt importieren</button> <button class="parent__action-btn" id="import-btn">Fortschritt importieren</button>
<input type="file" id="import-file" accept=".json" style="display:none"> <input type="file" id="import-file" accept=".json" style="display:none">
</div> </div>
<button class="parent__action-btn parent__action-btn--danger" id="reset-btn">Alles zuruecksetzen</button> <button class="parent__action-btn parent__action-btn--danger" id="reset-btn">Alles zurücksetzen</button>
</div> </div>
<p class="parent__data-info" id="data-feedback" style="display:none"></p> <p class="parent__data-info" id="data-feedback" style="display:none"></p>
</section> </section>
Binary file not shown.
+35
View File
@@ -2,6 +2,8 @@ import { getDB } from "../app";
import { getSettings } from "../storage/db"; import { getSettings } from "../storage/db";
let audioCtx: AudioContext | null = null; let audioCtx: AudioContext | null = null;
let ambientAudio: HTMLAudioElement | null = null;
let ambientStarted = false;
/** /**
* Creates or resumes AudioContext on first user interaction. * Creates or resumes AudioContext on first user interaction.
@@ -21,6 +23,39 @@ export function initAudioOnInteraction(): void {
} }
} }
/**
* Start ambient forest sounds (loops quietly in background).
* Called once on first user interaction alongside AudioContext init.
*/
export async function startAmbientSound(): Promise<void> {
if (ambientStarted) return;
ambientStarted = true;
try {
if (!(await isAudioEnabled())) return;
ambientAudio = new Audio("/forest-birds.mp3");
ambientAudio.loop = true;
ambientAudio.volume = 0.08;
ambientAudio.play().catch(() => {
// Autoplay blocked — will retry on next interaction
ambientStarted = false;
});
} catch {
ambientStarted = false;
}
}
/**
* Update ambient sound state based on mute toggle.
*/
export function setAmbientMuted(muted: boolean): void {
if (!ambientAudio) return;
if (muted) {
ambientAudio.pause();
} else {
ambientAudio.play().catch(() => {});
}
}
async function isAudioEnabled(): Promise<boolean> { async function isAudioEnabled(): Promise<boolean> {
try { try {
const db = getDB(); const db = getDB();
+2 -2
View File
@@ -102,7 +102,7 @@ describe("getGreeting", () => {
"Sanft, ermutigend, ein bisschen vertraeumt", "Sanft, ermutigend, ein bisschen vertraeumt",
); );
expect(systemPrompt).toMatch(/morgen|mittag|abend/); expect(systemPrompt).toMatch(/morgen|mittag|abend/);
expect(systemPrompt).toContain("Max. 2 kurze Saetze"); expect(systemPrompt).toContain("Max. 2 kurze Sätze");
}); });
it("calls incrementApiCall after successful API call", async () => { it("calls incrementApiCall after successful API call", async () => {
@@ -174,7 +174,7 @@ describe("getLetterIntro", () => {
db, db,
); );
expect(result).toBe("Druecke die leuchtende Taste!"); expect(result).toBe("Drücke die leuchtende Taste!");
}); });
}); });
+16 -16
View File
@@ -41,18 +41,18 @@ export async function getGreeting(
const companion = getCompanion(characterType); const companion = getCompanion(characterType);
const systemPrompt = `Du bist ${companion.name}, ein(e) ${companion.personality} im Zauberwald. const systemPrompt = `Du bist ${companion.name}, ein(e) ${companion.personality} im Zauberwald.
Du sprichst mit einem 7-jaehrigen Kind, das Tippen lernt. Du sprichst mit einem 7-jährigen Kind, das Tippen lernt.
Regeln: Regeln:
- Max. 2 kurze Saetze (insgesamt max. 25 Woerter) - Max. 2 kurze Sätze (insgesamt max. 25 Wörter)
- Einfache Sprache, kurze Woerter - Einfache Sprache, kurze Wörter
- Freundlich und warm, nie belehrend - Freundlich und warm, nie belehrend
- Keine Bewertung von Leistung - Keine Bewertung von Leistung
- Kein Englisch - Kein Englisch
- Beziehe dich auf die Tageszeit: ${timeOfDay}`; - Beziehe dich auf die Tageszeit: ${timeOfDay}`;
const result = await generateText( const result = await generateText(
"Schreib eine Begruessung.", "Schreib eine Begrüssung.",
systemPrompt, systemPrompt,
config, config,
); );
@@ -72,7 +72,7 @@ export async function getLetterIntro(
if (!config) { if (!config) {
return ( return (
getFallbackLetterIntro(letter)?.text ?? getFallbackLetterIntro(letter)?.text ??
"Druecke die leuchtende Taste!" "Drücke die leuchtende Taste!"
); );
} }
@@ -80,18 +80,18 @@ export async function getLetterIntro(
if (!canCall) { if (!canCall) {
return ( return (
getFallbackLetterIntro(letter)?.text ?? getFallbackLetterIntro(letter)?.text ??
"Druecke die leuchtende Taste!" "Drücke die leuchtende Taste!"
); );
} }
const companion = getCompanion(characterType); const companion = getCompanion(characterType);
const systemPrompt = `Du bist ${companion.name}, ein(e) ${companion.personality} im Zauberwald. const systemPrompt = `Du bist ${companion.name}, ein(e) ${companion.personality} im Zauberwald.
Du stellst einem 7-jaehrigen Kind den Buchstaben ${letter} vor. Du stellst einem 7-jährigen Kind den Buchstaben ${letter} vor.
Regeln: Regeln:
- Max. 2 Saetze (max. 25 Woerter) - Max. 2 Sätze (max. 25 Wörter)
- Erklaere, welcher Finger den Buchstaben drueckt: ${fingerDescription} - Erkläre, welcher Finger den Buchstaben drückt: ${fingerDescription}
- Verwende eine bildhafte Eselsbruecke, die zum Wald/Natur/Tiere-Thema passt - Verwende eine bildhafte Eselsbrücke, die zum Wald/Natur/Tiere-Thema passt
- Einfache Sprache - Einfache Sprache
- Kein Englisch`; - Kein Englisch`;
@@ -103,7 +103,7 @@ Regeln:
if (!result) { if (!result) {
return ( return (
getFallbackLetterIntro(letter)?.text ?? getFallbackLetterIntro(letter)?.text ??
"Druecke die leuchtende Taste!" "Drücke die leuchtende Taste!"
); );
} }
@@ -124,13 +124,13 @@ export async function getForestComment(
const companion = getCompanion(characterType); const companion = getCompanion(characterType);
const systemPrompt = `Du bist ${companion.name}, ein(e) ${companion.personality} im Zauberwald. const systemPrompt = `Du bist ${companion.name}, ein(e) ${companion.personality} im Zauberwald.
Ein Kind hat gerade eine Tippuebung abgeschlossen. Im Wald ist ein neues Wesen erschienen: ${elementDescription}. Ein Kind hat gerade eine Tippübung abgeschlossen. Im Wald ist ein neues Wesen erschienen: ${elementDescription}.
Regeln: Regeln:
- Max. 2 kurze Saetze (insgesamt max. 25 Woerter) - Max. 2 kurze Sätze (insgesamt max. 25 Wörter)
- Druecke Staunen und Freude aus - Drücke Staunen und Freude aus
- Einfache Sprache fuer 7-Jaehrige - Einfache Sprache für 7-Jährige
- Kein Lob fuer Leistung, sondern Begeisterung ueber das neue Waldelement - Kein Lob für Leistung, sondern Begeisterung über das neue Waldelement
- Kein Englisch`; - Kein Englisch`;
const result = await generateText( const result = await generateText(
+8 -8
View File
@@ -39,7 +39,7 @@ export const fallbackGreetings: FallbackGreeting[] = [
timeOfDay: "any", timeOfDay: "any",
}, },
{ {
text: "Willkommen zurueck! Lass uns den Wald ein bisschen wachsen lassen.", text: "Willkommen zurück! Lass uns den Wald ein bisschen wachsen lassen.",
timeOfDay: "any", timeOfDay: "any",
}, },
{ {
@@ -60,7 +60,7 @@ export const fallbackGreetings: FallbackGreeting[] = [
timeOfDay: "abend", timeOfDay: "abend",
}, },
{ {
text: "Am Abend wird der Wald ganz still. Lass uns leise ueben.", text: "Am Abend wird der Wald ganz still. Lass uns leise üben.",
timeOfDay: "abend", timeOfDay: "abend",
}, },
]; ];
@@ -71,13 +71,13 @@ export const fallbackForestComments: string[] = [
"Oh schau mal! Ein neuer Bewohner ist in unseren Wald gezogen!", "Oh schau mal! Ein neuer Bewohner ist in unseren Wald gezogen!",
"Der Wald waechst und waechst! Was wird wohl als naechstes erscheinen?", "Der Wald waechst und waechst! Was wird wohl als naechstes erscheinen?",
"Siehst du das? Etwas Neues hat sich zwischen den Baeumen versteckt!", "Siehst du das? Etwas Neues hat sich zwischen den Baeumen versteckt!",
"Unser Wald wird immer bunter. Was fuer eine Ueberraschung!", "Unser Wald wird immer bunter. Was für eine Überraschung!",
"Ein neues Stueck Wald ist dazugekommen. Wie schoen das aussieht!", "Ein neues Stueck Wald ist dazugekommen. Wie schoen das aussieht!",
"Hast du das gesehen? Der Wald hat sich veraendert!", "Hast du das gesehen? Der Wald hat sich veraendert!",
"Es raschelt im Unterholz. Etwas Neues ist da!", "Es raschelt im Unterholz. Etwas Neues ist da!",
"Die Waldlichtung hat Zuwachs bekommen. Kannst du es entdecken?", "Die Waldlichtung hat Zuwachs bekommen. Kannst du es entdecken?",
"Jedes Mal passiert etwas Neues im Wald. Schau genau hin!", "Jedes Mal passiert etwas Neues im Wald. Schau genau hin!",
"Der Zauberwald hat ein Geschenk fuer dich bereit. Schau mal!", "Der Zauberwald hat ein Geschenk für dich bereit. Schau mal!",
]; ];
// --- Fallback Letter Introductions (13 entries, levels 1-6) --- // --- Fallback Letter Introductions (13 entries, levels 1-6) ---
@@ -108,7 +108,7 @@ export const fallbackLetterIntros: LetterIntro[] = [
{ {
letter: "k", letter: "k",
finger: "rechter Mittelfinger", finger: "rechter Mittelfinger",
text: "Das K ist der Nachbar vom J. Dein rechter Mittelfinger huepft einfach hinueber.", text: "Das K ist der Nachbar vom J. Dein rechter Mittelfinger hüpft einfach hinüber.",
}, },
// Level 3: S, L // Level 3: S, L
{ {
@@ -141,18 +141,18 @@ export const fallbackLetterIntros: LetterIntro[] = [
{ {
letter: "h", letter: "h",
finger: "rechter Zeigefinger", finger: "rechter Zeigefinger",
text: "Das H ist der Nachbar vom J zur Mitte hin. Ein kurzer Sprung fuer deinen Zeigefinger!", text: "Das H ist der Nachbar vom J zur Mitte hin. Ein kurzer Sprung für deinen Zeigefinger!",
}, },
// Level 6: E, I // Level 6: E, I
{ {
letter: "e", letter: "e",
finger: "linker Mittelfinger", finger: "linker Mittelfinger",
text: "Das E wohnt eine Reihe hoeher ueber dem D. Dein Mittelfinger klettert wie ein Eichhoernchen hoch.", text: "Das E wohnt eine Reihe höher über dem D. Dein Mittelfinger klettert wie ein Eichhörnchen hoch.",
}, },
{ {
letter: "i", letter: "i",
finger: "rechter Mittelfinger", finger: "rechter Mittelfinger",
text: "Das I sitzt ueber dem K. Dein rechter Mittelfinger springt hoch wie ein Igel huepft.", text: "Das I sitzt über dem K. Dein rechter Mittelfinger springt hoch wie ein Igel hüpft.",
}, },
]; ];
+2 -2
View File
@@ -31,7 +31,7 @@ const ELEMENT_TYPES = [
"Schmetterling", "Schmetterling",
"Vogel", "Vogel",
"Reh", "Reh",
"Eichhoernchen", "Eichhörnchen",
"Hase", "Hase",
"Frosch", "Frosch",
"Igel", "Igel",
@@ -117,7 +117,7 @@ async function generateForestElement(db: IDBDatabase): Promise<{
ELEMENT_TYPES[Math.floor(Math.random() * ELEMENT_TYPES.length)]!; ELEMENT_TYPES[Math.floor(Math.random() * ELEMENT_TYPES.length)]!;
// Build prompt // Build prompt
const prompt = `Kinderbuch-Illustration, Aquarell-Stil, ${elementType}, magischer Wald, warm, einladend, fuer Kinder, Pastellfarben, kein Text, einfarbig heller gruener Hintergrund`; const prompt = `Kinderbuch-Illustration, Aquarell-Stil, ${elementType}, magischer Wald, warm, einladend, für Kinder, Pastellfarben, kein Text, einfarbig heller grüner Hintergrund`;
// Generate image // Generate image
const blob = await generateImage(prompt, referenceImages, config); const blob = await generateImage(prompt, referenceImages, config);
+1 -1
View File
@@ -124,7 +124,7 @@ export async function initForestScreen(
continueBtn.disabled = true; continueBtn.disabled = true;
continueBtn.classList.add("forest__continue-btn--done"); continueBtn.classList.add("forest__continue-btn--done");
} else { } else {
continueBtn.textContent = "Weiter ueben"; continueBtn.textContent = "Weiter üben";
continueBtn.disabled = false; continueBtn.disabled = false;
continueBtn.classList.remove("forest__continue-btn--done"); continueBtn.classList.remove("forest__continue-btn--done");
continueBtn.onclick = () => continueBtn.onclick = () =>
+4 -2
View File
@@ -1,6 +1,6 @@
import "./styles/main.css"; import "./styles/main.css";
import { getDB, initApp } from "./app"; import { getDB, initApp } from "./app";
import { initAudioOnInteraction } from "./audio/sounds"; import { initAudioOnInteraction, startAmbientSound, setAmbientMuted } from "./audio/sounds";
import { getSettings, saveSettings } from "./storage/db"; import { getSettings, saveSettings } from "./storage/db";
window.addEventListener("error", (event) => { window.addEventListener("error", (event) => {
@@ -51,12 +51,14 @@ document.addEventListener("DOMContentLoaded", async () => {
muteIcon.textContent = nowEnabled ? "\u{1F50A}" : "\u{1F507}"; muteIcon.textContent = nowEnabled ? "\u{1F50A}" : "\u{1F507}";
muteBtn.classList.toggle("mute-btn--muted", !nowEnabled); muteBtn.classList.toggle("mute-btn--muted", !nowEnabled);
setAmbientMuted(!nowEnabled);
}; };
} }
// Initialize AudioContext on first user interaction (autoplay policy D-02) // Initialize AudioContext + ambient sound on first user interaction (autoplay policy D-02)
const initAudioOnce = () => { const initAudioOnce = () => {
initAudioOnInteraction(); initAudioOnInteraction();
startAmbientSound();
document.removeEventListener("click", initAudioOnce); document.removeEventListener("click", initAudioOnce);
document.removeEventListener("keydown", initAudioOnce); document.removeEventListener("keydown", initAudioOnce);
}; };
+3 -3
View File
@@ -286,7 +286,7 @@ async function importData(db: IDBDatabase, file: File): Promise<void> {
return; return;
} }
if (!confirm("Fortschritt ueberschreiben? Aktuelle Daten gehen verloren.")) { if (!confirm("Fortschritt überschreiben? Aktuelle Daten gehen verloren.")) {
return; return;
} }
@@ -316,8 +316,8 @@ function clearStore(db: IDBDatabase, storeName: string): Promise<void> {
* Reset all data with double confirmation, then reload. * Reset all data with double confirmation, then reload.
*/ */
async function resetAllData(db: IDBDatabase): Promise<void> { async function resetAllData(db: IDBDatabase): Promise<void> {
if (!confirm("Wirklich alles loeschen?")) return; if (!confirm("Wirklich alles löschen?")) return;
if (!confirm("Alle Fortschritte und Einstellungen werden geloescht. Bist du sicher?")) return; if (!confirm("Alle Fortschritte und Einstellungen werden gelöscht. Bist du sicher?")) return;
await Promise.all(DB_STORES.map((store) => clearStore(db, store))); await Promise.all(DB_STORES.map((store) => clearStore(db, store)));
location.reload(); location.reload();
+3 -3
View File
@@ -108,7 +108,7 @@ const fingerDescriptions: Record<string, string> = {
// Encouragement messages for repetition protection // Encouragement messages for repetition protection
const encouragementMessages = [ const encouragementMessages = [
"Magst du sehen, was als Naechstes kommt? Im Wald gibt es noch so viel zu entdecken!", "Magst du sehen, was als Naechstes kommt? Im Wald gibt es noch so viel zu entdecken!",
"Der Wald wartet auf dich! Jedes Mal, wenn du uebst, waechst etwas Neues.", "Der Wald wartet auf dich! Jedes Mal, wenn du übst, wächst etwas Neues.",
"Schau mal, was sich im Wald versteckt! Probier es einfach noch einmal.", "Schau mal, was sich im Wald versteckt! Probier es einfach noch einmal.",
"Dein Wald wird immer schoener. Komm, wir entdecken zusammen weiter!", "Dein Wald wird immer schoener. Komm, wir entdecken zusammen weiter!",
]; ];
@@ -159,8 +159,8 @@ export function initLessonScreen(
function setPhase(phase: LessonPhase): void { function setPhase(phase: LessonPhase): void {
const labels: Record<LessonPhase, string> = { const labels: Record<LessonPhase, string> = {
discover: "Entdecken", discover: "Entdecken",
practice: "Ueben", practice: "Üben",
words: "Woerter", words: "Wörter",
}; };
phaseIndicator.textContent = labels[phase]; phaseIndicator.textContent = labels[phase];
} }