Experimental Auto Inline Translation (AIT)

For short posts for now and throttled API calls
This commit is contained in:
Lim Chee Aun 2023-07-18 13:31:26 +08:00
parent ff41cd3563
commit 92a4f502a0
3 changed files with 96 additions and 32 deletions

View file

@ -57,6 +57,7 @@ import MenuLink from './menu-link';
import RelativeTime from './relative-time'; import RelativeTime from './relative-time';
import TranslationBlock from './translation-block'; import TranslationBlock from './translation-block';
const INLINE_TRASNSLATE_LIMIT = 140;
const throttle = pThrottle({ const throttle = pThrottle({
limit: 1, limit: 1,
interval: 1000, interval: 1000,
@ -244,11 +245,23 @@ function Status({
); );
} }
const isSizeLarge = size === 'l';
const [forceTranslate, setForceTranslate] = useState(_forceTranslate); const [forceTranslate, setForceTranslate] = useState(_forceTranslate);
const targetLanguage = getTranslateTargetLanguage(true); const targetLanguage = getTranslateTargetLanguage(true);
const contentTranslationHideLanguages = const contentTranslationHideLanguages =
snapStates.settings.contentTranslationHideLanguages || []; snapStates.settings.contentTranslationHideLanguages || [];
if (!snapStates.settings.contentTranslation) enableTranslate = false; if (!snapStates.settings.contentTranslation) enableTranslate = false;
const inlineTranslate = useMemo(() => {
return (
!isSizeLarge &&
!spoilerText &&
!poll &&
!mediaAttachments?.length &&
content?.length > 0 &&
content?.length <= INLINE_TRASNSLATE_LIMIT
);
}, [isSizeLarge, content, spoilerText, poll, mediaAttachments]);
const [showEdited, setShowEdited] = useState(false); const [showEdited, setShowEdited] = useState(false);
const [showReactions, setShowReactions] = useState(false); const [showReactions, setShowReactions] = useState(false);
@ -306,7 +319,6 @@ function Status({
const createdDateText = niceDateTime(createdAtDate); const createdDateText = niceDateTime(createdAtDate);
const editedDateText = editedAt && niceDateTime(editedAtDate); const editedDateText = editedAt && niceDateTime(editedAtDate);
const isSizeLarge = size === 'l';
// Can boost if: // Can boost if:
// - authenticated AND // - authenticated AND
// - visibility != direct OR // - visibility != direct OR
@ -1091,10 +1103,13 @@ function Status({
}} }}
/> />
)} )}
{((enableTranslate && !!content.trim() && differentLanguage) || {(((enableTranslate || inlineTranslate) &&
!!content.trim() &&
differentLanguage) ||
forceTranslate) && ( forceTranslate) && (
<TranslationBlock <TranslationBlock
forceTranslate={forceTranslate} forceTranslate={forceTranslate || inlineTranslate}
mini={inlineTranslate}
sourceLanguage={language} sourceLanguage={language}
text={ text={
(spoilerText ? `${spoilerText}\n\n` : '') + (spoilerText ? `${spoilerText}\n\n` : '') +

View file

@ -105,3 +105,22 @@
overflow: visible; overflow: visible;
mask-image: none; mask-image: none;
} }
/* MINI */
.status-translation-block-mini {
display: flex;
margin: 8px 0 0;
padding: 8px 0 0;
font-size: 90%;
border-top: var(--hairline-width) solid var(--outline-color);
color: var(--text-insignificant-color);
gap: 8px;
transition: color 0.3s ease-in-out;
}
.status-translation-block-mini .icon {
margin-top: 2px;
}
.status:is(:hover, :active) .status-translation-block-mini {
color: var(--text-color);
}

View file

@ -1,5 +1,6 @@
import './translation-block.css'; import './translation-block.css';
import pThrottle from 'p-throttle';
import { useEffect, useRef, useState } from 'preact/hooks'; import { useEffect, useRef, useState } from 'preact/hooks';
import sourceLanguages from '../data/lingva-source-languages'; import sourceLanguages from '../data/lingva-source-languages';
@ -9,11 +10,41 @@ import localeCode2Text from '../utils/localeCode2Text';
import Icon from './icon'; import Icon from './icon';
import Loader from './loader'; import Loader from './loader';
const throttle = pThrottle({
limit: 1,
interval: 2000,
});
function lingvaTranslate(text, source, target) {
console.log('TRANSLATE', text, source, target);
// Using another API instance instead of lingva.ml because of this bug (slashes don't work):
// https://github.com/thedaviddelta/lingva-translate/issues/68
return fetch(
`https://lingva.garudalinux.org/api/v1/${source}/${target}/${encodeURIComponent(
text,
)}`,
)
.then((res) => res.json())
.then((res) => {
return {
provider: 'lingva',
content: res.translation,
detectedSourceLanguage: res.info?.detectedSource,
info: res.info,
};
});
// return masto.v1.statuses.translate(id, {
// lang: DEFAULT_LANG,
// });
}
const throttledLingvaTranslate = throttle(lingvaTranslate);
function TranslationBlock({ function TranslationBlock({
forceTranslate, forceTranslate,
sourceLanguage, sourceLanguage,
onTranslate, onTranslate,
text = '', text = '',
mini,
}) { }) {
const targetLang = getTranslateTargetLanguage(true); const targetLang = getTranslateTargetLanguage(true);
const [uiState, setUIState] = useState('default'); const [uiState, setUIState] = useState('default');
@ -28,35 +59,15 @@ function TranslationBlock({
const targetLangText = localeCode2Text(targetLang); const targetLangText = localeCode2Text(targetLang);
const apiSourceLang = useRef('auto'); const apiSourceLang = useRef('auto');
if (!onTranslate) if (!onTranslate) {
onTranslate = (source, target) => { onTranslate = mini ? throttledLingvaTranslate : lingvaTranslate;
console.log('TRANSLATE', source, target, text); }
// Using another API instance instead of lingva.ml because of this bug (slashes don't work):
// https://github.com/thedaviddelta/lingva-translate/issues/68
return fetch(
`https://lingva.garudalinux.org/api/v1/${source}/${target}/${encodeURIComponent(
text,
)}`,
)
.then((res) => res.json())
.then((res) => {
return {
provider: 'lingva',
content: res.translation,
detectedSourceLanguage: res.info?.detectedSource,
info: res.info,
};
});
// return masto.v1.statuses.translate(id, {
// lang: DEFAULT_LANG,
// });
};
const translate = async () => { const translate = async () => {
setUIState('loading'); setUIState('loading');
try { try {
const { content, detectedSourceLanguage, provider, ...props } = const { content, detectedSourceLanguage, provider, ...props } =
await onTranslate(apiSourceLang.current, targetLang); await onTranslate(text, apiSourceLang.current, targetLang);
if (content) { if (content) {
if (detectedSourceLanguage) { if (detectedSourceLanguage) {
const detectedLangText = localeCode2Text(detectedSourceLanguage); const detectedLangText = localeCode2Text(detectedSourceLanguage);
@ -70,11 +81,13 @@ function TranslationBlock({
} }
setTranslatedContent(content); setTranslatedContent(content);
setUIState('default'); setUIState('default');
detailsRef.current.open = true; if (!mini) {
detailsRef.current.scrollIntoView({ detailsRef.current.open = true;
behavior: 'smooth', detailsRef.current.scrollIntoView({
block: 'nearest', behavior: 'smooth',
}); block: 'nearest',
});
}
} else { } else {
console.error(result); console.error(result);
setUIState('error'); setUIState('error');
@ -91,6 +104,23 @@ function TranslationBlock({
} }
}, [forceTranslate]); }, [forceTranslate]);
if (mini) {
if (!!translatedContent && detectedLang !== targetLangText) {
return (
<div class="status-translation-block-mini">
<Icon
icon="translate"
alt={`Auto-translated from ${sourceLangText}`}
/>
<output lang={targetLang} dir="auto">
{translatedContent}
</output>
</div>
);
}
return null;
}
return ( return (
<div <div
class="status-translation-block" class="status-translation-block"