mirror of
https://github.com/cheeaun/phanpy.git
synced 2024-11-24 10:15:37 +03:00
Alrighty, let's test this post translation out!
This commit is contained in:
parent
f4275d27fe
commit
355b3be6e9
16 changed files with 1530 additions and 14 deletions
18
scripts/fetch-lingva-languages.js
Normal file
18
scripts/fetch-lingva-languages.js
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
// Fetch https://lingva.ml/api/v1/languages/{source|target}
|
||||||
|
import fs from 'fs';
|
||||||
|
|
||||||
|
fetch('https://lingva.ml/api/v1/languages/source')
|
||||||
|
.then((response) => response.json())
|
||||||
|
.then((json) => {
|
||||||
|
const file = './src/data/lingva-source-languages.json';
|
||||||
|
console.log(`Writing ${file}...`);
|
||||||
|
fs.writeFileSync(file, JSON.stringify(json.languages, null, '\t'), 'utf8');
|
||||||
|
});
|
||||||
|
|
||||||
|
fetch('https://lingva.ml/api/v1/languages/target')
|
||||||
|
.then((response) => response.json())
|
||||||
|
.then((json) => {
|
||||||
|
const file = './src/data/lingva-target-languages.json';
|
||||||
|
console.log(`Writing ${file}...`);
|
||||||
|
fs.writeFileSync(file, JSON.stringify(json.languages, null, '\t'), 'utf8');
|
||||||
|
});
|
|
@ -1007,6 +1007,12 @@ body:has(.status-deck) .media-post-link {
|
||||||
.sheet header :is(h1, h2, h3) {
|
.sheet header :is(h1, h2, h3) {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
.sheet header.header-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr auto;
|
||||||
|
grid-gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
.sheet main {
|
.sheet main {
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
|
|
|
@ -63,6 +63,7 @@ const ICONS = {
|
||||||
share: 'mingcute:share-2-line',
|
share: 'mingcute:share-2-line',
|
||||||
sparkles: 'mingcute:sparkles-line',
|
sparkles: 'mingcute:sparkles-line',
|
||||||
exit: 'mingcute:exit-line',
|
exit: 'mingcute:exit-line',
|
||||||
|
translate: 'mingcute:translate-line',
|
||||||
};
|
};
|
||||||
|
|
||||||
const modules = import.meta.glob('/node_modules/@iconify-icons/mingcute/*.js');
|
const modules = import.meta.glob('/node_modules/@iconify-icons/mingcute/*.js');
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
animation: appear 0.3s ease-in-out 1s both;
|
animation: appear 0.3s ease-in-out 1s both;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
margin: 8px;
|
margin: 8px;
|
||||||
|
vertical-align: baseline !important;
|
||||||
}
|
}
|
||||||
.loader-container.abrupt {
|
.loader-container.abrupt {
|
||||||
animation: none;
|
animation: none;
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { Menu, MenuItem } from '@szhsin/react-menu';
|
||||||
import { getBlurHashAverageColor } from 'fast-blurhash';
|
import { getBlurHashAverageColor } from 'fast-blurhash';
|
||||||
import { useEffect, useLayoutEffect, useRef, useState } from 'preact/hooks';
|
import { useEffect, useLayoutEffect, useRef, useState } from 'preact/hooks';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
|
@ -6,6 +7,7 @@ import Icon from './icon';
|
||||||
import Link from './link';
|
import Link from './link';
|
||||||
import Media from './media';
|
import Media from './media';
|
||||||
import Modal from './modal';
|
import Modal from './modal';
|
||||||
|
import TranslationBlock from './translation-block';
|
||||||
|
|
||||||
function MediaModal({
|
function MediaModal({
|
||||||
mediaAttachments,
|
mediaAttachments,
|
||||||
|
@ -234,9 +236,39 @@ function MediaModal({
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<MediaAltModal alt={showMediaAlt} />
|
||||||
|
</Modal>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function MediaAltModal({ alt }) {
|
||||||
|
const [forceTranslate, setForceTranslate] = useState(false);
|
||||||
|
return (
|
||||||
<div class="sheet">
|
<div class="sheet">
|
||||||
<header>
|
<header class="header-grid">
|
||||||
<h2>Media description</h2>
|
<h2>Media description</h2>
|
||||||
|
<div class="header-side">
|
||||||
|
<Menu
|
||||||
|
align="end"
|
||||||
|
menuButton={
|
||||||
|
<button type="button" class="plain4">
|
||||||
|
<Icon icon="more" alt="More" size="xl" />
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<MenuItem
|
||||||
|
disabled={forceTranslate}
|
||||||
|
onClick={() => {
|
||||||
|
setForceTranslate(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon icon="translate" />
|
||||||
|
<span>Translate</span>
|
||||||
|
</MenuItem>
|
||||||
|
</Menu>
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<main>
|
<main>
|
||||||
<p
|
<p
|
||||||
|
@ -244,13 +276,13 @@ function MediaModal({
|
||||||
whiteSpace: 'pre-wrap',
|
whiteSpace: 'pre-wrap',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{showMediaAlt}
|
{alt}
|
||||||
</p>
|
</p>
|
||||||
|
{forceTranslate && (
|
||||||
|
<TranslationBlock forceTranslate={forceTranslate} text={alt} />
|
||||||
|
)}
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,7 @@ import Modal from '../components/modal';
|
||||||
import NameText from '../components/name-text';
|
import NameText from '../components/name-text';
|
||||||
import { api } from '../utils/api';
|
import { api } from '../utils/api';
|
||||||
import enhanceContent from '../utils/enhance-content';
|
import enhanceContent from '../utils/enhance-content';
|
||||||
|
import getTranslateTargetLanguage from '../utils/get-translate-target-language';
|
||||||
import handleContentLinks from '../utils/handle-content-links';
|
import handleContentLinks from '../utils/handle-content-links';
|
||||||
import htmlContentLength from '../utils/html-content-length';
|
import htmlContentLength from '../utils/html-content-length';
|
||||||
import niceDateTime from '../utils/nice-date-time';
|
import niceDateTime from '../utils/nice-date-time';
|
||||||
|
@ -35,6 +36,7 @@ import Link from './link';
|
||||||
import Media from './media';
|
import Media from './media';
|
||||||
import MenuLink from './MenuLink';
|
import MenuLink from './MenuLink';
|
||||||
import RelativeTime from './relative-time';
|
import RelativeTime from './relative-time';
|
||||||
|
import TranslationBlock from './translation-block';
|
||||||
|
|
||||||
const throttle = pThrottle({
|
const throttle = pThrottle({
|
||||||
limit: 1,
|
limit: 1,
|
||||||
|
@ -66,6 +68,7 @@ function Status({
|
||||||
skeleton,
|
skeleton,
|
||||||
readOnly,
|
readOnly,
|
||||||
contentTextWeight,
|
contentTextWeight,
|
||||||
|
enableTranslate,
|
||||||
}) {
|
}) {
|
||||||
if (skeleton) {
|
if (skeleton) {
|
||||||
return (
|
return (
|
||||||
|
@ -194,6 +197,10 @@ function Status({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const [forceTranslate, setForceTranslate] = useState(false);
|
||||||
|
const targetLanguage = getTranslateTargetLanguage(true);
|
||||||
|
if (!snapStates.settings.contentTranslation) enableTranslate = false;
|
||||||
|
|
||||||
const [showEdited, setShowEdited] = useState(false);
|
const [showEdited, setShowEdited] = useState(false);
|
||||||
|
|
||||||
const spoilerContentRef = useRef(null);
|
const spoilerContentRef = useRef(null);
|
||||||
|
@ -450,6 +457,17 @@ function Status({
|
||||||
<Icon icon="link" />
|
<Icon icon="link" />
|
||||||
<span>Copy link to post</span>
|
<span>Copy link to post</span>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
{enableTranslate && (
|
||||||
|
<MenuItem
|
||||||
|
disabled={forceTranslate}
|
||||||
|
onClick={() => {
|
||||||
|
setForceTranslate(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon icon="translate" />
|
||||||
|
<span>Translate</span>
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
{navigator?.share &&
|
{navigator?.share &&
|
||||||
navigator?.canShare?.({
|
navigator?.canShare?.({
|
||||||
url,
|
url,
|
||||||
|
@ -770,6 +788,25 @@ function Status({
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{((enableTranslate &&
|
||||||
|
!!content.trim() &&
|
||||||
|
language &&
|
||||||
|
language !== targetLanguage) ||
|
||||||
|
forceTranslate) && (
|
||||||
|
<TranslationBlock
|
||||||
|
forceTranslate={forceTranslate}
|
||||||
|
sourceLanguage={language}
|
||||||
|
text={
|
||||||
|
(spoilerText ? `${spoilerText}\n\n` : '') +
|
||||||
|
getHTMLText(content) +
|
||||||
|
(poll?.options?.length
|
||||||
|
? `\n\nPoll:\n${poll.options
|
||||||
|
.map((option) => `- ${option.title}`)
|
||||||
|
.join('\n')}`
|
||||||
|
: '')
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{!spoilerText && sensitive && !!mediaAttachments.length && (
|
{!spoilerText && sensitive && !!mediaAttachments.length && (
|
||||||
<button
|
<button
|
||||||
class={`plain spoiler ${showSpoiler ? 'spoiling' : ''}`}
|
class={`plain spoiler ${showSpoiler ? 'spoiling' : ''}`}
|
||||||
|
@ -1480,4 +1517,16 @@ function _unfurlMastodonLink(instance, url) {
|
||||||
|
|
||||||
const unfurlMastodonLink = throttle(_unfurlMastodonLink);
|
const unfurlMastodonLink = throttle(_unfurlMastodonLink);
|
||||||
|
|
||||||
|
const div = document.createElement('div');
|
||||||
|
function getHTMLText(html) {
|
||||||
|
if (!html) return 0;
|
||||||
|
div.innerHTML = html
|
||||||
|
.replace(/<\/p>/g, '</p>\n\n')
|
||||||
|
.replace(/<\/li>/g, '</li>\n');
|
||||||
|
div.querySelectorAll('br').forEach((br) => {
|
||||||
|
br.replaceWith('\n');
|
||||||
|
});
|
||||||
|
return div.innerText.replace(/[\r\n]{3,}/g, '\n\n').trim();
|
||||||
|
}
|
||||||
|
|
||||||
export default memo(Status);
|
export default memo(Status);
|
||||||
|
|
86
src/components/translation-block.css
Normal file
86
src/components/translation-block.css
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
.status-translation-block {
|
||||||
|
margin: 8px 0 0;
|
||||||
|
padding: 0;
|
||||||
|
font-size: 90%;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
.status-translation-block summary {
|
||||||
|
list-style: none;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
.status-translation-block summary::-webkit-details-marker {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.status-translation-block summary button {
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid var(--outline-color);
|
||||||
|
padding: 8px;
|
||||||
|
background-color: var(--bg-color);
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-insignificant-color);
|
||||||
|
}
|
||||||
|
.status-translation-block summary button:is(:hover, :focus) {
|
||||||
|
color: var(--text-color);
|
||||||
|
filter: none !important;
|
||||||
|
}
|
||||||
|
.status-translation-block details:not([open]) .detected {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
/* .status-translation-block details summary button:active, */
|
||||||
|
.status-translation-block details[open] summary button {
|
||||||
|
/* color: var(--text-color); */
|
||||||
|
/* background-color: var(--bg-faded-color); */
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
border-bottom: 0;
|
||||||
|
margin-bottom: -1px;
|
||||||
|
background-image: linear-gradient(
|
||||||
|
to top left,
|
||||||
|
var(--bg-color) 50%,
|
||||||
|
var(--bg-faded-blur-color)
|
||||||
|
);
|
||||||
|
box-shadow: inset 0 0 0 1px var(--bg-color);
|
||||||
|
}
|
||||||
|
.status-translation-block .translated-block {
|
||||||
|
border: 1px solid var(--outline-color);
|
||||||
|
line-height: 1.3;
|
||||||
|
border-radius: 0 8px 8px 8px;
|
||||||
|
margin: 0;
|
||||||
|
padding: 8px;
|
||||||
|
background-color: var(--bg-color);
|
||||||
|
background-image: linear-gradient(
|
||||||
|
to bottom right,
|
||||||
|
var(--bg-color),
|
||||||
|
var(--bg-faded-blur-color)
|
||||||
|
);
|
||||||
|
white-space: pre-wrap;
|
||||||
|
box-shadow: inset 0 0 0 1px var(--bg-color),
|
||||||
|
0 1px 5px -2px var(--drop-shadow-color);
|
||||||
|
text-shadow: 0 1px var(--bg-color);
|
||||||
|
}
|
||||||
|
.status-translation-block .translated-block .translation-info * {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
.status-translation-block .translated-source-select {
|
||||||
|
appearance: none;
|
||||||
|
display: inline-block;
|
||||||
|
margin: 0;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: var(--bg-faded-color);
|
||||||
|
color: inherit;
|
||||||
|
width: min-content;
|
||||||
|
}
|
||||||
|
.status-translation-block .translated-block output {
|
||||||
|
display: block;
|
||||||
|
margin-top: 1em;
|
||||||
|
}
|
||||||
|
.status-translation-block
|
||||||
|
.translated-block
|
||||||
|
output.translated-pronunciation-content {
|
||||||
|
opacity: 0.75;
|
||||||
|
padding-bottom: 1em;
|
||||||
|
border-top: var(--hairline-width) solid var(--bg-color);
|
||||||
|
border-bottom: var(--hairline-width) solid var(--outline-color);
|
||||||
|
}
|
154
src/components/translation-block.jsx
Normal file
154
src/components/translation-block.jsx
Normal file
|
@ -0,0 +1,154 @@
|
||||||
|
import './translation-block.css';
|
||||||
|
|
||||||
|
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 Icon from './icon';
|
||||||
|
import Loader from './loader';
|
||||||
|
|
||||||
|
function TranslationBlock({
|
||||||
|
forceTranslate,
|
||||||
|
sourceLanguage,
|
||||||
|
onTranslate,
|
||||||
|
text = '',
|
||||||
|
}) {
|
||||||
|
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 = (source, target) => {
|
||||||
|
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 () => {
|
||||||
|
setUIState('loading');
|
||||||
|
const { content, detectedSourceLanguage, provider, ...props } =
|
||||||
|
await onTranslate(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');
|
||||||
|
detailsRef.current.open = true;
|
||||||
|
detailsRef.current.scrollIntoView({
|
||||||
|
behavior: 'smooth',
|
||||||
|
block: 'nearest',
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.error(result);
|
||||||
|
setUIState('error');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (forceTranslate) {
|
||||||
|
translate();
|
||||||
|
}
|
||||||
|
}, [forceTranslate]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="status-translation-block">
|
||||||
|
<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…'
|
||||||
|
: sourceLanguage && !detectedLang
|
||||||
|
? `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 && (
|
||||||
|
<>
|
||||||
|
{!!pronunciationContent && (
|
||||||
|
<output class="translated-pronunciation-content">
|
||||||
|
{pronunciationContent}
|
||||||
|
</output>
|
||||||
|
)}
|
||||||
|
<output class="translated-content" lang={targetLang}>
|
||||||
|
{translatedContent}
|
||||||
|
</output>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TranslationBlock;
|
534
src/data/lingva-source-languages.json
Normal file
534
src/data/lingva-source-languages.json
Normal file
|
@ -0,0 +1,534 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"code": "auto",
|
||||||
|
"name": "Detect"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "af",
|
||||||
|
"name": "Afrikaans"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "sq",
|
||||||
|
"name": "Albanian"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "am",
|
||||||
|
"name": "Amharic"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "ar",
|
||||||
|
"name": "Arabic"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "hy",
|
||||||
|
"name": "Armenian"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "as",
|
||||||
|
"name": "Assamese"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "ay",
|
||||||
|
"name": "Aymara"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "az",
|
||||||
|
"name": "Azerbaijani"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "bm",
|
||||||
|
"name": "Bambara"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "eu",
|
||||||
|
"name": "Basque"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "be",
|
||||||
|
"name": "Belarusian"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "bn",
|
||||||
|
"name": "Bengali"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "bho",
|
||||||
|
"name": "Bhojpuri"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "bs",
|
||||||
|
"name": "Bosnian"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "bg",
|
||||||
|
"name": "Bulgarian"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "ca",
|
||||||
|
"name": "Catalan"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "ceb",
|
||||||
|
"name": "Cebuano"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "ny",
|
||||||
|
"name": "Chichewa"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "zh",
|
||||||
|
"name": "Chinese"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "co",
|
||||||
|
"name": "Corsican"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "hr",
|
||||||
|
"name": "Croatian"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "cs",
|
||||||
|
"name": "Czech"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "da",
|
||||||
|
"name": "Danish"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "dv",
|
||||||
|
"name": "Dhivehi"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "doi",
|
||||||
|
"name": "Dogri"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "nl",
|
||||||
|
"name": "Dutch"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "en",
|
||||||
|
"name": "English"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "eo",
|
||||||
|
"name": "Esperanto"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "et",
|
||||||
|
"name": "Estonian"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "ee",
|
||||||
|
"name": "Ewe"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "tl",
|
||||||
|
"name": "Filipino"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "fi",
|
||||||
|
"name": "Finnish"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "fr",
|
||||||
|
"name": "French"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "fy",
|
||||||
|
"name": "Frisian"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "gl",
|
||||||
|
"name": "Galician"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "ka",
|
||||||
|
"name": "Georgian"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "de",
|
||||||
|
"name": "German"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "el",
|
||||||
|
"name": "Greek"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "gn",
|
||||||
|
"name": "Guarani"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "gu",
|
||||||
|
"name": "Gujarati"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "ht",
|
||||||
|
"name": "Haitian Creole"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "ha",
|
||||||
|
"name": "Hausa"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "haw",
|
||||||
|
"name": "Hawaiian"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "iw",
|
||||||
|
"name": "Hebrew"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "hi",
|
||||||
|
"name": "Hindi"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "hmn",
|
||||||
|
"name": "Hmong"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "hu",
|
||||||
|
"name": "Hungarian"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "is",
|
||||||
|
"name": "Icelandic"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "ig",
|
||||||
|
"name": "Igbo"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "ilo",
|
||||||
|
"name": "Ilocano"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "id",
|
||||||
|
"name": "Indonesian"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "ga",
|
||||||
|
"name": "Irish"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "it",
|
||||||
|
"name": "Italian"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "ja",
|
||||||
|
"name": "Japanese"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "jw",
|
||||||
|
"name": "Javanese"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "kn",
|
||||||
|
"name": "Kannada"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "kk",
|
||||||
|
"name": "Kazakh"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "km",
|
||||||
|
"name": "Khmer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "rw",
|
||||||
|
"name": "Kinyarwanda"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "gom",
|
||||||
|
"name": "Konkani"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "ko",
|
||||||
|
"name": "Korean"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "kri",
|
||||||
|
"name": "Krio"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "ku",
|
||||||
|
"name": "Kurdish (Kurmanji)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "ckb",
|
||||||
|
"name": "Kurdish (Sorani)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "ky",
|
||||||
|
"name": "Kyrgyz"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "lo",
|
||||||
|
"name": "Lao"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "la",
|
||||||
|
"name": "Latin"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "lv",
|
||||||
|
"name": "Latvian"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "ln",
|
||||||
|
"name": "Lingala"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "lt",
|
||||||
|
"name": "Lithuanian"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "lg",
|
||||||
|
"name": "Luganda"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "lb",
|
||||||
|
"name": "Luxembourgish"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "mk",
|
||||||
|
"name": "Macedonian"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "mai",
|
||||||
|
"name": "Maithili"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "mg",
|
||||||
|
"name": "Malagasy"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "ms",
|
||||||
|
"name": "Malay"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "ml",
|
||||||
|
"name": "Malayalam"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "mt",
|
||||||
|
"name": "Maltese"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "mi",
|
||||||
|
"name": "Maori"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "mr",
|
||||||
|
"name": "Marathi"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "mni-Mtei",
|
||||||
|
"name": "Meiteilon (Manipuri)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "lus",
|
||||||
|
"name": "Mizo"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "mn",
|
||||||
|
"name": "Mongolian"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "my",
|
||||||
|
"name": "Myanmar (Burmese)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "ne",
|
||||||
|
"name": "Nepali"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "no",
|
||||||
|
"name": "Norwegian"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "or",
|
||||||
|
"name": "Odia (Oriya)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "om",
|
||||||
|
"name": "Oromo"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "ps",
|
||||||
|
"name": "Pashto"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "fa",
|
||||||
|
"name": "Persian"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "pl",
|
||||||
|
"name": "Polish"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "pt",
|
||||||
|
"name": "Portuguese"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "pa",
|
||||||
|
"name": "Punjabi"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "qu",
|
||||||
|
"name": "Quechua"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "ro",
|
||||||
|
"name": "Romanian"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "ru",
|
||||||
|
"name": "Russian"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "sm",
|
||||||
|
"name": "Samoan"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "sa",
|
||||||
|
"name": "Sanskrit"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "gd",
|
||||||
|
"name": "Scots Gaelic"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "nso",
|
||||||
|
"name": "Sepedi"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "sr",
|
||||||
|
"name": "Serbian"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "st",
|
||||||
|
"name": "Sesotho"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "sn",
|
||||||
|
"name": "Shona"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "sd",
|
||||||
|
"name": "Sindhi"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "si",
|
||||||
|
"name": "Sinhala"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "sk",
|
||||||
|
"name": "Slovak"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "sl",
|
||||||
|
"name": "Slovenian"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "so",
|
||||||
|
"name": "Somali"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "es",
|
||||||
|
"name": "Spanish"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "su",
|
||||||
|
"name": "Sundanese"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "sw",
|
||||||
|
"name": "Swahili"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "sv",
|
||||||
|
"name": "Swedish"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "tg",
|
||||||
|
"name": "Tajik"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "ta",
|
||||||
|
"name": "Tamil"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "tt",
|
||||||
|
"name": "Tatar"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "te",
|
||||||
|
"name": "Telugu"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "th",
|
||||||
|
"name": "Thai"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "ti",
|
||||||
|
"name": "Tigrinya"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "ts",
|
||||||
|
"name": "Tsonga"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "tr",
|
||||||
|
"name": "Turkish"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "tk",
|
||||||
|
"name": "Turkmen"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "ak",
|
||||||
|
"name": "Twi"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "uk",
|
||||||
|
"name": "Ukrainian"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "ur",
|
||||||
|
"name": "Urdu"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "ug",
|
||||||
|
"name": "Uyghur"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "uz",
|
||||||
|
"name": "Uzbek"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "vi",
|
||||||
|
"name": "Vietnamese"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "cy",
|
||||||
|
"name": "Welsh"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "xh",
|
||||||
|
"name": "Xhosa"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "yi",
|
||||||
|
"name": "Yiddish"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "yo",
|
||||||
|
"name": "Yoruba"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "zu",
|
||||||
|
"name": "Zulu"
|
||||||
|
}
|
||||||
|
]
|
534
src/data/lingva-target-languages.json
Normal file
534
src/data/lingva-target-languages.json
Normal file
|
@ -0,0 +1,534 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"code": "af",
|
||||||
|
"name": "Afrikaans"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "sq",
|
||||||
|
"name": "Albanian"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "am",
|
||||||
|
"name": "Amharic"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "ar",
|
||||||
|
"name": "Arabic"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "hy",
|
||||||
|
"name": "Armenian"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "as",
|
||||||
|
"name": "Assamese"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "ay",
|
||||||
|
"name": "Aymara"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "az",
|
||||||
|
"name": "Azerbaijani"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "bm",
|
||||||
|
"name": "Bambara"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "eu",
|
||||||
|
"name": "Basque"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "be",
|
||||||
|
"name": "Belarusian"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "bn",
|
||||||
|
"name": "Bengali"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "bho",
|
||||||
|
"name": "Bhojpuri"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "bs",
|
||||||
|
"name": "Bosnian"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "bg",
|
||||||
|
"name": "Bulgarian"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "ca",
|
||||||
|
"name": "Catalan"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "ceb",
|
||||||
|
"name": "Cebuano"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "ny",
|
||||||
|
"name": "Chichewa"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "zh",
|
||||||
|
"name": "Chinese"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "zh_HANT",
|
||||||
|
"name": "Chinese (Traditional)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "co",
|
||||||
|
"name": "Corsican"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "hr",
|
||||||
|
"name": "Croatian"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "cs",
|
||||||
|
"name": "Czech"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "da",
|
||||||
|
"name": "Danish"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "dv",
|
||||||
|
"name": "Dhivehi"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "doi",
|
||||||
|
"name": "Dogri"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "nl",
|
||||||
|
"name": "Dutch"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "en",
|
||||||
|
"name": "English"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "eo",
|
||||||
|
"name": "Esperanto"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "et",
|
||||||
|
"name": "Estonian"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "ee",
|
||||||
|
"name": "Ewe"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "tl",
|
||||||
|
"name": "Filipino"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "fi",
|
||||||
|
"name": "Finnish"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "fr",
|
||||||
|
"name": "French"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "fy",
|
||||||
|
"name": "Frisian"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "gl",
|
||||||
|
"name": "Galician"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "ka",
|
||||||
|
"name": "Georgian"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "de",
|
||||||
|
"name": "German"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "el",
|
||||||
|
"name": "Greek"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "gn",
|
||||||
|
"name": "Guarani"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "gu",
|
||||||
|
"name": "Gujarati"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "ht",
|
||||||
|
"name": "Haitian Creole"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "ha",
|
||||||
|
"name": "Hausa"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "haw",
|
||||||
|
"name": "Hawaiian"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "iw",
|
||||||
|
"name": "Hebrew"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "hi",
|
||||||
|
"name": "Hindi"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "hmn",
|
||||||
|
"name": "Hmong"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "hu",
|
||||||
|
"name": "Hungarian"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "is",
|
||||||
|
"name": "Icelandic"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "ig",
|
||||||
|
"name": "Igbo"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "ilo",
|
||||||
|
"name": "Ilocano"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "id",
|
||||||
|
"name": "Indonesian"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "ga",
|
||||||
|
"name": "Irish"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "it",
|
||||||
|
"name": "Italian"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "ja",
|
||||||
|
"name": "Japanese"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "jw",
|
||||||
|
"name": "Javanese"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "kn",
|
||||||
|
"name": "Kannada"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "kk",
|
||||||
|
"name": "Kazakh"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "km",
|
||||||
|
"name": "Khmer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "rw",
|
||||||
|
"name": "Kinyarwanda"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "gom",
|
||||||
|
"name": "Konkani"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "ko",
|
||||||
|
"name": "Korean"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "kri",
|
||||||
|
"name": "Krio"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "ku",
|
||||||
|
"name": "Kurdish (Kurmanji)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "ckb",
|
||||||
|
"name": "Kurdish (Sorani)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "ky",
|
||||||
|
"name": "Kyrgyz"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "lo",
|
||||||
|
"name": "Lao"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "la",
|
||||||
|
"name": "Latin"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "lv",
|
||||||
|
"name": "Latvian"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "ln",
|
||||||
|
"name": "Lingala"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "lt",
|
||||||
|
"name": "Lithuanian"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "lg",
|
||||||
|
"name": "Luganda"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "lb",
|
||||||
|
"name": "Luxembourgish"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "mk",
|
||||||
|
"name": "Macedonian"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "mai",
|
||||||
|
"name": "Maithili"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "mg",
|
||||||
|
"name": "Malagasy"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "ms",
|
||||||
|
"name": "Malay"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "ml",
|
||||||
|
"name": "Malayalam"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "mt",
|
||||||
|
"name": "Maltese"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "mi",
|
||||||
|
"name": "Maori"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "mr",
|
||||||
|
"name": "Marathi"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "mni-Mtei",
|
||||||
|
"name": "Meiteilon (Manipuri)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "lus",
|
||||||
|
"name": "Mizo"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "mn",
|
||||||
|
"name": "Mongolian"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "my",
|
||||||
|
"name": "Myanmar (Burmese)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "ne",
|
||||||
|
"name": "Nepali"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "no",
|
||||||
|
"name": "Norwegian"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "or",
|
||||||
|
"name": "Odia (Oriya)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "om",
|
||||||
|
"name": "Oromo"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "ps",
|
||||||
|
"name": "Pashto"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "fa",
|
||||||
|
"name": "Persian"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "pl",
|
||||||
|
"name": "Polish"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "pt",
|
||||||
|
"name": "Portuguese"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "pa",
|
||||||
|
"name": "Punjabi"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "qu",
|
||||||
|
"name": "Quechua"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "ro",
|
||||||
|
"name": "Romanian"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "ru",
|
||||||
|
"name": "Russian"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "sm",
|
||||||
|
"name": "Samoan"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "sa",
|
||||||
|
"name": "Sanskrit"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "gd",
|
||||||
|
"name": "Scots Gaelic"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "nso",
|
||||||
|
"name": "Sepedi"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "sr",
|
||||||
|
"name": "Serbian"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "st",
|
||||||
|
"name": "Sesotho"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "sn",
|
||||||
|
"name": "Shona"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "sd",
|
||||||
|
"name": "Sindhi"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "si",
|
||||||
|
"name": "Sinhala"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "sk",
|
||||||
|
"name": "Slovak"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "sl",
|
||||||
|
"name": "Slovenian"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "so",
|
||||||
|
"name": "Somali"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "es",
|
||||||
|
"name": "Spanish"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "su",
|
||||||
|
"name": "Sundanese"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "sw",
|
||||||
|
"name": "Swahili"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "sv",
|
||||||
|
"name": "Swedish"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "tg",
|
||||||
|
"name": "Tajik"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "ta",
|
||||||
|
"name": "Tamil"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "tt",
|
||||||
|
"name": "Tatar"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "te",
|
||||||
|
"name": "Telugu"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "th",
|
||||||
|
"name": "Thai"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "ti",
|
||||||
|
"name": "Tigrinya"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "ts",
|
||||||
|
"name": "Tsonga"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "tr",
|
||||||
|
"name": "Turkish"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "tk",
|
||||||
|
"name": "Turkmen"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "ak",
|
||||||
|
"name": "Twi"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "uk",
|
||||||
|
"name": "Ukrainian"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "ur",
|
||||||
|
"name": "Urdu"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "ug",
|
||||||
|
"name": "Uyghur"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "uz",
|
||||||
|
"name": "Uzbek"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "vi",
|
||||||
|
"name": "Vietnamese"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "cy",
|
||||||
|
"name": "Welsh"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "xh",
|
||||||
|
"name": "Xhosa"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "yi",
|
||||||
|
"name": "Yiddish"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "yo",
|
||||||
|
"name": "Yoruba"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "zu",
|
||||||
|
"name": "Zulu"
|
||||||
|
}
|
||||||
|
]
|
|
@ -59,6 +59,10 @@
|
||||||
#settings-container section > ul > li > div:last-child {
|
#settings-container section > ul > li > div:last-child {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
#settings-container section > ul > li .sub-section {
|
||||||
|
text-align: left !important;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
#settings-container div,
|
#settings-container div,
|
||||||
#settings-container div > * {
|
#settings-container div > * {
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
|
|
|
@ -10,7 +10,10 @@ import Icon from '../components/icon';
|
||||||
import Link from '../components/link';
|
import Link from '../components/link';
|
||||||
import NameText from '../components/name-text';
|
import NameText from '../components/name-text';
|
||||||
import RelativeTime from '../components/relative-time';
|
import RelativeTime from '../components/relative-time';
|
||||||
|
import targetLanguages from '../data/lingva-target-languages';
|
||||||
import { api } from '../utils/api';
|
import { api } from '../utils/api';
|
||||||
|
import getTranslateTargetLanguage from '../utils/get-translate-target-language';
|
||||||
|
import localeCode2Text from '../utils/localeCode2Text';
|
||||||
import states from '../utils/states';
|
import states from '../utils/states';
|
||||||
import store from '../utils/store';
|
import store from '../utils/store';
|
||||||
|
|
||||||
|
@ -33,6 +36,11 @@ function Settings({ onClose }) {
|
||||||
|
|
||||||
const [_, reload] = useReducer((x) => x + 1, 0);
|
const [_, reload] = useReducer((x) => x + 1, 0);
|
||||||
|
|
||||||
|
const targetLanguage =
|
||||||
|
snapStates.settings.contentTranslationTargetLanguage || null;
|
||||||
|
const systemTargetLanguage = getTranslateTargetLanguage();
|
||||||
|
const systemTargetLanguageText = localeCode2Text(systemTargetLanguage);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div id="settings-container" class="sheet" tabIndex="-1">
|
<div id="settings-container" class="sheet" tabIndex="-1">
|
||||||
<main>
|
<main>
|
||||||
|
@ -240,6 +248,53 @@ function Settings({ onClose }) {
|
||||||
Boosts carousel (experimental)
|
Boosts carousel (experimental)
|
||||||
</label>
|
</label>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={snapStates.settings.contentTranslation}
|
||||||
|
onChange={(e) => {
|
||||||
|
states.settings.contentTranslation = e.target.checked;
|
||||||
|
}}
|
||||||
|
/>{' '}
|
||||||
|
Post translation (experimental)
|
||||||
|
</label>
|
||||||
|
{snapStates.settings.contentTranslation && (
|
||||||
|
<div class="sub-section">
|
||||||
|
<label>
|
||||||
|
Translate to{' '}
|
||||||
|
<select
|
||||||
|
value={targetLanguage}
|
||||||
|
onChange={(e) => {
|
||||||
|
states.settings.contentTranslationTargetLanguage =
|
||||||
|
e.target.value || null;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<option value="">
|
||||||
|
System language ({systemTargetLanguageText})
|
||||||
|
</option>
|
||||||
|
<option disabled>──────────</option>
|
||||||
|
{targetLanguages.map((lang) => (
|
||||||
|
<option value={lang.code}>{lang.name}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<p>
|
||||||
|
<small>
|
||||||
|
Note: This feature uses an external API to translate,
|
||||||
|
powered by{' '}
|
||||||
|
<a
|
||||||
|
href="https://github.com/thedaviddelta/lingva-translate"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
Lingva Translate
|
||||||
|
</a>
|
||||||
|
.
|
||||||
|
</small>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
<h2>Hidden features</h2>
|
<h2>Hidden features</h2>
|
||||||
|
|
|
@ -624,6 +624,7 @@ function StatusPage() {
|
||||||
instance={instance}
|
instance={instance}
|
||||||
withinContext
|
withinContext
|
||||||
size="l"
|
size="l"
|
||||||
|
enableTranslate
|
||||||
/>
|
/>
|
||||||
</InView>
|
</InView>
|
||||||
{uiState !== 'loading' && !authenticated ? (
|
{uiState !== 'loading' && !authenticated ? (
|
||||||
|
@ -700,6 +701,7 @@ function StatusPage() {
|
||||||
instance={instance}
|
instance={instance}
|
||||||
withinContext
|
withinContext
|
||||||
size={thread || ancestor ? 'm' : 's'}
|
size={thread || ancestor ? 'm' : 's'}
|
||||||
|
enableTranslate
|
||||||
/>
|
/>
|
||||||
{/* {replies?.length > LIMIT && (
|
{/* {replies?.length > LIMIT && (
|
||||||
<div class="replies-link">
|
<div class="replies-link">
|
||||||
|
@ -880,6 +882,7 @@ function SubComments({ hasManyStatuses, replies, instance, hasParentThread }) {
|
||||||
instance={instance}
|
instance={instance}
|
||||||
withinContext
|
withinContext
|
||||||
size="s"
|
size="s"
|
||||||
|
enableTranslate
|
||||||
/>
|
/>
|
||||||
{!r.replies?.length && r.repliesCount > 0 && (
|
{!r.replies?.length && r.repliesCount > 0 && (
|
||||||
<div class="replies-link">
|
<div class="replies-link">
|
||||||
|
|
24
src/utils/get-translate-target-language.jsx
Normal file
24
src/utils/get-translate-target-language.jsx
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import { match } from '@formatjs/intl-localematcher';
|
||||||
|
|
||||||
|
import translationTargetLanguages from '../data/lingva-target-languages';
|
||||||
|
|
||||||
|
import states from './states';
|
||||||
|
|
||||||
|
function getTranslateTargetLanguage(fromSettings = false) {
|
||||||
|
if (fromSettings) {
|
||||||
|
const { contentTranslationTargetLanguage } = states.settings;
|
||||||
|
if (contentTranslationTargetLanguage) {
|
||||||
|
return contentTranslationTargetLanguage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return match(
|
||||||
|
[
|
||||||
|
new Intl.DateTimeFormat().resolvedOptions().locale,
|
||||||
|
...navigator.languages,
|
||||||
|
],
|
||||||
|
translationTargetLanguages.map((l) => l.code.replace('_', '-')), // The underscore will fail Intl.Locale inside `match`
|
||||||
|
'en',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default getTranslateTargetLanguage;
|
5
src/utils/localeCode2Text.jsx
Normal file
5
src/utils/localeCode2Text.jsx
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
export default function localeCode2Text(code) {
|
||||||
|
return new Intl.DisplayNames(navigator.languages, {
|
||||||
|
type: 'language',
|
||||||
|
}).of(code);
|
||||||
|
}
|
|
@ -42,6 +42,10 @@ const states = proxy({
|
||||||
shortcutsColumnsMode:
|
shortcutsColumnsMode:
|
||||||
store.account.get('settings-shortcutsColumnsMode') ?? false,
|
store.account.get('settings-shortcutsColumnsMode') ?? false,
|
||||||
boostsCarousel: store.account.get('settings-boostsCarousel') ?? true,
|
boostsCarousel: store.account.get('settings-boostsCarousel') ?? true,
|
||||||
|
contentTranslation:
|
||||||
|
store.account.get('settings-contentTranslation') ?? true,
|
||||||
|
contentTranslationTargetLanguage:
|
||||||
|
store.account.get('settings-contentTranslationTargetLanguage') || null,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -63,6 +67,12 @@ subscribe(states, (v) => {
|
||||||
if (path.join('.') === 'settings.shortcutsViewMode') {
|
if (path.join('.') === 'settings.shortcutsViewMode') {
|
||||||
store.account.set('settings-shortcutsViewMode', value);
|
store.account.set('settings-shortcutsViewMode', value);
|
||||||
}
|
}
|
||||||
|
if (path.join('.') === 'settings.contentTranslation') {
|
||||||
|
store.account.set('settings-contentTranslation', !!value);
|
||||||
|
}
|
||||||
|
if (path.join('.') === 'settings.contentTranslationTargetLanguage') {
|
||||||
|
store.account.set('settings-contentTranslationTargetLanguage', value);
|
||||||
|
}
|
||||||
if (path?.[0] === 'shortcuts') {
|
if (path?.[0] === 'shortcuts') {
|
||||||
store.account.set('shortcuts', states.shortcuts);
|
store.account.set('shortcuts', states.shortcuts);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue