centralized language catalog

This commit is contained in:
ValueOn AG 2026-04-19 00:36:42 +02:00
parent d814a76660
commit 13f4574098
9 changed files with 277 additions and 157 deletions

View file

@ -31,6 +31,7 @@ import { LanguageProvider } from './providers/language/LanguageContext';
import { ToastProvider } from './contexts/ToastContext'; import { ToastProvider } from './contexts/ToastContext';
import { WorkflowSelectionProvider } from './contexts/WorkflowSelectionContext'; import { WorkflowSelectionProvider } from './contexts/WorkflowSelectionContext';
import { FileProvider } from './contexts/FileContext'; import { FileProvider } from './contexts/FileContext';
import { VoiceCatalogProvider } from './contexts/VoiceCatalogContext';
import { MainLayout } from './layouts/MainLayout'; import { MainLayout } from './layouts/MainLayout';
import { FeatureLayout } from './layouts/FeatureLayout'; import { FeatureLayout } from './layouts/FeatureLayout';
import { DashboardPage } from './pages/Dashboard'; import { DashboardPage } from './pages/Dashboard';
@ -72,6 +73,7 @@ function App() {
<LanguageProvider> <LanguageProvider>
<AuthProvider> <AuthProvider>
<ToastProvider> <ToastProvider>
<VoiceCatalogProvider>
<WorkflowSelectionProvider> <WorkflowSelectionProvider>
<Router> <Router>
<Routes> <Routes>
@ -231,6 +233,7 @@ function App() {
</Routes> </Routes>
</Router> </Router>
</WorkflowSelectionProvider> </WorkflowSelectionProvider>
</VoiceCatalogProvider>
</ToastProvider> </ToastProvider>
</AuthProvider> </AuthProvider>
</LanguageProvider> </LanguageProvider>

View file

@ -102,18 +102,10 @@ export interface ConfigUpdateRequest {
debugMode?: boolean; debugMode?: boolean;
} }
// Voice/Language Types (from Google TTS API) // Voice option type re-exported from the central voice catalog API.
export interface VoiceLanguage { // The legacy teamsbot-specific {code,name} language type is gone — consumers
code: string; // should use VoiceLanguage from voiceCatalogApi (catalog SSOT).
name: string; export type { VoiceOption } from './voiceCatalogApi';
}
export interface VoiceOption {
name: string;
languageCodes: string[];
ssmlGender: string;
naturalSampleRateHertz: number;
}
// Auth Detection Test Types // Auth Detection Test Types
export interface StepScreenshot { export interface StepScreenshot {
@ -313,25 +305,19 @@ export async function testVoice(
} }
/** /**
* Fetch available TTS languages from Google Cloud. * Fetch the curated voice/language catalog (single source of truth).
* Returns array of language codes (e.g. ["de-DE", "en-US", ...]) * Re-exports the central voiceCatalogApi.fetchVoiceCatalog so legacy
* teamsbot consumers stay on one import surface.
*/ */
export async function fetchLanguages(): Promise<string[]> { export { fetchVoiceCatalog as fetchLanguages } from './voiceCatalogApi';
try {
const response = await api.get('/voice-google/languages');
return response.data?.languages || [];
} catch {
return [];
}
}
/** /**
* Fetch available TTS voices for a language from Google Cloud. * Fetch available TTS voices for a language from Google Cloud.
*/ */
export async function fetchVoices(languageCode: string): Promise<VoiceOption[]> { export async function fetchVoices(languageCode: string): Promise<VoiceOption[]> {
try { try {
const response = await api.get('/voice-google/voices', { const response = await api.get('/api/voice/voices', {
params: { languageCode }, params: { language: languageCode },
}); });
return response.data?.voices || []; return response.data?.voices || [];
} catch { } catch {

View file

@ -0,0 +1,47 @@
/**
* Voice / Language Catalog API.
*
* Single source of truth for every voice-language picker, default-voice
* lookup, and ISO BCP-47 mapping in the frontend. Mirrors
* gateway/modules/shared/voiceCatalog.py 1:1.
*
* Hard-coded language lists or ad-hoc maps in components are forbidden
* consume `useVoiceCatalog()` instead.
*/
import api from '../api';
export interface VoiceLanguage {
bcp47: string;
iso: string;
label: string;
flag: string;
defaultVoice: string | null;
}
export interface VoiceOption {
name: string;
languageCodes: string[];
ssmlGender: string;
naturalSampleRateHertz: number;
}
interface CatalogResponse {
languages: VoiceLanguage[];
}
interface VoicesResponse {
voices: VoiceOption[];
}
export async function fetchVoiceCatalog(): Promise<VoiceLanguage[]> {
const response = await api.get<CatalogResponse>('/api/voice/languages');
return response.data?.languages ?? [];
}
export async function fetchVoicesForLanguage(bcp47: string): Promise<VoiceOption[]> {
const response = await api.get<VoicesResponse>('/api/voice/voices', {
params: { language: bcp47 },
});
return response.data?.voices ?? [];
}

View file

@ -1,67 +1,29 @@
/** /**
* VoiceLanguageSelect * VoiceLanguageSelect
* *
* Reusable component for selecting voice/speech recognition language. * Reusable picker for voice/speech-recognition language. Reads the language
* Defaults to user's profile language. * list from the central VoiceCatalog (single source of truth) never
* Can be used for speech-to-text, text-to-speech, and translation features. * hard-coded here.
*/ */
import React from 'react'; import React from 'react';
import { useLanguage } from '../../../providers/language/LanguageContext'; import { useLanguage } from '../../../providers/language/LanguageContext';
import { useVoiceCatalog, useDefaultVoiceLocale } from '../../../contexts/VoiceCatalogContext';
import type { VoiceLanguage } from '../../../api/voiceCatalogApi';
import styles from './VoiceLanguageSelect.module.css'; import styles from './VoiceLanguageSelect.module.css';
// Voice language options with full locale codes for Google Cloud Speech export type VoiceLanguageOption = VoiceLanguage;
export interface VoiceLanguageOption {
code: string; // Full locale code (e.g., 'de-DE')
label: string; // Display label
shortCode: string; // Short code for mapping (e.g., 'de')
flag?: string; // Optional flag emoji
}
// Supported languages for speech recognition
export const voiceLanguages: VoiceLanguageOption[] = [
{ code: 'de-DE', label: 'Deutsch', shortCode: 'de', flag: '🇩🇪' },
{ code: 'de-CH', label: 'Deutsch (Schweiz)', shortCode: 'de', flag: '🇨🇭' },
{ code: 'en-US', label: 'English (US)', shortCode: 'en', flag: '🇺🇸' },
{ code: 'en-GB', label: 'English (UK)', shortCode: 'en', flag: '🇬🇧' },
{ code: 'fr-FR', label: 'Français', shortCode: 'fr', flag: '🇫🇷' },
{ code: 'fr-CH', label: 'Français (Suisse)', shortCode: 'fr', flag: '🇨🇭' },
{ code: 'it-IT', label: 'Italiano', shortCode: 'it', flag: '🇮🇹' },
{ code: 'it-CH', label: 'Italiano (Svizzera)', shortCode: 'it', flag: '🇨🇭' },
{ code: 'es-ES', label: 'Español', shortCode: 'es', flag: '🇪🇸' },
{ code: 'pt-BR', label: 'Português', shortCode: 'pt', flag: '🇧🇷' },
];
// Map user profile language (short code) to default voice language (full code)
const profileToVoiceLanguage: Record<string, string> = {
'de': 'de-DE',
'en': 'en-US',
'fr': 'fr-FR',
'it': 'it-IT',
'es': 'es-ES',
'pt': 'pt-BR',
};
export interface VoiceLanguageSelectProps { export interface VoiceLanguageSelectProps {
value: string; value: string;
onChange: (languageCode: string) => void; onChange: (languageCode: string) => void;
disabled?: boolean; disabled?: boolean;
compact?: boolean; // Compact mode shows only flag/short code compact?: boolean;
showFlags?: boolean; // Show flag emojis showFlags?: boolean;
className?: string; className?: string;
title?: string; title?: string;
} }
/**
* Get the default voice language based on user's profile language
*/
export const getDefaultVoiceLanguage = (profileLanguage?: string): string => {
if (profileLanguage && profileToVoiceLanguage[profileLanguage]) {
return profileToVoiceLanguage[profileLanguage];
}
return 'de-DE'; // Default fallback
};
export const VoiceLanguageSelect: React.FC<VoiceLanguageSelectProps> = ({ export const VoiceLanguageSelect: React.FC<VoiceLanguageSelectProps> = ({
value, value,
onChange, onChange,
@ -71,6 +33,8 @@ export const VoiceLanguageSelect: React.FC<VoiceLanguageSelectProps> = ({
className = '', className = '',
title = 'Sprache für Spracherkennung', title = 'Sprache für Spracherkennung',
}) => { }) => {
const { languages, isLoading } = useVoiceCatalog();
const handleChange = (e: React.ChangeEvent<HTMLSelectElement>) => { const handleChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
onChange(e.target.value); onChange(e.target.value);
}; };
@ -81,13 +45,13 @@ export const VoiceLanguageSelect: React.FC<VoiceLanguageSelectProps> = ({
className={styles.select} className={styles.select}
value={value} value={value}
onChange={handleChange} onChange={handleChange}
disabled={disabled} disabled={disabled || isLoading}
title={title} title={title}
> >
{voiceLanguages.map((lang) => ( {languages.map((lang) => (
<option key={lang.code} value={lang.code}> <option key={lang.bcp47} value={lang.bcp47}>
{showFlags && lang.flag ? `${lang.flag} ` : ''} {showFlags && lang.flag ? `${lang.flag} ` : ''}
{compact ? lang.code.split('-')[0].toUpperCase() : lang.label} {compact ? lang.iso.toUpperCase() : lang.label}
</option> </option>
))} ))}
</select> </select>
@ -96,28 +60,25 @@ export const VoiceLanguageSelect: React.FC<VoiceLanguageSelectProps> = ({
}; };
/** /**
* Hook to manage voice language state with user profile default * Hook to manage voice language state with user profile default.
* Initial value falls back to the catalog-derived default for the profile language.
*/ */
export const useVoiceLanguage = (initialValue?: string) => { export const useVoiceLanguage = (initialValue?: string) => {
const { currentLanguage } = useLanguage(); const { currentLanguage } = useLanguage();
const { languages } = useVoiceCatalog();
const defaultLocale = useDefaultVoiceLocale(currentLanguage);
// Track if user has manually changed the language
const hasManuallyChanged = React.useRef(false); const hasManuallyChanged = React.useRef(false);
// Initialize with user's profile language (or provided initial value)
const [voiceLanguage, setVoiceLanguage] = React.useState<string>( const [voiceLanguage, setVoiceLanguage] = React.useState<string>(
initialValue || getDefaultVoiceLanguage(currentLanguage) initialValue || defaultLocale,
); );
// Update voice language when user profile language changes (only if not manually set)
React.useEffect(() => { React.useEffect(() => {
if (!initialValue && !hasManuallyChanged.current) { if (!initialValue && !hasManuallyChanged.current) {
const newDefault = getDefaultVoiceLanguage(currentLanguage); setVoiceLanguage(defaultLocale);
setVoiceLanguage(newDefault);
} }
}, [currentLanguage, initialValue]); }, [defaultLocale, initialValue]);
// Wrapper to track manual changes
const handleSetVoiceLanguage = React.useCallback((newLanguage: string) => { const handleSetVoiceLanguage = React.useCallback((newLanguage: string) => {
hasManuallyChanged.current = true; hasManuallyChanged.current = true;
setVoiceLanguage(newLanguage); setVoiceLanguage(newLanguage);
@ -126,7 +87,7 @@ export const useVoiceLanguage = (initialValue?: string) => {
return { return {
voiceLanguage, voiceLanguage,
setVoiceLanguage: handleSetVoiceLanguage, setVoiceLanguage: handleSetVoiceLanguage,
voiceLanguages, voiceLanguages: languages,
}; };
}; };

View file

@ -1,8 +1,6 @@
export { export {
VoiceLanguageSelect, VoiceLanguageSelect,
useVoiceLanguage, useVoiceLanguage,
getDefaultVoiceLanguage,
voiceLanguages,
type VoiceLanguageOption, type VoiceLanguageOption,
type VoiceLanguageSelectProps type VoiceLanguageSelectProps,
} from './VoiceLanguageSelect'; } from './VoiceLanguageSelect';

View file

@ -0,0 +1,151 @@
/**
* VoiceCatalogContext
*
* Loads the central voice/language catalog from the backend exactly once and
* makes it available to every component via `useVoiceCatalog()`.
*
* Provides convenience helpers for ISO BCP-47 lookups and curated default
* voices, mirroring the backend `voiceCatalog` API. Components MUST NOT keep
* their own static language lists.
*/
import React, {
createContext,
ReactNode,
useCallback,
useContext,
useEffect,
useMemo,
useState,
} from 'react';
import { fetchVoiceCatalog, VoiceLanguage } from '../api/voiceCatalogApi';
interface VoiceCatalogContextType {
languages: VoiceLanguage[];
isLoading: boolean;
error: string | null;
getByBcp47: (code: string | null | undefined) => VoiceLanguage | undefined;
getByIso: (iso: string | null | undefined) => VoiceLanguage | undefined;
isoToBcp47: (iso: string | null | undefined) => string | undefined;
getDefaultVoice: (bcp47: string | null | undefined) => string | null;
}
const VoiceCatalogContext = createContext<VoiceCatalogContextType | undefined>(undefined);
interface VoiceCatalogProviderProps {
children: ReactNode;
}
export const VoiceCatalogProvider: React.FC<VoiceCatalogProviderProps> = ({ children }) => {
const [languages, setLanguages] = useState<VoiceLanguage[]>([]);
const [isLoading, setIsLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
let cancelled = false;
(async () => {
try {
const data = await fetchVoiceCatalog();
if (!cancelled) {
setLanguages(data);
setError(null);
}
} catch (err: any) {
if (!cancelled) {
setError(err?.message || 'Failed to load voice catalog');
setLanguages([]);
}
} finally {
if (!cancelled) setIsLoading(false);
}
})();
return () => {
cancelled = true;
};
}, []);
const byBcp47 = useMemo(() => {
const map = new Map<string, VoiceLanguage>();
for (const v of languages) map.set(v.bcp47.toLowerCase(), v);
return map;
}, [languages]);
const byIso = useMemo(() => {
const map = new Map<string, VoiceLanguage>();
for (const v of languages) {
if (!map.has(v.iso.toLowerCase())) map.set(v.iso.toLowerCase(), v);
}
return map;
}, [languages]);
const getByBcp47 = useCallback(
(code: string | null | undefined) =>
code ? byBcp47.get(code.trim().toLowerCase()) : undefined,
[byBcp47],
);
const getByIso = useCallback(
(iso: string | null | undefined) =>
iso ? byIso.get(iso.trim().toLowerCase()) : undefined,
[byIso],
);
const isoToBcp47 = useCallback(
(iso: string | null | undefined): string | undefined => {
if (!iso) return undefined;
const trimmed = iso.trim();
if (!trimmed) return undefined;
if (trimmed.includes('-')) {
const canonical = byBcp47.get(trimmed.toLowerCase());
return canonical ? canonical.bcp47 : trimmed;
}
const entry = byIso.get(trimmed.toLowerCase());
if (entry) return entry.bcp47;
return `${trimmed.toLowerCase()}-${trimmed.toUpperCase()}`;
},
[byBcp47, byIso],
);
const getDefaultVoice = useCallback(
(bcp47: string | null | undefined): string | null => {
const entry = getByBcp47(bcp47);
return entry?.defaultVoice ?? null;
},
[getByBcp47],
);
const value = useMemo<VoiceCatalogContextType>(
() => ({
languages,
isLoading,
error,
getByBcp47,
getByIso,
isoToBcp47,
getDefaultVoice,
}),
[languages, isLoading, error, getByBcp47, getByIso, isoToBcp47, getDefaultVoice],
);
return (
<VoiceCatalogContext.Provider value={value}>{children}</VoiceCatalogContext.Provider>
);
};
export const useVoiceCatalog = (): VoiceCatalogContextType => {
const ctx = useContext(VoiceCatalogContext);
if (!ctx) {
throw new Error('useVoiceCatalog must be used within VoiceCatalogProvider');
}
return ctx;
};
/**
* Map a profile language (ISO short code) to a default voice locale.
* Returns the catalog's BCP-47 for the ISO if available, else falls back to
* `de-DE` so the UI always has a deterministic starting value.
*/
export const useDefaultVoiceLocale = (profileLanguage?: string | null): string => {
const { isoToBcp47 } = useVoiceCatalog();
return isoToBcp47(profileLanguage) || 'de-DE';
};

View file

@ -11,6 +11,7 @@ import { setUserDataCache, getUserDataCache } from '../utils/userCache';
import { FormGeneratorForm } from '../components/FormGenerator/FormGeneratorForm/FormGeneratorForm'; import { FormGeneratorForm } from '../components/FormGenerator/FormGeneratorForm/FormGeneratorForm';
import type { AttributeDefinition } from '../components/FormGenerator/FormGeneratorForm/FormGeneratorForm'; import type { AttributeDefinition } from '../components/FormGenerator/FormGeneratorForm/FormGeneratorForm';
import { useApiRequest } from '../hooks/useApi'; import { useApiRequest } from '../hooks/useApi';
import { useVoiceCatalog } from '../contexts/VoiceCatalogContext';
import styles from './Settings.module.css'; import styles from './Settings.module.css';
// ============================================================================= // =============================================================================
@ -92,6 +93,7 @@ interface VoiceMapEntry { language: string; voiceName: string; }
const VoiceSettingsTab: React.FC = () => { const VoiceSettingsTab: React.FC = () => {
const { t } = useLanguage(); const { t } = useLanguage();
const { request } = useApiRequest(); const { request } = useApiRequest();
const { languages: voiceCatalog } = useVoiceCatalog();
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
@ -100,7 +102,6 @@ const VoiceSettingsTab: React.FC = () => {
const [success, setSuccess] = useState<string | null>(null); const [success, setSuccess] = useState<string | null>(null);
const [sttLanguage, setSttLanguage] = useState('de-DE'); const [sttLanguage, setSttLanguage] = useState('de-DE');
const [languages, setLanguages] = useState<any[]>([]);
const [voiceMap, setVoiceMap] = useState<VoiceMapEntry[]>([]); const [voiceMap, setVoiceMap] = useState<VoiceMapEntry[]>([]);
const [addLanguage, setAddLanguage] = useState('de-DE'); const [addLanguage, setAddLanguage] = useState('de-DE');
@ -111,13 +112,7 @@ const VoiceSettingsTab: React.FC = () => {
const _loadSettings = useCallback(async () => { const _loadSettings = useCallback(async () => {
setLoading(true); setLoading(true);
try { try {
const [prefsData, languagesData] = await Promise.all([ const prefsData = await request({ url: '/api/voice/preferences', method: 'get' });
request({ url: '/api/voice/preferences', method: 'get' }),
request({ url: '/api/voice/languages', method: 'get' }),
]);
const langList = (languagesData as any)?.languages || [];
setLanguages(langList);
const prefs = prefsData as any; const prefs = prefsData as any;
setSttLanguage(prefs?.sttLanguage || 'de-DE'); setSttLanguage(prefs?.sttLanguage || 'de-DE');
@ -203,16 +198,9 @@ const VoiceSettingsTab: React.FC = () => {
}, [request]); }, [request]);
const _getLanguageName = useCallback((code: string) => { const _getLanguageName = useCallback((code: string) => {
const found = languages.find((l: any) => (l.code || l) === code); const entry = voiceCatalog.find(l => l.bcp47.toLowerCase() === code.toLowerCase());
return found?.name || found?.code || code; return entry ? `${entry.flag ? entry.flag + ' ' : ''}${entry.label}` : code;
}, [languages]); }, [voiceCatalog]);
const _defaultLangs = [
{ code: 'de-DE', name: 'Deutsch' }, { code: 'en-US', name: 'English (US)' },
{ code: 'fr-FR', name: 'Francais' }, { code: 'it-IT', name: 'Italiano' },
{ code: 'es-ES', name: 'Espanol' },
];
const _displayLanguages = languages.length > 0 ? languages : _defaultLangs;
if (loading) return <div style={{ padding: '1rem', color: '#888' }}>{t('Einstellungen werden geladen')}</div>; if (loading) return <div style={{ padding: '1rem', color: '#888' }}>{t('Einstellungen werden geladen')}</div>;
@ -230,8 +218,10 @@ const VoiceSettingsTab: React.FC = () => {
</div> </div>
<div className={styles.settingControl}> <div className={styles.settingControl}>
<select className={styles.select} value={sttLanguage} onChange={e => setSttLanguage(e.target.value)}> <select className={styles.select} value={sttLanguage} onChange={e => setSttLanguage(e.target.value)}>
{_displayLanguages.map((lang: any) => ( {voiceCatalog.map(lang => (
<option key={lang.code || lang} value={lang.code || lang}>{lang.name || lang.code || lang}</option> <option key={lang.bcp47} value={lang.bcp47}>
{lang.flag ? `${lang.flag} ` : ''}{lang.label}
</option>
))} ))}
</select> </select>
</div> </div>
@ -274,8 +264,10 @@ const VoiceSettingsTab: React.FC = () => {
<div> <div>
<label className={styles.settingLabel} style={{ fontSize: '0.8rem' }}>{t('Sprache')}</label> <label className={styles.settingLabel} style={{ fontSize: '0.8rem' }}>{t('Sprache')}</label>
<select className={styles.select} value={addLanguage} onChange={e => setAddLanguage(e.target.value)}> <select className={styles.select} value={addLanguage} onChange={e => setAddLanguage(e.target.value)}>
{_displayLanguages.map((lang: any) => ( {voiceCatalog.map(lang => (
<option key={lang.code || lang} value={lang.code || lang}>{lang.name || lang.code || lang}</option> <option key={lang.bcp47} value={lang.bcp47}>
{lang.flag ? `${lang.flag} ` : ''}{lang.label}
</option>
))} ))}
</select> </select>
</div> </div>

View file

@ -2,6 +2,7 @@ import React, { useState, useEffect, useCallback, useRef } from 'react';
import { useCurrentInstance } from '../../../hooks/useCurrentInstance'; import { useCurrentInstance } from '../../../hooks/useCurrentInstance';
import * as teamsbotApi from '../../../api/teamsbotApi'; import * as teamsbotApi from '../../../api/teamsbotApi';
import type { TeamsbotConfig, ConfigUpdateRequest, VoiceOption } from '../../../api/teamsbotApi'; import type { TeamsbotConfig, ConfigUpdateRequest, VoiceOption } from '../../../api/teamsbotApi';
import type { VoiceLanguage } from '../../../api/voiceCatalogApi';
import { FaPlay, FaSpinner } from 'react-icons/fa'; import { FaPlay, FaSpinner } from 'react-icons/fa';
import styles from './Teamsbot.module.css'; import styles from './Teamsbot.module.css';
@ -36,8 +37,8 @@ export const TeamsbotSettingsView: React.FC = () => {
// Form state // Form state
const [formData, setFormData] = useState<ConfigUpdateRequest>({}); const [formData, setFormData] = useState<ConfigUpdateRequest>({});
// Dynamic voice data from Google TTS API // Voice catalog (single source of truth) + dynamic voices for the selected language
const [languages, setLanguages] = useState<string[]>([]); const [languages, setLanguages] = useState<VoiceLanguage[]>([]);
const [voices, setVoices] = useState<VoiceOption[]>([]); const [voices, setVoices] = useState<VoiceOption[]>([]);
const [loadingVoices, setLoadingVoices] = useState(false); const [loadingVoices, setLoadingVoices] = useState(false);
@ -247,19 +248,13 @@ export const TeamsbotSettingsView: React.FC = () => {
value={formData.language || 'de-DE'} value={formData.language || 'de-DE'}
onChange={(e) => _handleLanguageChange(e.target.value)} onChange={(e) => _handleLanguageChange(e.target.value)}
> >
{languages.length > 0 ? ( {languages.map(lang => (
languages.map((langCode, idx) => ( <option key={lang.bcp47} value={lang.bcp47}>
<option key={`${langCode}-${idx}`} value={langCode}>{langCode}</option> {lang.flag ? `${lang.flag} ` : ''}{lang.label} ({lang.bcp47})
)) </option>
) : ( ))}
<>
<option value="de-DE">{t('Deutsch (Deutschland)')}</option>
<option value="en-US">{t('Englisch (US)')}</option>
<option value="fr-FR">Francais</option>
</>
)}
</select> </select>
<span className={styles.hint}>Sprache fuer Captions und Sprachausgabe ({languages.length} Sprachen verfuegbar)</span> <span className={styles.hint}>{t('Sprache für Captions und Sprachausgabe')} ({languages.length} {t('Sprachen verfügbar')})</span>
</div> </div>
<div className={styles.formGroup}> <div className={styles.formGroup}>

View file

@ -11,21 +11,7 @@ import { useVoiceStream } from '../../../hooks/useSpeechAudioCapture';
import type { WorkspaceFile, DataSource, FeatureDataSource } from './useWorkspace'; import type { WorkspaceFile, DataSource, FeatureDataSource } from './useWorkspace';
import { useLanguage } from '../../../providers/language/LanguageContext'; import { useLanguage } from '../../../providers/language/LanguageContext';
import { useVoiceCatalog } from '../../../contexts/VoiceCatalogContext';
const _STT_LANGUAGES = [
{ code: 'de-DE', label: 'Deutsch' },
{ code: 'en-US', label: 'English (US)' },
{ code: 'en-GB', label: 'English (UK)' },
{ code: 'fr-FR', label: 'Francais' },
{ code: 'it-IT', label: 'Italiano' },
{ code: 'es-ES', label: 'Espanol' },
{ code: 'pt-BR', label: 'Portugues' },
{ code: 'nl-NL', label: 'Nederlands' },
{ code: 'pl-PL', label: 'Polski' },
{ code: 'ru-RU', label: 'Russkij' },
{ code: 'ja-JP', label: 'Japanese' },
{ code: 'zh-CN', label: 'Chinese' },
];
interface PendingFile { interface PendingFile {
fileId: string; fileId: string;
@ -88,6 +74,7 @@ export const WorkspaceInput: React.FC<WorkspaceInputProps> = ({ instanceId: _ins
onDraftAppendConsumed, onDraftAppendConsumed,
}) => { }) => {
const { t } = useLanguage(); const { t } = useLanguage();
const { languages: voiceCatalogLanguages } = useVoiceCatalog();
const [prompt, setPrompt] = useState(''); const [prompt, setPrompt] = useState('');
const [showAutocomplete, setShowAutocomplete] = useState(false); const [showAutocomplete, setShowAutocomplete] = useState(false);
const [autocompleteFilter, setAutocompleteFilter] = useState(''); const [autocompleteFilter, setAutocompleteFilter] = useState('');
@ -617,23 +604,23 @@ export const WorkspaceInput: React.FC<WorkspaceInputProps> = ({ instanceId: _ins
borderRadius: 8, boxShadow: '0 -2px 8px rgba(0,0,0,0.1)', zIndex: 20, borderRadius: 8, boxShadow: '0 -2px 8px rgba(0,0,0,0.1)', zIndex: 20,
maxHeight: 240, overflowY: 'auto', minWidth: 160, maxHeight: 240, overflowY: 'auto', minWidth: 160,
}}> }}>
{_STT_LANGUAGES.map(lang => ( {voiceCatalogLanguages.map(lang => (
<div <div
key={lang.code} key={lang.bcp47}
onClick={() => { onClick={() => {
setVoiceLanguage(lang.code); setVoiceLanguage(lang.bcp47);
setShowLangPicker(false); setShowLangPicker(false);
fetch('/api/voice/preferences', { method: 'PUT', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ sttLanguage: lang.code }) }).catch(() => {}); fetch('/api/voice/preferences', { method: 'PUT', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ sttLanguage: lang.bcp47 }) }).catch(() => {});
}} }}
style={{ style={{
padding: '8px 12px', cursor: 'pointer', fontSize: 13, padding: '8px 12px', cursor: 'pointer', fontSize: 13,
background: lang.code === voiceLanguage ? 'var(--primary-color, #F25843)' : 'transparent', background: lang.bcp47 === voiceLanguage ? 'var(--primary-color, #F25843)' : 'transparent',
color: lang.code === voiceLanguage ? '#fff' : 'var(--text-primary, #333)', color: lang.bcp47 === voiceLanguage ? '#fff' : 'var(--text-primary, #333)',
}} }}
onMouseEnter={e => { if (lang.code !== voiceLanguage) e.currentTarget.style.background = 'var(--bg-secondary, #f5f5f5)'; }} onMouseEnter={e => { if (lang.bcp47 !== voiceLanguage) e.currentTarget.style.background = 'var(--bg-secondary, #f5f5f5)'; }}
onMouseLeave={e => { if (lang.code !== voiceLanguage) e.currentTarget.style.background = ''; }} onMouseLeave={e => { if (lang.bcp47 !== voiceLanguage) e.currentTarget.style.background = ''; }}
> >
{lang.label} ({lang.code}) {lang.flag ? `${lang.flag} ` : ''}{lang.label} ({lang.bcp47})
</div> </div>
))} ))}
</div> </div>