import './translation-block.css'; import { Trans, useLingui } from '@lingui/react/macro'; import pRetry from 'p-retry'; import pThrottle from 'p-throttle'; import { useEffect, useRef, useState } from 'preact/hooks'; import languages from '../data/translang-languages'; import { translate as browserTranslate, supportsBrowserTranslator, } from '../utils/browser-translator'; import getTranslateTargetLanguage from '../utils/get-translate-target-language'; import localeCode2Text from '../utils/localeCode2Text'; import pmem from '../utils/pmem'; import Icon from './icon'; import LazyShazam from './lazy-shazam'; import Loader from './loader'; const sourceLanguages = Object.entries(languages.sl).map(([code, name]) => ({ code, name, })); const { PHANPY_TRANSLANG_INSTANCES } = import.meta.env; const TRANSLANG_INSTANCES = PHANPY_TRANSLANG_INSTANCES ? PHANPY_TRANSLANG_INSTANCES.split(/\s+/) : []; const throttle = pThrottle({ limit: 1, interval: 2000, }); const TRANSLATED_MAX_AGE = 1000 * 60 * 60; // 1 hour let currentTranslangInstance = 0; function _translangTranslate(text, source, target) { console.log('TRANSLATE', text, source, target); const fetchCall = () => { let instance = TRANSLANG_INSTANCES[currentTranslangInstance]; const tooLong = text.length > 2000; let fetchPromise; if (tooLong) { // POST fetchPromise = fetch(`https://${instance}/api/v1/translate`, { method: 'POST', priority: 'low', referrerPolicy: 'no-referrer', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ sl: source, tl: target, text, }), }); } else { // GET fetchPromise = fetch( `https://${instance}/api/v1/translate?sl=${encodeURIComponent(source)}&tl=${encodeURIComponent(target)}&text=${encodeURIComponent(text)}`, { priority: 'low', referrerPolicy: 'no-referrer', }, ); } return fetchPromise .then((res) => { if (!res.ok) throw new Error(res.statusText); return res.json(); }) .then((res) => { return { provider: 'translang', content: res.translated_text, detectedSourceLanguage: res.detected_language, pronunciation: res.pronunciation, }; }); }; return pRetry(fetchCall, { retries: 3, onFailedAttempt: (e) => { currentTranslangInstance = (currentTranslangInstance + 1) % TRANSLANG_INSTANCES.length; console.log( 'Retrying translation with another instance', currentTranslangInstance, ); }, }); } const translangTranslate = pmem(_translangTranslate, { maxAge: TRANSLATED_MAX_AGE, }); const throttledTranslangTranslate = pmem(throttle(translangTranslate), { // I know, this is double-layered memoization maxAge: TRANSLATED_MAX_AGE, }); const throttledBrowserTranslate = throttle(browserTranslate); function TranslationBlock({ forceTranslate, sourceLanguage, onTranslate, text = '', mini, autoDetected, }) { const { t } = useLingui(); 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 = async (...args) => { if (supportsBrowserTranslator) { const result = await throttledBrowserTranslate(...args); if (result && !result.error) { return result; } } return mini ? await throttledTranslangTranslate(...args) : await translangTranslate(...args); }; } 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 === 'translang') { const pronunciation = props?.pronunciation; 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 (
{translatedContent}
); } return null; } return (
{ e.preventDefault(); }} >
{' '} → {targetLangText}
{uiState === 'error' ? (

Failed to translate

) : ( !!translatedContent && ( <> {translatedContent} {!!pronunciationContent && ( { e.target.classList.toggle('expand'); }} > {pronunciationContent} )} ) )}
); } export default TRANSLANG_INSTANCES?.length ? TranslationBlock : () => null;