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 urlRegex from '../data/url-regex';
import { api } from '../utils/api';
import { langDetector } from '../utils/browser-translator';
import db from '../utils/db';
import emojifyText from '../utils/emojify-text';
import i18nDuration from '../utils/i18n-duration';
@ -1875,6 +1876,12 @@ const getCustomEmojis = pmem(_getCustomEmojis, {
});
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 langs = detectAll(text);
if (langs?.length) {

View file

@ -41,6 +41,7 @@ import Modal from '../components/modal';
import NameText from '../components/name-text';
import Poll from '../components/poll';
import { api } from '../utils/api';
import { langDetector } from '../utils/browser-translator';
import emojifyText from '../utils/emojify-text';
import enhanceContent from '../utils/enhance-content';
import FilterContext from '../utils/filter-context';
@ -253,7 +254,6 @@ const SIZE_CLASS = {
};
const detectLang = pmem(async (text) => {
const { detectAll } = await import('tinyld/light');
text = text?.trim();
// Ref: https://github.com/komodojp/tinyld/blob/develop/docs/benchmark.md
@ -261,7 +261,29 @@ const detectLang = pmem(async (text) => {
if (text?.length > 500) {
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);
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];
if (lang?.lang && lang?.accuracy > 0.5) {
// If > 50% accurate, use it

View file

@ -6,6 +6,10 @@ import pThrottle from 'p-throttle';
import { useEffect, useRef, useState } from 'preact/hooks';
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 localeCode2Text from '../utils/localeCode2Text';
import pmem from '../utils/pmem';
@ -95,7 +99,22 @@ function TranslationBlock({
const apiSourceLang = useRef('auto');
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 () => {

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,
};
}
};