import './translation-block.css'; import pRetry from 'p-retry'; import pThrottle from 'p-throttle'; import { useEffect, useRef, useState } from 'preact/hooks'; import sourceLanguages from '../data/lingva-source-languages'; import getTranslateTargetLanguage from '../utils/get-translate-target-language'; import localeCode2Text from '../utils/localeCode2Text'; import pmem from '../utils/pmem'; import Icon from './icon'; import Loader from './loader'; const throttle = pThrottle({ limit: 1, interval: 2000, }); // Using other API instances instead of lingva.ml because of this bug (slashes don't work): // https://github.com/thedaviddelta/lingva-translate/issues/68 const LINGVA_INSTANCES = [ 'lingva.phanpy.social', 'lingva.lunar.icu', 'lingva.garudalinux.org', 'translate.plausibility.cloud', ]; let currentLingvaInstance = 0; function _lingvaTranslate(text, source, target) { console.log('TRANSLATE', text, source, target); const fetchCall = () => { let instance = LINGVA_INSTANCES[currentLingvaInstance]; return fetch( `https://${instance}/api/v1/${source}/${target}/${encodeURIComponent( text, )}`, ) .then((res) => { if (!res.ok) throw new Error(res.statusText); return res.json(); }) .then((res) => { return { provider: 'lingva', content: res.translation, detectedSourceLanguage: res.info?.detectedSource, info: res.info, }; }); }; return pRetry(fetchCall, { retries: 3, onFailedAttempt: (e) => { currentLingvaInstance = (currentLingvaInstance + 1) % LINGVA_INSTANCES.length; console.log( 'Retrying translation with another instance', currentLingvaInstance, ); }, }); // return masto.v1.statuses.$select(id).translate({ // lang: DEFAULT_LANG, // }); } const TRANSLATED_MAX_AGE = 1000 * 60 * 60; // 1 hour const lingvaTranslate = pmem(_lingvaTranslate, { maxAge: TRANSLATED_MAX_AGE, }); const throttledLingvaTranslate = pmem(throttle(lingvaTranslate), { // I know, this is double-layered memoization maxAge: TRANSLATED_MAX_AGE, }); function TranslationBlock({ forceTranslate, sourceLanguage, onTranslate, text = '', mini, }) { const targetLang = getTranslateTargetLanguage(true); const [uiState, setUIState] = useState('default'); const [pronunciationContent, setPronunciationContent] = useState(null); const [translatedContent, setTranslatedContent] = useState(null); const [detectedLang, setDetectedLang] = useState(null); const detailsRef = useRef(); const sourceLangText = sourceLanguage ? localeCode2Text(sourceLanguage) : null; const targetLangText = localeCode2Text(targetLang); const apiSourceLang = useRef('auto'); if (!onTranslate) { onTranslate = mini ? throttledLingvaTranslate : lingvaTranslate; } const translate = async () => { setUIState('loading'); try { const { content, detectedSourceLanguage, provider, error, ...props } = await onTranslate(text, apiSourceLang.current, targetLang); if (content) { if (detectedSourceLanguage) { const detectedLangText = localeCode2Text(detectedSourceLanguage); setDetectedLang(detectedLangText); } if (provider === 'lingva') { const pronunciation = props?.info?.pronunciation?.query; if (pronunciation) { setPronunciationContent(pronunciation); } } setTranslatedContent(content); setUIState('default'); if (!mini && content.trim() !== text.trim()) { detailsRef.current.open = true; detailsRef.current.scrollIntoView({ behavior: 'smooth', block: 'nearest', }); } } else { if (error) console.error(error); setUIState('error'); } } catch (e) { console.error(e); setUIState('error'); } }; useEffect(() => { if (forceTranslate) { translate(); } }, [forceTranslate]); if (mini) { if ( !!translatedContent && translatedContent.trim() !== text.trim() && detectedLang !== targetLangText ) { return (
Failed to translate
) : ( !!translatedContent && ( <> {!!pronunciationContent && ( )} > ) )}