tts-voice-creator-clone-and.../static/js/i18n.js
mARTin-B78 40e42590cc Release v1.6.0: a11y (WCAG AA), i18n (DE), PWA, perf, tests, Cast UX
Cast: card/list views, sort & filter, online voice picker, "Hear a line"
sample button, AI character notes, import auto-save.

Platform: WCAG 2.1 AA accessibility pass; German UI translation + language
picker; installable PWA with offline shell; GZip + content-visibility
virtualization + lazy images + Rehearser PCM memory cap (mobile stability);
Playwright suite (desktop + iPhone); opt-in minified bundle build.

Fixes: screenplay parser false characters; Fish-Speech inline-tag tones;
narrator/voice pickers list full library; clone GUI rework; fish.audio
import dedup; voice-ID rename; bulk-delete modal.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 14:23:35 +02:00

127 lines
7.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',
'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.',
// 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();
};
// Inject the picker into the sidebar brand once the DOM is ready.
window.initLangPicker = function () {
const brand = document.querySelector('.sidebar-brand');
if (!brand || document.getElementById('app-lang-picker')) return;
const sel = document.createElement('select');
sel.id = 'app-lang-picker';
sel.className = 'app-lang-picker';
sel.setAttribute('aria-label', 'Interface language');
sel.innerHTML = Object.entries(I18N_LANGS)
.map(([code, name]) => `<option value="${code}"${code === _appLang ? ' selected' : ''}>${name}</option>`).join('');
sel.addEventListener('change', () => window.setAppLang(sel.value));
brand.appendChild(sel);
};