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:
@@ -2,6 +2,8 @@ import { getDB } from "../app";
|
||||
import { getSettings } from "../storage/db";
|
||||
|
||||
let audioCtx: AudioContext | null = null;
|
||||
let ambientAudio: HTMLAudioElement | null = null;
|
||||
let ambientStarted = false;
|
||||
|
||||
/**
|
||||
* 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> {
|
||||
try {
|
||||
const db = getDB();
|
||||
|
||||
@@ -102,7 +102,7 @@ describe("getGreeting", () => {
|
||||
"Sanft, ermutigend, ein bisschen vertraeumt",
|
||||
);
|
||||
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 () => {
|
||||
@@ -174,7 +174,7 @@ describe("getLetterIntro", () => {
|
||||
db,
|
||||
);
|
||||
|
||||
expect(result).toBe("Druecke die leuchtende Taste!");
|
||||
expect(result).toBe("Drücke die leuchtende Taste!");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+16
-16
@@ -41,18 +41,18 @@ export async function getGreeting(
|
||||
|
||||
const companion = getCompanion(characterType);
|
||||
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:
|
||||
- Max. 2 kurze Saetze (insgesamt max. 25 Woerter)
|
||||
- Einfache Sprache, kurze Woerter
|
||||
- Max. 2 kurze Sätze (insgesamt max. 25 Wörter)
|
||||
- Einfache Sprache, kurze Wörter
|
||||
- Freundlich und warm, nie belehrend
|
||||
- Keine Bewertung von Leistung
|
||||
- Kein Englisch
|
||||
- Beziehe dich auf die Tageszeit: ${timeOfDay}`;
|
||||
|
||||
const result = await generateText(
|
||||
"Schreib eine Begruessung.",
|
||||
"Schreib eine Begrüssung.",
|
||||
systemPrompt,
|
||||
config,
|
||||
);
|
||||
@@ -72,7 +72,7 @@ export async function getLetterIntro(
|
||||
if (!config) {
|
||||
return (
|
||||
getFallbackLetterIntro(letter)?.text ??
|
||||
"Druecke die leuchtende Taste!"
|
||||
"Drücke die leuchtende Taste!"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -80,18 +80,18 @@ export async function getLetterIntro(
|
||||
if (!canCall) {
|
||||
return (
|
||||
getFallbackLetterIntro(letter)?.text ??
|
||||
"Druecke die leuchtende Taste!"
|
||||
"Drücke die leuchtende Taste!"
|
||||
);
|
||||
}
|
||||
|
||||
const companion = getCompanion(characterType);
|
||||
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:
|
||||
- Max. 2 Saetze (max. 25 Woerter)
|
||||
- Erklaere, welcher Finger den Buchstaben drueckt: ${fingerDescription}
|
||||
- Verwende eine bildhafte Eselsbruecke, die zum Wald/Natur/Tiere-Thema passt
|
||||
- Max. 2 Sätze (max. 25 Wörter)
|
||||
- Erkläre, welcher Finger den Buchstaben drückt: ${fingerDescription}
|
||||
- Verwende eine bildhafte Eselsbrücke, die zum Wald/Natur/Tiere-Thema passt
|
||||
- Einfache Sprache
|
||||
- Kein Englisch`;
|
||||
|
||||
@@ -103,7 +103,7 @@ Regeln:
|
||||
if (!result) {
|
||||
return (
|
||||
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 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:
|
||||
- Max. 2 kurze Saetze (insgesamt max. 25 Woerter)
|
||||
- Druecke Staunen und Freude aus
|
||||
- Einfache Sprache fuer 7-Jaehrige
|
||||
- Kein Lob fuer Leistung, sondern Begeisterung ueber das neue Waldelement
|
||||
- Max. 2 kurze Sätze (insgesamt max. 25 Wörter)
|
||||
- Drücke Staunen und Freude aus
|
||||
- Einfache Sprache für 7-Jährige
|
||||
- Kein Lob für Leistung, sondern Begeisterung über das neue Waldelement
|
||||
- Kein Englisch`;
|
||||
|
||||
const result = await generateText(
|
||||
|
||||
@@ -39,7 +39,7 @@ export const fallbackGreetings: FallbackGreeting[] = [
|
||||
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",
|
||||
},
|
||||
{
|
||||
@@ -60,7 +60,7 @@ export const fallbackGreetings: FallbackGreeting[] = [
|
||||
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",
|
||||
},
|
||||
];
|
||||
@@ -71,13 +71,13 @@ export const fallbackForestComments: string[] = [
|
||||
"Oh schau mal! Ein neuer Bewohner ist in unseren Wald gezogen!",
|
||||
"Der Wald waechst und waechst! Was wird wohl als naechstes erscheinen?",
|
||||
"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!",
|
||||
"Hast du das gesehen? Der Wald hat sich veraendert!",
|
||||
"Es raschelt im Unterholz. Etwas Neues ist da!",
|
||||
"Die Waldlichtung hat Zuwachs bekommen. Kannst du es entdecken?",
|
||||
"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) ---
|
||||
@@ -108,7 +108,7 @@ export const fallbackLetterIntros: LetterIntro[] = [
|
||||
{
|
||||
letter: "k",
|
||||
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
|
||||
{
|
||||
@@ -141,18 +141,18 @@ export const fallbackLetterIntros: LetterIntro[] = [
|
||||
{
|
||||
letter: "h",
|
||||
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
|
||||
{
|
||||
letter: "e",
|
||||
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",
|
||||
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.",
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ const ELEMENT_TYPES = [
|
||||
"Schmetterling",
|
||||
"Vogel",
|
||||
"Reh",
|
||||
"Eichhoernchen",
|
||||
"Eichhörnchen",
|
||||
"Hase",
|
||||
"Frosch",
|
||||
"Igel",
|
||||
@@ -117,7 +117,7 @@ async function generateForestElement(db: IDBDatabase): Promise<{
|
||||
ELEMENT_TYPES[Math.floor(Math.random() * ELEMENT_TYPES.length)]!;
|
||||
|
||||
// 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
|
||||
const blob = await generateImage(prompt, referenceImages, config);
|
||||
|
||||
+1
-1
@@ -124,7 +124,7 @@ export async function initForestScreen(
|
||||
continueBtn.disabled = true;
|
||||
continueBtn.classList.add("forest__continue-btn--done");
|
||||
} else {
|
||||
continueBtn.textContent = "Weiter ueben";
|
||||
continueBtn.textContent = "Weiter üben";
|
||||
continueBtn.disabled = false;
|
||||
continueBtn.classList.remove("forest__continue-btn--done");
|
||||
continueBtn.onclick = () =>
|
||||
|
||||
+4
-2
@@ -1,6 +1,6 @@
|
||||
import "./styles/main.css";
|
||||
import { getDB, initApp } from "./app";
|
||||
import { initAudioOnInteraction } from "./audio/sounds";
|
||||
import { initAudioOnInteraction, startAmbientSound, setAmbientMuted } from "./audio/sounds";
|
||||
import { getSettings, saveSettings } from "./storage/db";
|
||||
|
||||
window.addEventListener("error", (event) => {
|
||||
@@ -51,12 +51,14 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
|
||||
muteIcon.textContent = nowEnabled ? "\u{1F50A}" : "\u{1F507}";
|
||||
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 = () => {
|
||||
initAudioOnInteraction();
|
||||
startAmbientSound();
|
||||
document.removeEventListener("click", initAudioOnce);
|
||||
document.removeEventListener("keydown", initAudioOnce);
|
||||
};
|
||||
|
||||
+3
-3
@@ -286,7 +286,7 @@ async function importData(db: IDBDatabase, file: File): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!confirm("Fortschritt ueberschreiben? Aktuelle Daten gehen verloren.")) {
|
||||
if (!confirm("Fortschritt überschreiben? Aktuelle Daten gehen verloren.")) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -316,8 +316,8 @@ function clearStore(db: IDBDatabase, storeName: string): Promise<void> {
|
||||
* Reset all data with double confirmation, then reload.
|
||||
*/
|
||||
async function resetAllData(db: IDBDatabase): Promise<void> {
|
||||
if (!confirm("Wirklich alles loeschen?")) return;
|
||||
if (!confirm("Alle Fortschritte und Einstellungen werden geloescht. Bist du sicher?")) return;
|
||||
if (!confirm("Wirklich alles löschen?")) return;
|
||||
if (!confirm("Alle Fortschritte und Einstellungen werden gelöscht. Bist du sicher?")) return;
|
||||
|
||||
await Promise.all(DB_STORES.map((store) => clearStore(db, store)));
|
||||
location.reload();
|
||||
|
||||
+3
-3
@@ -108,7 +108,7 @@ const fingerDescriptions: Record<string, string> = {
|
||||
// Encouragement messages for repetition protection
|
||||
const encouragementMessages = [
|
||||
"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.",
|
||||
"Dein Wald wird immer schoener. Komm, wir entdecken zusammen weiter!",
|
||||
];
|
||||
@@ -159,8 +159,8 @@ export function initLessonScreen(
|
||||
function setPhase(phase: LessonPhase): void {
|
||||
const labels: Record<LessonPhase, string> = {
|
||||
discover: "Entdecken",
|
||||
practice: "Ueben",
|
||||
words: "Woerter",
|
||||
practice: "Üben",
|
||||
words: "Wörter",
|
||||
};
|
||||
phaseIndicator.textContent = labels[phase];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user