From bd3ca854ef6d463edebc471b4945183c8a6a652c Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Mon, 27 Apr 2026 00:18:12 +0200 Subject: [PATCH] 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) --- studio-v2/lib/exerciseTranslations.ts | 50 +++++++++++++++++++++++++++ studio-v2/lib/useNativeLanguage.ts | 38 ++++++++++++++++++++ 2 files changed, 88 insertions(+) create mode 100644 studio-v2/lib/exerciseTranslations.ts create mode 100644 studio-v2/lib/useNativeLanguage.ts diff --git a/studio-v2/lib/exerciseTranslations.ts b/studio-v2/lib/exerciseTranslations.ts new file mode 100644 index 0000000..5aad8b5 --- /dev/null +++ b/studio-v2/lib/exerciseTranslations.ts @@ -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 diff --git a/studio-v2/lib/useNativeLanguage.ts b/studio-v2/lib/useNativeLanguage.ts new file mode 100644 index 0000000..c2efe7c --- /dev/null +++ b/studio-v2/lib/useNativeLanguage.ts @@ -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)[nativeLang] || entry.de || key + } + + /** Get native translation of a vocab word from translations JSONB */ + const wordInNative = (translations?: Record): string => { + if (!translations || !isThirdLanguage) return '' + const entry = translations[nativeLang] + if (!entry) return '' + return typeof entry === 'string' ? entry : entry.text || '' + } + + return { nativeLang, isThirdLanguage, t, wordInNative } +}