Add central exercise translation system (7 languages, 30+ keys)

useNativeLanguage.ts: Hook that reads bp_native_language from
localStorage and provides t(key) for translated UI text and
wordInNative() for vocabulary translations.

exerciseTranslations.ts: All exercise UI strings in DE/EN/TR/AR/UK/RU/PL.
Buttons (Richtig/Falsch), instructions, labels, result texts.

Next: Wire into all 9 exercise pages for trilingual display.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-04-27 00:18:12 +02:00
parent b495e63e6f
commit bd3ca854ef
2 changed files with 88 additions and 0 deletions

View File

@@ -0,0 +1,50 @@
/**
* Exercise UI translations for all 7 supported languages.
* Used by useNativeLanguage() hook across all learning pages.
*/
export type ExerciseKey = keyof typeof exerciseT
export const exerciseT = {
// --- Buttons ---
correct: { de: 'Richtig', en: 'Correct', tr: 'Dogru', ar: '\u0635\u062d\u064a\u062d', uk: '\u041f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e', ru: '\u041f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e', pl: 'Dobrze' },
wrong: { de: 'Falsch', en: 'Wrong', tr: 'Yanlis', ar: '\u062e\u0637\u0623', uk: '\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e', ru: '\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e', pl: 'Zle' },
check: { de: 'Pruefen', en: 'Check', tr: 'Kontrol et', ar: '\u062a\u062d\u0642\u0642', uk: '\u041f\u0435\u0440\u0435\u0432\u0456\u0440\u0438\u0442\u0438', ru: '\u041f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c', pl: 'Sprawdz' },
next: { de: 'Weiter', en: 'Next', tr: 'Ileri', ar: '\u0627\u0644\u062a\u0627\u0644\u064a', uk: '\u0414\u0430\u043b\u0456', ru: '\u0414\u0430\u043b\u0435\u0435', pl: 'Dalej' },
back: { de: 'Zurueck', en: 'Back', tr: 'Geri', ar: '\u0631\u062c\u0648\u0639', uk: '\u041d\u0430\u0437\u0430\u0434', ru: '\u041d\u0430\u0437\u0430\u0434', pl: 'Wstecz' },
again: { de: 'Nochmal', en: 'Again', tr: 'Tekrar', ar: '\u0645\u0631\u0629 \u0623\u062e\u0631\u0649', uk: '\u0429\u0435 \u0440\u0430\u0437', ru: '\u0415\u0449\u0435 \u0440\u0430\u0437', pl: 'Jeszcze raz' },
done: { de: 'Geschafft!', en: 'Done!', tr: 'Bitti!', ar: '\u0627\u0646\u062a\u0647\u0649!', uk: '\u0413\u043e\u0442\u043e\u0432\u043e!', ru: '\u0413\u043e\u0442\u043e\u0432\u043e!', pl: 'Gotowe!' },
show_answer: { de: 'Antwort zeigen', en: 'Show answer', tr: 'Cevabi goster', ar: '\u0625\u0638\u0647\u0627\u0631 \u0627\u0644\u0625\u062c\u0627\u0628\u0629', uk: '\u041f\u043e\u043a\u0430\u0437\u0430\u0442\u0438', ru: '\u041f\u043e\u043a\u0430\u0437\u0430\u0442\u044c', pl: 'Pokaz odpowiedz' },
hide_answer: { de: 'Antwort verbergen', en: 'Hide answer', tr: 'Cevabi gizle', ar: '\u0625\u062e\u0641\u0627\u0621', uk: '\u0421\u0445\u043e\u0432\u0430\u0442\u0438', ru: '\u0421\u043a\u0440\u044b\u0442\u044c', pl: 'Ukryj odpowiedz' },
// --- Instructions ---
flip_card: { de: 'Klick zum Umdrehen', en: 'Click to flip', tr: 'Cevirmek icin tikla', ar: '\u0627\u0646\u0642\u0631 \u0644\u0644\u0642\u0644\u0628', uk: '\u041a\u043b\u0456\u043a \u0434\u043b\u044f \u043f\u0435\u0440\u0435\u0432\u0435\u0440\u0442\u0430\u043d\u043d\u044f', ru: '\u041d\u0430\u0436\u043c\u0438\u0442\u0435 \u0447\u0442\u043e\u0431\u044b \u043f\u0435\u0440\u0435\u0432\u0435\u0440\u043d\u0443\u0442\u044c', pl: 'Kliknij aby odwrocic' },
ask_child: { de: 'Fragen Sie Ihr Kind:', en: 'Ask your child:', tr: 'Cocugunuza sorun:', ar: '\u0627\u0633\u0623\u0644 \u0637\u0641\u0644\u0643:', uk: '\u0417\u0430\u043f\u0438\u0442\u0430\u0439\u0442\u0435 \u0434\u0438\u0442\u0438\u043d\u0443:', ru: '\u0421\u043f\u0440\u043e\u0441\u0438\u0442\u0435 \u0440\u0435\u0431\u0435\u043d\u043a\u0430:', pl: 'Zapytaj dziecko:' },
correct_answer: { de: 'Richtige Antwort:', en: 'Correct answer:', tr: 'Dogru cevap:', ar: '\u0627\u0644\u0625\u062c\u0627\u0628\u0629 \u0627\u0644\u0635\u062d\u064a\u062d\u0629:', uk: '\u041f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u0430 \u0432\u0456\u0434\u043f\u043e\u0432\u0456\u0434\u044c:', ru: '\u041f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u044b\u0439 \u043e\u0442\u0432\u0435\u0442:', pl: 'Poprawna odpowiedz:' },
translate: { de: 'Uebersetze:', en: 'Translate:', tr: 'Cevir:', ar: '\u062a\u0631\u062c\u0645:', uk: '\u041f\u0435\u0440\u0435\u043a\u043b\u0430\u0434\u0438:', ru: '\u041f\u0435\u0440\u0435\u0432\u0435\u0434\u0438\u0442\u0435:', pl: 'Przetlumacz:' },
type_answer: { de: 'Antwort eintippen...', en: 'Type your answer...', tr: 'Cevabin yaz...', ar: '\u0627\u0643\u062a\u0628 \u0625\u062c\u0627\u0628\u062a\u0643...', uk: '\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u0432\u0456\u0434\u043f\u043e\u0432\u0456\u0434\u044c...', ru: '\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043e\u0442\u0432\u0435\u0442...', pl: 'Wpisz odpowiedz...' },
listen_instruction: { de: 'Hoere das Wort und waehle die Uebersetzung', en: 'Listen and choose the translation', tr: 'Kelimeyi dinle ve ceviriyi sec', ar: '\u0627\u0633\u062a\u0645\u0639 \u0648\u0627\u062e\u062a\u0631 \u0627\u0644\u062a\u0631\u062c\u0645\u0629', uk: '\u041f\u043e\u0441\u043b\u0443\u0445\u0430\u0439 \u0456 \u0432\u0438\u0431\u0435\u0440\u0438 \u043f\u0435\u0440\u0435\u043a\u043b\u0430\u0434', ru: '\u041f\u043e\u0441\u043b\u0443\u0448\u0430\u0439\u0442\u0435 \u0438 \u0432\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043f\u0435\u0440\u0435\u0432\u043e\u0434', pl: 'Posluchaj i wybierz tlumaczenie' },
pronounce_instruction: { de: 'Hoere zu, dann sprich nach', en: 'Listen, then repeat', tr: 'Dinle, sonra tekrar et', ar: '\u0627\u0633\u062a\u0645\u0639 \u062b\u0645 \u0643\u0631\u0631', uk: '\u041f\u043e\u0441\u043b\u0443\u0445\u0430\u0439, \u043f\u043e\u0442\u0456\u043c \u043f\u043e\u0432\u0442\u043e\u0440\u0438', ru: '\u041f\u043e\u0441\u043b\u0443\u0448\u0430\u0439\u0442\u0435, \u0437\u0430\u0442\u0435\u043c \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435', pl: 'Posluchaj, potem powtorz' },
match_instruction: { de: 'Verbinde die Paare', en: 'Match the pairs', tr: 'Esleri eslestir', ar: '\u0637\u0627\u0628\u0642 \u0627\u0644\u0623\u0632\u0648\u0627\u062c', uk: '\u0417\u2019\u0454\u0434\u043d\u0430\u0439 \u043f\u0430\u0440\u0438', ru: '\u0421\u043e\u0435\u0434\u0438\u043d\u0438\u0442\u0435 \u043f\u0430\u0440\u044b', pl: 'Polacz pary' },
tap_speaker: { de: 'Tippe auf den Lautsprecher', en: 'Tap the speaker', tr: 'Hoparlore dokun', ar: '\u0627\u0646\u0642\u0631 \u0639\u0644\u0649 \u0627\u0644\u0645\u0643\u0628\u0631', uk: '\u041d\u0430\u0442\u0438\u0441\u043d\u0456\u0442\u044c \u043d\u0430 \u0434\u0438\u043d\u0430\u043c\u0456\u043a', ru: '\u041d\u0430\u0436\u043c\u0438\u0442\u0435 \u043d\u0430 \u0434\u0438\u043d\u0430\u043c\u0438\u043a', pl: 'Dotknij glosnika' },
// --- Results ---
almost_right: { de: 'Fast richtig!', en: 'Almost right!', tr: 'Neredeyse dogru!', ar: '\u062a\u0642\u0631\u064a\u0628\u0627 \u0635\u062d\u064a\u062d!', uk: '\u041c\u0430\u0439\u0436\u0435 \u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e!', ru: '\u041f\u043e\u0447\u0442\u0438 \u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e!', pl: 'Prawie dobrze!' },
well_done: { de: 'Super gemacht!', en: 'Well done!', tr: 'Harika!', ar: '\u0623\u062d\u0633\u0646\u062a!', uk: '\u0427\u0443\u0434\u043e\u0432\u043e!', ru: '\u041c\u043e\u043b\u043e\u0434\u0435\u0446!', pl: 'Swietnie!' },
correct_spoken: { de: 'Richtig ausgesprochen!', en: 'Correct pronunciation!', tr: 'Dogru telaffuz!', ar: '\u0646\u0637\u0642 \u0635\u062d\u064a\u062d!', uk: '\u041f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u0430 \u0432\u0438\u043c\u043e\u0432\u0430!', ru: '\u041f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e\u0435 \u043f\u0440\u043e\u0438\u0437\u043d\u043e\u0448\u0435\u043d\u0438\u0435!', pl: 'Poprawna wymowa!' },
all_matched: { de: 'Alle zugeordnet!', en: 'All matched!', tr: 'Hepsi eslesti!', ar: '\u062a\u0645 \u0627\u0644\u0645\u0637\u0627\u0628\u0642\u0629!', uk: '\u0412\u0441\u0435 \u0437\u2019\u0454\u0434\u043d\u0430\u043d\u043e!', ru: '\u0412\u0441\u0435 \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u043e!', pl: 'Wszystko dopasowane!' },
errors: { de: 'Fehler', en: 'Errors', tr: 'Hata', ar: '\u0623\u062e\u0637\u0627\u0621', uk: '\u041f\u043e\u043c\u0438\u043b\u043a\u0438', ru: '\u041e\u0448\u0438\u0431\u043a\u0438', pl: 'Bledy' },
// --- Labels ---
english: { de: 'Englisch', en: 'English', tr: 'Ingilizce', ar: '\u0627\u0644\u0625\u0646\u062c\u0644\u064a\u0632\u064a\u0629', uk: '\u0410\u043d\u0433\u043b\u0456\u0439\u0441\u044c\u043a\u0430', ru: '\u0410\u043d\u0433\u043b\u0438\u0439\u0441\u043a\u0438\u0439', pl: 'Angielski' },
german: { de: 'Deutsch', en: 'German', tr: 'Almanca', ar: '\u0627\u0644\u0623\u0644\u0645\u0627\u0646\u064a\u0629', uk: '\u041d\u0456\u043c\u0435\u0446\u044c\u043a\u0430', ru: '\u041d\u0435\u043c\u0435\u0446\u043a\u0438\u0439', pl: 'Niemiecki' },
question: { de: 'Frage', en: 'Question', tr: 'Soru', ar: '\u0633\u0624\u0627\u0644', uk: '\u041f\u0438\u0442\u0430\u043d\u043d\u044f', ru: '\u0412\u043e\u043f\u0440\u043e\u0441', pl: 'Pytanie' },
cards: { de: 'Karten', en: 'Cards', tr: 'Kartlar', ar: '\u0628\u0637\u0627\u0642\u0627\u062a', uk: '\u041a\u0430\u0440\u0442\u043a\u0438', ru: '\u041a\u0430\u0440\u0442\u043e\u0447\u043a\u0438', pl: 'Karty' },
flashcards: { de: 'Karteikarten', en: 'Flashcards', tr: 'Kartlar', ar: '\u0628\u0637\u0627\u0642\u0627\u062a', uk: '\u041a\u0430\u0440\u0442\u043a\u0438', ru: '\u041a\u0430\u0440\u0442\u043e\u0447\u043a\u0438', pl: 'Fiszki' },
quiz: { de: 'Quiz', en: 'Quiz', tr: 'Quiz', ar: '\u0627\u062e\u062a\u0628\u0627\u0631', uk: '\u0422\u0435\u0441\u0442', ru: '\u0422\u0435\u0441\u0442', pl: 'Quiz' },
type_exercise: { de: 'Eintippen', en: 'Type', tr: 'Yaz', ar: '\u0627\u0643\u062a\u0628', uk: '\u0412\u0432\u0435\u0434\u0438', ru: '\u041d\u0430\u043f\u0438\u0448\u0438', pl: 'Wpisz' },
listen: { de: 'Hoeren', en: 'Listen', tr: 'Dinle', ar: '\u0627\u0633\u062a\u0645\u0639', uk: '\u0421\u043b\u0443\u0445\u0430\u0439', ru: '\u0421\u043b\u0443\u0448\u0430\u0442\u044c', pl: 'Sluchaj' },
match: { de: 'Zuordnen', en: 'Match', tr: 'Eslestir', ar: '\u0637\u0627\u0628\u0642', uk: '\u0417\u2019\u0454\u0434\u043d\u0430\u0439', ru: '\u0421\u043e\u0435\u0434\u0438\u043d\u0438', pl: 'Dopasuj' },
pronounce: { de: 'Sprechen', en: 'Speak', tr: 'Konus', ar: '\u062a\u062d\u062f\u062b', uk: '\u0413\u043e\u0432\u043e\u0440\u0438', ru: '\u0413\u043e\u0432\u043e\u0440\u0438', pl: 'Mow' },
story: { de: 'Geschichte', en: 'Story', tr: 'Hikaye', ar: '\u0642\u0635\u0629', uk: '\u0406\u0441\u0442\u043e\u0440\u0456\u044f', ru: '\u0418\u0441\u0442\u043e\u0440\u0438\u044f', pl: 'Historia' },
} as const

View File

@@ -0,0 +1,38 @@
'use client'
import { useState, useEffect } from 'react'
import { exerciseT, type ExerciseKey } from './exerciseTranslations'
const STORAGE_KEY = 'bp_native_language'
/**
* Hook to read the user's native language.
* Returns translation helper for exercise UI texts.
*/
export function useNativeLanguage() {
const [nativeLang, setNativeLang] = useState('de')
useEffect(() => {
const stored = localStorage.getItem(STORAGE_KEY)
if (stored) setNativeLang(stored)
}, [])
const isThirdLanguage = nativeLang !== 'de' && nativeLang !== 'en'
/** Get translated exercise UI text */
const t = (key: ExerciseKey): string => {
const entry = exerciseT[key]
if (!entry) return key
return (entry as Record<string, string>)[nativeLang] || entry.de || key
}
/** Get native translation of a vocab word from translations JSONB */
const wordInNative = (translations?: Record<string, any>): string => {
if (!translations || !isThirdLanguage) return ''
const entry = translations[nativeLang]
if (!entry) return ''
return typeof entry === 'string' ? entry : entry.text || ''
}
return { nativeLang, isThirdLanguage, t, wordInNative }
}