2023-03-07 17:38:06 +03:00
|
|
|
import './translation-block.css';
|
|
|
|
|
2023-09-23 14:45:54 +03:00
|
|
|
import pRetry from 'p-retry';
|
2023-07-18 08:31:26 +03:00
|
|
|
import pThrottle from 'p-throttle';
|
2023-03-07 17:38:06 +03:00
|
|
|
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';
|
2023-10-14 15:10:34 +03:00
|
|
|
import pmem from '../utils/pmem';
|
2023-03-07 17:38:06 +03:00
|
|
|
|
|
|
|
import Icon from './icon';
|
|
|
|
import Loader from './loader';
|
|
|
|
|
2023-07-18 08:31:26 +03:00
|
|
|
const throttle = pThrottle({
|
|
|
|
limit: 1,
|
|
|
|
interval: 2000,
|
|
|
|
});
|
|
|
|
|
2023-09-23 14:45:54 +03:00
|
|
|
// 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.lunar.icu',
|
2023-10-24 19:14:26 +03:00
|
|
|
'lingva.garudalinux.org',
|
2023-09-23 14:45:54 +03:00
|
|
|
'translate.plausibility.cloud',
|
|
|
|
];
|
|
|
|
let currentLingvaInstance = 0;
|
|
|
|
|
2023-10-14 15:10:34 +03:00
|
|
|
function _lingvaTranslate(text, source, target) {
|
2023-07-18 08:31:26 +03:00
|
|
|
console.log('TRANSLATE', text, source, target);
|
2023-09-23 14:45:54 +03:00
|
|
|
const fetchCall = () => {
|
|
|
|
let instance = LINGVA_INSTANCES[currentLingvaInstance];
|
|
|
|
return fetch(
|
|
|
|
`https://${instance}/api/v1/${source}/${target}/${encodeURIComponent(
|
|
|
|
text,
|
|
|
|
)}`,
|
|
|
|
)
|
2023-10-24 19:13:42 +03:00
|
|
|
.then((res) => {
|
|
|
|
if (!res.ok) throw new Error(res.statusText);
|
|
|
|
return res.json();
|
|
|
|
})
|
2023-09-23 14:45:54 +03:00
|
|
|
.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,
|
|
|
|
);
|
|
|
|
},
|
|
|
|
});
|
2023-10-12 07:48:09 +03:00
|
|
|
// return masto.v1.statuses.$select(id).translate({
|
2023-07-18 08:31:26 +03:00
|
|
|
// lang: DEFAULT_LANG,
|
|
|
|
// });
|
|
|
|
}
|
2023-10-14 15:10:34 +03:00
|
|
|
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,
|
|
|
|
});
|
2023-07-18 08:31:26 +03:00
|
|
|
|
2023-03-07 17:38:06 +03:00
|
|
|
function TranslationBlock({
|
|
|
|
forceTranslate,
|
|
|
|
sourceLanguage,
|
|
|
|
onTranslate,
|
|
|
|
text = '',
|
2023-07-18 08:31:26 +03:00
|
|
|
mini,
|
2023-03-07 17:38:06 +03:00
|
|
|
}) {
|
|
|
|
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');
|
|
|
|
|
2023-07-18 08:31:26 +03:00
|
|
|
if (!onTranslate) {
|
|
|
|
onTranslate = mini ? throttledLingvaTranslate : lingvaTranslate;
|
|
|
|
}
|
2023-03-07 17:38:06 +03:00
|
|
|
|
|
|
|
const translate = async () => {
|
|
|
|
setUIState('loading');
|
2023-04-27 13:12:38 +03:00
|
|
|
try {
|
2023-09-23 14:45:54 +03:00
|
|
|
const { content, detectedSourceLanguage, provider, error, ...props } =
|
2023-07-18 08:31:26 +03:00
|
|
|
await onTranslate(text, apiSourceLang.current, targetLang);
|
2023-04-27 13:12:38 +03:00
|
|
|
if (content) {
|
|
|
|
if (detectedSourceLanguage) {
|
|
|
|
const detectedLangText = localeCode2Text(detectedSourceLanguage);
|
|
|
|
setDetectedLang(detectedLangText);
|
|
|
|
}
|
|
|
|
if (provider === 'lingva') {
|
|
|
|
const pronunciation = props?.info?.pronunciation?.query;
|
|
|
|
if (pronunciation) {
|
|
|
|
setPronunciationContent(pronunciation);
|
|
|
|
}
|
2023-03-07 17:38:06 +03:00
|
|
|
}
|
2023-04-27 13:12:38 +03:00
|
|
|
setTranslatedContent(content);
|
|
|
|
setUIState('default');
|
2023-07-22 15:48:01 +03:00
|
|
|
if (!mini && content.trim() !== text.trim()) {
|
2023-07-18 08:31:26 +03:00
|
|
|
detailsRef.current.open = true;
|
|
|
|
detailsRef.current.scrollIntoView({
|
|
|
|
behavior: 'smooth',
|
|
|
|
block: 'nearest',
|
|
|
|
});
|
|
|
|
}
|
2023-04-27 13:12:38 +03:00
|
|
|
} else {
|
2023-09-23 14:45:18 +03:00
|
|
|
if (error) console.error(error);
|
2023-04-27 13:12:38 +03:00
|
|
|
setUIState('error');
|
2023-03-07 17:38:06 +03:00
|
|
|
}
|
2023-04-27 13:12:38 +03:00
|
|
|
} catch (e) {
|
|
|
|
console.error(e);
|
2023-03-07 17:38:06 +03:00
|
|
|
setUIState('error');
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
if (forceTranslate) {
|
|
|
|
translate();
|
|
|
|
}
|
|
|
|
}, [forceTranslate]);
|
|
|
|
|
2023-07-18 08:31:26 +03:00
|
|
|
if (mini) {
|
2023-07-22 15:30:18 +03:00
|
|
|
if (
|
|
|
|
!!translatedContent &&
|
|
|
|
translatedContent.trim() !== text.trim() &&
|
|
|
|
detectedLang !== targetLangText
|
|
|
|
) {
|
2023-07-18 08:31:26 +03:00
|
|
|
return (
|
2023-07-24 17:27:30 +03:00
|
|
|
<div class="shazam-container">
|
|
|
|
<div class="shazam-container-inner">
|
|
|
|
<div class="status-translation-block-mini">
|
|
|
|
<Icon
|
|
|
|
icon="translate"
|
|
|
|
alt={`Auto-translated from ${sourceLangText}`}
|
|
|
|
/>
|
|
|
|
<output
|
|
|
|
lang={targetLang}
|
|
|
|
dir="auto"
|
|
|
|
title={pronunciationContent || ''}
|
|
|
|
>
|
|
|
|
{translatedContent}
|
|
|
|
</output>
|
|
|
|
</div>
|
|
|
|
</div>
|
2023-07-18 08:31:26 +03:00
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2023-03-07 17:38:06 +03:00
|
|
|
return (
|
2023-03-21 15:48:26 +03:00
|
|
|
<div
|
|
|
|
class="status-translation-block"
|
|
|
|
onClick={(e) => {
|
|
|
|
e.preventDefault();
|
|
|
|
}}
|
|
|
|
>
|
2023-03-07 17:38:06 +03:00
|
|
|
<details ref={detailsRef}>
|
|
|
|
<summary>
|
|
|
|
<button
|
|
|
|
type="button"
|
|
|
|
onClick={async (e) => {
|
|
|
|
e.preventDefault();
|
|
|
|
e.stopPropagation();
|
|
|
|
detailsRef.current.open = !detailsRef.current.open;
|
|
|
|
if (uiState === 'loading') return;
|
|
|
|
if (!translatedContent) translate();
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
<Icon icon="translate" />{' '}
|
|
|
|
<span>
|
|
|
|
{uiState === 'loading'
|
|
|
|
? 'Translating…'
|
2023-06-05 06:40:30 +03:00
|
|
|
: sourceLanguage && sourceLangText && !detectedLang
|
2023-03-07 17:38:06 +03:00
|
|
|
? `Translate from ${sourceLangText}`
|
|
|
|
: `Translate`}
|
|
|
|
</span>
|
|
|
|
</button>
|
|
|
|
</summary>
|
|
|
|
<div class="translated-block">
|
|
|
|
<div class="translation-info insignificant">
|
|
|
|
<select
|
|
|
|
class="translated-source-select"
|
|
|
|
disabled={uiState === 'loading'}
|
|
|
|
onChange={(e) => {
|
|
|
|
apiSourceLang.current = e.target.value;
|
|
|
|
translate();
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
{sourceLanguages.map((l) => (
|
|
|
|
<option value={l.code}>
|
|
|
|
{l.code === 'auto' ? `Auto (${detectedLang ?? '…'})` : l.name}
|
|
|
|
</option>
|
|
|
|
))}
|
|
|
|
</select>{' '}
|
|
|
|
<span>→ {targetLangText}</span>
|
|
|
|
<Loader abrupt hidden={uiState !== 'loading'} />
|
|
|
|
</div>
|
|
|
|
{uiState === 'error' ? (
|
|
|
|
<p class="ui-state">Failed to translate</p>
|
|
|
|
) : (
|
|
|
|
!!translatedContent && (
|
|
|
|
<>
|
2023-04-10 15:23:40 +03:00
|
|
|
<output class="translated-content" lang={targetLang} dir="auto">
|
2023-03-31 04:44:12 +03:00
|
|
|
{translatedContent}
|
|
|
|
</output>
|
2023-03-07 17:38:06 +03:00
|
|
|
{!!pronunciationContent && (
|
2023-03-31 04:44:12 +03:00
|
|
|
<output
|
|
|
|
class="translated-pronunciation-content"
|
|
|
|
tabIndex={-1}
|
2023-03-31 09:09:21 +03:00
|
|
|
onClick={(e) => {
|
|
|
|
e.target.classList.toggle('expand');
|
|
|
|
}}
|
2023-03-31 04:44:12 +03:00
|
|
|
>
|
2023-03-07 17:38:06 +03:00
|
|
|
{pronunciationContent}
|
|
|
|
</output>
|
|
|
|
)}
|
|
|
|
</>
|
|
|
|
)
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
</details>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
export default TranslationBlock;
|