// ── Lightweight i18n ───────────────────────────────────────────────────────── // English is the source language (keys = the exact English UI text). Switching // language persists the choice and reloads, so translations are applied once to the // freshly-rendered English DOM (no fragile reverse-translation / re-render tracking). // // Coverage v1: the static UI chrome (nav, section titles/subtitles, common // buttons/labels). Add more by adding "English": "translation" pairs below, or call // window.t('English') for JS-generated strings. Dynamic lists can be translated by // calling window.applyI18n(container) after rendering. const I18N_LANGS = { en: 'English', de: 'Deutsch' }; const I18N_DICT = { de: { // Brand / sidebar groups 'Voice Creator': 'Voice Creator', 'Clone · Design · Deploy': 'Klonen · Entwerfen · Bereitstellen', 'Voices': 'Stimmen', 'Setup': 'Einrichtung', 'Tags': 'Tags', // Sidebar nav 'My Voices': 'Meine Stimmen', 'All voices': 'Alle Stimmen', 'Cloned': 'Geklont', 'Designed': 'Entworfen', 'Favorites': 'Favoriten', 'Hidden': 'Ausgeblendet', 'Library tools': 'Bibliothekswerkzeuge', 'Clone a Voice': 'Stimme klonen', 'Design a Voice': 'Stimme entwerfen', 'Get Voices Online': 'Stimmen online holen', 'Try It Out': 'Ausprobieren', 'Read Aloud': 'Vorlesen', 'Script Rehearser': 'Skript-Probe', 'Library': 'Bibliothek', 'Cast': 'Besetzung', 'Stage': 'Bühne', 'Summary': 'Zusammenfassung', 'Import / Export': 'Import / Export', 'Conversation': 'Unterhaltung', 'Benchmark': 'Benchmark', 'Engines': 'Engines', 'Language Models': 'Sprachmodelle', 'Speech to Text': 'Sprache-zu-Text', 'Text to Speech': 'Text-zu-Sprache', 'App Routing': 'App-Routing', 'Connect Apps': 'Apps verbinden', 'Settings': 'Einstellungen', // Section titles 'Conversation Playground': 'Unterhaltungs-Spielplatz', // Section subtitles 'Pick a voice on the left, edit on the right.': 'Wähle links eine Stimme, bearbeite sie rechts.', 'Capture 3–20 seconds of clean speech, trim it, name it, then save it as a reusable voice clone.': 'Nimm 3–20 Sekunden saubere Sprache auf, schneide sie zu, benenne sie und speichere sie als wiederverwendbaren Stimmklon.', 'Describe a voice in words and let the AI create it. No recording needed.': 'Beschreibe eine Stimme in Worten und lass sie von der KI erstellen. Keine Aufnahme nötig.', 'Browse public voice clip sources, preview direct audio files, and import voices from the web.': 'Durchsuche öffentliche Stimm-Quellen, höre Audiodateien vorab an und importiere Stimmen aus dem Web.', 'Generate speech from text using any backend and voice. Also transcribe audio and re-speak it.': 'Erzeuge Sprache aus Text mit beliebigem Backend und Stimme. Transkribiere Audio und sprich es neu ein.', 'Upload a script, cast characters to TTS voices or your own mic, then rehearse scene by scene.': 'Lade ein Skript hoch, besetze Figuren mit TTS-Stimmen oder deinem Mikrofon und probe Szene für Szene.', 'Import a PDF or text document, pick a voice and speed, then have it read to you while the word being spoken is highlighted.': 'Importiere ein PDF- oder Textdokument, wähle Stimme und Tempo und lass es dir vorlesen, während das gerade gesprochene Wort hervorgehoben wird.', // Read Aloud UI 'My books': 'Meine Bücher', 'Voice consistency': 'Stimmkonstanz', 'Normalise loudness': 'Lautstärke angleichen', 'Export MP3': 'MP3 exportieren', 'Select range': 'Bereich wählen', 'Synthesise': 'Synthetisieren', 'Tone / style': 'Ton / Stil', 'Temperature': 'Temperatur', 'Speed': 'Tempo', 'Saved documents with their synthesised audio — reopen to continue.': 'Gespeicherte Dokumente mit ihrem synthetisierten Audio — zum Fortsetzen erneut öffnen.', // Common buttons / labels 'Save changes': 'Änderungen speichern', 'Save to library': 'In Bibliothek speichern', 'Save to Voice Library': 'In Stimmbibliothek speichern', 'Delete voice': 'Stimme löschen', 'Cancel': 'Abbrechen', 'Refresh': 'Aktualisieren', 'Delete': 'Löschen', 'Save': 'Speichern', 'Back': 'Zurück', 'Apply →': 'Anwenden →', 'Browse': 'Durchsuchen', 'Download': 'Herunterladen', 'Record': 'Aufnehmen', 'Stop': 'Stopp', 'Play': 'Abspielen', 'Play selection': 'Auswahl abspielen', 'Check level': 'Pegel prüfen', 'Stop monitor': 'Monitor stoppen', 'Auto trim': 'Auto-Zuschnitt', 'Auto-transcribe': 'Auto-Transkription', 'Active': 'Aktiv', 'copy ID': 'ID kopieren', 'edit ID': 'ID bearbeiten', 'New voice': 'Neue Stimme', 'All languages': 'Alle Sprachen', 'All genders': 'Alle Geschlechter', 'Microphone': 'Mikrofon', 'Upload file': 'Datei hochladen', 'Sort': 'Sortieren', 'Cards': 'Karten', 'List': 'Liste', 'Develop': 'Ausarbeiten', 'Match local': 'Lokal zuordnen', 'Match online': 'Online zuordnen', 'Design all': 'Alle entwerfen', 'I play this': 'Ich spiele das', 'Save to library': 'In Bibliothek speichern', 'Open rehearsal': 'Probe öffnen', 'Fetch voices': 'Stimmen abrufen', 'Name your voice': 'Benenne deine Stimme', 'Reference transcript': 'Referenz-Transkript', 'Preview': 'Vorschau', 'Trim your sample': 'Probe zuschneiden', 'Language': 'Sprache', 'Gender': 'Geschlecht', 'Voice': 'Stimme', 'Tags': 'Tags', 'Speaking style · voice-design prompt': 'Sprechstil · Voice-Design-Prompt', // Common placeholders 'Search voices…': 'Stimmen suchen…', 'Search voices...': 'Stimmen suchen...', 'Filter name or tag…': 'Name oder Tag filtern…', }, }; let _appLang = 'en'; try { _appLang = localStorage.getItem('app-lang') || 'en'; } catch (_) {} window.__APP_LANG__ = _appLang; // Translate a single source string (for JS-generated UI). window.t = function (s) { if (_appLang === 'en' || s == null) return s; const d = I18N_DICT[_appLang]; return (d && d[String(s).trim()] != null) ? d[String(s).trim()] : s; }; const _I18N_SKIP = new Set(['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'CODE', 'PRE', 'OPTION']); // Translate visible text nodes + common attributes within `root` (default: whole doc). window.applyI18n = function (root) { if (_appLang === 'en') return; const dict = I18N_DICT[_appLang]; if (!dict) return; root = root || document.body; if (!root) return; const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, { acceptNode(n) { if (!n.nodeValue || !n.nodeValue.trim()) return NodeFilter.FILTER_REJECT; const p = n.parentNode; if (p && _I18N_SKIP.has(p.nodeName)) return NodeFilter.FILTER_REJECT; return NodeFilter.FILTER_ACCEPT; }, }); const nodes = []; while (walker.nextNode()) nodes.push(walker.currentNode); for (const n of nodes) { const key = n.nodeValue.trim(); if (dict[key] != null) n.nodeValue = n.nodeValue.replace(key, dict[key]); } root.querySelectorAll('[placeholder],[title],[aria-label]').forEach(el => { ['placeholder', 'title', 'aria-label'].forEach(a => { const v = el.getAttribute(a); if (v && dict[v.trim()] != null) el.setAttribute(a, dict[v.trim()]); }); }); }; // Language change → persist + reload (translations re-apply to fresh English DOM). window.setAppLang = function (lang) { if (!I18N_LANGS[lang]) return; try { localStorage.setItem('app-lang', lang); } catch (_) {} location.reload(); }; // Flag of a language code (flag-icons classes; English → GB). function _langFlag(code) { return code === 'en' ? 'gb' : code; } // Inject a flag toggle next to the "Voice Creator" headline. Clicking cycles to // the next language (a simple toggle while there are two). window.initLangPicker = function () { const brand = document.querySelector('.sidebar-brand'); const head = brand && brand.querySelector('h1'); if (!head || document.getElementById('app-lang-toggle')) return; const codes = Object.keys(I18N_LANGS); const btn = document.createElement('button'); btn.id = 'app-lang-toggle'; btn.className = 'app-lang-toggle'; btn.type = 'button'; const nextOf = () => codes[(codes.indexOf(_appLang) + 1) % codes.length]; const render = () => { btn.innerHTML = ``; const label = 'Language: ' + I18N_LANGS[_appLang] + ' — switch to ' + I18N_LANGS[nextOf()]; btn.title = label; btn.setAttribute('aria-label', label); }; render(); btn.addEventListener('click', () => window.setAppLang(nextOf())); head.appendChild(btn); };