import { useCallback, useEffect, useState } from 'react'; import { useTranslation } from '@/contexts/language'; import type { Language } from '@/contexts/language'; import { useLocalStorage } from '@/hooks/useLocalStorage'; import { detectLanguage } from '@/lib/detectLanguage'; import { isTranslationPairSupported, translateText } from '@/lib/translation'; import { cn } from '@/lib/utils'; const SOURCE_NAMES: Record = { en: 'English', es: 'Spanish', ca: 'Catalan', eu: 'Basque', gl: 'Galician', hi: 'Hindi', th: 'Thai', ja: 'Japanese', pt: 'Portuguese', 'pt-PT': 'Portuguese (Portugal)', fr: 'French', de: 'German', it: 'Italian', nl: 'Dutch', pl: 'Polish', ru: 'Russian', ko: 'Korean', 'ko-KP': 'Korean (North)', 'zh-CN': 'Chinese (Simplified)', 'zh-TW': 'Chinese (Traditional)', uk: 'Ukrainian', tr: 'Turkish', vi: 'Vietnamese', id: 'Indonesian', sv: 'Swedish', cs: 'Czech', ro: 'Romanian', el: 'Greek', }; type State = 'idle' | 'detecting' | 'loading' | 'done' | 'error' | 'same'; interface TranslateMessageProps { text: string; messageId: string; isOwn?: boolean; className?: string; } /** * Message body with on-device translation. * * English ↔ Spanish is the pilot. The model runs in a worker; the message text * is only sent to that local worker (never to a server). Translations are kept * in memory and cleared on reload/logout. */ export function TranslateMessage({ text, messageId, isOwn = false, className }: TranslateMessageProps) { const { language, t } = useTranslation(); const [autoTranslate] = useLocalStorage('skatespots:auto-translate', false); const [state, setState] = useState('idle'); const [translated, setTranslated] = useState(null); const [source, setSource] = useState(null); const [showOriginal, setShowOriginal] = useState(false); const [progress, setProgress] = useState(null); const [error, setError] = useState(null); const handleTranslate = useCallback(async () => { if (!text.trim()) return; setState('detecting'); setError(null); setProgress(null); setShowOriginal(false); const detected = await detectLanguage(text); if (!detected.language) { setState('error'); setError(t('translateUi.failed')); return; } setSource(detected.language); if (detected.language === language) { setState('same'); return; } if (!isTranslationPairSupported(detected.language, language)) { setState('error'); setError(t('translateUi.unsupported')); return; } setState('loading'); try { const result = await translateText({ id: messageId, text, source: detected.language, target: language, onProgress: (p) => setProgress(typeof p.progress === 'number' ? p.progress : null), }); setTranslated(result); setState('done'); } catch { setState('error'); setError(t('translateUi.failed')); } }, [text, messageId, language, t]); useEffect(() => { if (!autoTranslate || isOwn) return; if (state !== 'idle' || !text.trim()) return; void handleTranslate(); }, [autoTranslate, isOwn, state, text, handleTranslate]); const body = state === 'done' && !showOriginal ? translated ?? text : text; return (

{body}

{!isOwn && (
{state === 'idle' && ( )} {(state === 'detecting' || state === 'loading') && ( {t('translateUi.translating')} {progress !== null ? ` ${Math.round(progress)}%` : '…'} )} {state === 'done' && ( <> {source && ( {t('translateUi.translatedFrom', { language: SOURCE_NAMES[source] })} )} )} {state === 'error' && ( <> {error ?? t('translateUi.failed')} )}
)}
); }