Experiment using browser's built in translation API

Spec: https://webmachinelearning.github.io/translation-api/

API may change in the future
This commit is contained in:
Lim Chee Aun 2025-03-01 17:52:27 +08:00
parent 23e9d034e9
commit 945d2ac206
5 changed files with 395 additions and 240 deletions

View file

@ -28,6 +28,7 @@ import Menu2 from '../components/menu2';
import supportedLanguages from '../data/status-supported-languages'; import supportedLanguages from '../data/status-supported-languages';
import urlRegex from '../data/url-regex'; import urlRegex from '../data/url-regex';
import { api } from '../utils/api'; import { api } from '../utils/api';
import { langDetector } from '../utils/browser-translator';
import db from '../utils/db'; import db from '../utils/db';
import emojifyText from '../utils/emojify-text'; import emojifyText from '../utils/emojify-text';
import i18nDuration from '../utils/i18n-duration'; import i18nDuration from '../utils/i18n-duration';
@ -1875,6 +1876,12 @@ const getCustomEmojis = pmem(_getCustomEmojis, {
}); });
const detectLangs = async (text) => { const detectLangs = async (text) => {
if (langDetector) {
const langs = await langDetector.detect(text);
if (langs?.length) {
return langs.slice(0, 2).map((lang) => lang.detectedLanguage);
}
}
const { detectAll } = await import('tinyld/light'); const { detectAll } = await import('tinyld/light');
const langs = detectAll(text); const langs = detectAll(text);
if (langs?.length) { if (langs?.length) {

View file

@ -41,6 +41,7 @@ import Modal from '../components/modal';
import NameText from '../components/name-text'; import NameText from '../components/name-text';
import Poll from '../components/poll'; import Poll from '../components/poll';
import { api } from '../utils/api'; import { api } from '../utils/api';
import { langDetector } from '../utils/browser-translator';
import emojifyText from '../utils/emojify-text'; import emojifyText from '../utils/emojify-text';
import enhanceContent from '../utils/enhance-content'; import enhanceContent from '../utils/enhance-content';
import FilterContext from '../utils/filter-context'; import FilterContext from '../utils/filter-context';
@ -253,7 +254,6 @@ const SIZE_CLASS = {
}; };
const detectLang = pmem(async (text) => { const detectLang = pmem(async (text) => {
const { detectAll } = await import('tinyld/light');
text = text?.trim(); text = text?.trim();
// Ref: https://github.com/komodojp/tinyld/blob/develop/docs/benchmark.md // Ref: https://github.com/komodojp/tinyld/blob/develop/docs/benchmark.md
@ -261,7 +261,29 @@ const detectLang = pmem(async (text) => {
if (text?.length > 500) { if (text?.length > 500) {
return null; return null;
} }
if (langDetector) {
const langs = await langDetector.detect(text);
console.groupCollapsed(
'💬 DETECTLANG BROWSER',
langs.slice(0, 3).map((l) => l.detectedLanguage),
);
console.log(text, langs.slice(0, 3));
console.groupEnd();
const lang = langs[0];
if (lang?.detectedLanguage && lang?.confidence > 0.5) {
return lang.detectedLanguage;
}
}
const { detectAll } = await import('tinyld/light');
const langs = detectAll(text); const langs = detectAll(text);
console.groupCollapsed(
'💬 DETECTLANG TINYLD',
langs.slice(0, 3).map((l) => l.lang),
);
console.log(text, langs.slice(0, 3));
console.groupEnd();
const lang = langs[0]; const lang = langs[0];
if (lang?.lang && lang?.accuracy > 0.5) { if (lang?.lang && lang?.accuracy > 0.5) {
// If > 50% accurate, use it // If > 50% accurate, use it

View file

@ -6,6 +6,10 @@ 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';
import {
translate as browserTranslate,
supportsBrowserTranslator,
} from '../utils/browser-translator';
import getTranslateTargetLanguage from '../utils/get-translate-target-language'; import getTranslateTargetLanguage from '../utils/get-translate-target-language';
import localeCode2Text from '../utils/localeCode2Text'; import localeCode2Text from '../utils/localeCode2Text';
import pmem from '../utils/pmem'; import pmem from '../utils/pmem';
@ -95,7 +99,22 @@ function TranslationBlock({
const apiSourceLang = useRef('auto'); const apiSourceLang = useRef('auto');
if (!onTranslate) { if (!onTranslate) {
onTranslate = mini ? throttledLingvaTranslate : lingvaTranslate; // onTranslate = supportsBrowserTranslator
// ? browserTranslate
// : mini
// ? throttledLingvaTranslate
// : lingvaTranslate;
onTranslate = async (...args) => {
if (supportsBrowserTranslator) {
const result = await browserTranslate(...args);
if (result && !result.error) {
return result;
}
}
return mini
? await throttledLingvaTranslate(...args)
: await lingvaTranslate(...args);
};
} }
const translate = async () => { const translate = async () => {

476
src/locales/en.po generated

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,107 @@
export const supportsBrowserTranslator =
'ai' in self && 'translator' in self.ai;
// https://developer.chrome.com/docs/ai/language-detection
export let langDetector;
if (supportsBrowserTranslator) {
try {
const languageDetectorCapabilities =
await self.ai.languageDetector.capabilities();
const canDetect = languageDetectorCapabilities.capabilities;
if (canDetect === 'no') {
// The language detector isn't usable.
// return;
}
if (canDetect === 'readily') {
// The language detector can immediately be used.
langDetector = await self.ai.languageDetector.create();
} else {
// The language detector can be used after model download.
langDetector = await self.ai.languageDetector.create({
monitor(m) {
m.addEventListener('downloadprogress', (e) => {
console.log(
`Detector: Downloaded ${e.loaded} of ${e.total} bytes.`,
);
});
},
});
await langDetector.ready;
}
} catch (e) {
console.error(e);
}
}
// https://developer.chrome.com/docs/ai/translator-api
export const translate = async (text, source, target) => {
let detectedSourceLanguage;
const originalSource = source;
if (source === 'auto') {
try {
const results = await langDetector.detect(text);
source = results[0].detectedLanguage;
detectedSourceLanguage = source;
} catch (e) {
console.error(e);
return {
error: e,
};
}
}
console.groupCollapsed(
'💬 BROWSER TRANSLATE',
originalSource,
detectedSourceLanguage,
target,
);
console.log(text);
try {
const translatorCapabilities = await self.ai.translator.capabilities();
const canTranslate = translatorCapabilities.languagePairAvailable(
source,
target,
);
if (canTranslate === 'no') {
console.groupEnd();
return {
error: `Unsupported language pair: ${source} -> ${target}`,
};
}
let translator;
if (canTranslate === 'readily') {
translator = await self.ai.translator.create({
sourceLanguage: source,
targetLanguage: target,
});
} else {
translator = await self.ai.translator.create({
sourceLanguage: source,
targetLanguage: target,
monitor(m) {
m.addEventListener('downloadprogress', (e) => {
console.log(
`Translate ${source} -> ${target}: Downloaded ${e.loaded} of ${e.total} bytes.`,
);
});
},
});
}
const content = await translator.translate(text);
console.log(content);
console.groupEnd();
return {
content,
detectedSourceLanguage,
provider: 'browser',
};
} catch (e) {
console.groupEnd();
console.error(e);
return {
error: e,
};
}
};