tts-voice-creator-clone-and.../static/js/i18n.js
mARTin-B78 a4128788d6 feat: slim icon-rail sidebar, collapsible panels, Read Aloud fit-height (v1.12.0)
- Sidebar collapses to a 56px icon rail; hovering flies the full menu out as
  an overlay (icons + titles/nested items). Language picker moved to Settings.
- Unify settings collapsibles to the app's standard card-collapse style:
  Conversation, Read Aloud (drag-&-drop now inside), Try It Out, Casting panel.
- Read Aloud: reordered (settings → toolbar → document → transport/synth) and
  the document fits the viewport height so controls below stay visible; remove
  the redundant My Books card (lives in Library → Books).
- Conversation: stacked full-width config, fills viewport height; fix
  intermittent webm decode in hands-free mode (recorder restarts cleanly,
  in-browser WAV encode); barge-in via Live agent.
- Fix casting feed overflow that pushed the sidebar off-screen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 15:47:52 +02:00

141 lines
8.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ── 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 320 seconds of clean speech, trim it, name it, then save it as a reusable voice clone.':
'Nimm 320 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();
};
// ── Language picker — lives in Settings → General ────────────────────────────
// Populate + sync the <select id="s-lang-select">. The Settings fragment loads
// lazily, so this is idempotent and safe to call whenever Settings opens.
window.populateLangSelect = function () {
const sel = document.getElementById('s-lang-select');
if (!sel) return;
if (!sel.dataset.filled) {
sel.innerHTML = Object.keys(I18N_LANGS)
.map(c => `<option value="${c}">${I18N_LANGS[c]}</option>`).join('');
sel.dataset.filled = '1';
}
sel.value = _appLang;
};
// Back-compat: loader.js calls initLangPicker() once at startup. The picker now
// lives in Settings → General, so this just tries to populate it.
window.initLangPicker = function () { window.populateLangSelect(); };