From d5ba623882c3ee1c4cf9dd3bb58ca5db887de9ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20S=C5=82uszniak?= Date: Thu, 22 Oct 2026 16:23:43 +0200 Subject: [PATCH 1/6] feat(nlp): add Opus-MT translation task --- apps/nlp/app/_layout.tsx | 7 + apps/nlp/app/index.tsx | 3 + apps/nlp/app/translation/index.tsx | 248 +++++++++++++++++ .../natural-language/07-translation.md | 110 ++++++++ .../08-native-libraries.md | 11 +- .../api/__snapshots__/apiSurface.test.ts.snap | 4 + .../__tests__/api/modelVariants.test.ts | 1 + .../__tests__/tasks/marianTranslation.test.ts | 191 ++++++++++++++ .../scripts/download-libs.js | 4 +- .../src/extensions/nlp/index.ts | 5 +- .../extensions/nlp/tasks/marianTranslation.ts | 249 ++++++++++++++++++ .../extensions/nlp/utils/translationUtils.ts | 42 +++ .../src/hooks/useTranslator.ts | 65 +++++ packages/react-native-executorch/src/index.ts | 2 + .../react-native-executorch/src/models.ts | 68 +++++ 15 files changed, 1002 insertions(+), 8 deletions(-) create mode 100644 apps/nlp/app/translation/index.tsx create mode 100644 docs/docs/02-extensions/natural-language/07-translation.md create mode 100644 packages/react-native-executorch/__tests__/tasks/marianTranslation.test.ts create mode 100644 packages/react-native-executorch/src/extensions/nlp/tasks/marianTranslation.ts create mode 100644 packages/react-native-executorch/src/extensions/nlp/utils/translationUtils.ts create mode 100644 packages/react-native-executorch/src/hooks/useTranslator.ts diff --git a/apps/nlp/app/_layout.tsx b/apps/nlp/app/_layout.tsx index 7d637faa15..69520828e7 100644 --- a/apps/nlp/app/_layout.tsx +++ b/apps/nlp/app/_layout.tsx @@ -48,6 +48,13 @@ export default function Layout() { title: 'Privacy Filter', }} /> + router.navigate('privacy-filter/')}> Privacy Filter + router.navigate('translation/')}> + Translation + router.navigate('llm/')}> LLM diff --git a/apps/nlp/app/translation/index.tsx b/apps/nlp/app/translation/index.tsx new file mode 100644 index 0000000000..1948ea3009 --- /dev/null +++ b/apps/nlp/app/translation/index.tsx @@ -0,0 +1,248 @@ +import React, { useState } from 'react'; +import { + View, + Text, + TextInput, + ScrollView, + StyleSheet, + TouchableOpacity, + KeyboardAvoidingView, + Platform, +} from 'react-native'; +import { useTranslator, models, type MarianTranslatorModel } from 'react-native-executorch'; +import ScreenWrapper from '../../components/ScreenWrapper'; +import { ModelStatus } from '../../components/ModelStatus'; +import { Button } from '../../components/Button'; +import { theme } from '../../theme'; + +/* cspell:disable */ +const SAMPLES = { + en: 'The weather is beautiful today. Shall we go for a walk in the park?\nI will bring something to drink.', + de: 'Das Wetter ist heute wunderschön. Sollen wir im Park spazieren gehen?\nIch bringe etwas zu trinken mit.', + es: 'Hoy hace un tiempo precioso. ¿Vamos a dar un paseo por el parque?\nYo llevaré algo de beber.', + fr: 'Il fait très beau aujourd’hui. On va se promener dans le parc ?\nJ’apporterai quelque chose à boire.', +} as const; +/* cspell:enable */ + +const PAIRS: { label: string; value: MarianTranslatorModel }[] = [ + { label: 'EN → DE', value: models.translation.OPUS_MT_EN_DE.DEFAULT }, + { label: 'DE → EN', value: models.translation.OPUS_MT_DE_EN.DEFAULT }, + { label: 'EN → ES', value: models.translation.OPUS_MT_EN_ES.DEFAULT }, + { label: 'ES → EN', value: models.translation.OPUS_MT_ES_EN.DEFAULT }, + { label: 'EN → FR', value: models.translation.OPUS_MT_EN_FR.DEFAULT }, + { label: 'FR → EN', value: models.translation.OPUS_MT_FR_EN.DEFAULT }, +]; + +const sampleFor = (model: MarianTranslatorModel): string => + SAMPLES[model.sourceLanguage as keyof typeof SAMPLES] ?? ''; + +function TranslationContent() { + const [selected, setSelected] = useState(0); + const active = PAIRS[selected]!; + const { isReady, downloadProgress, error, translate, translateStop } = useTranslator( + active.value + ); + + const [text, setText] = useState(sampleFor(active.value)); + const [output, setOutput] = useState(''); + const [busy, setBusy] = useState(false); + const [runError, setRunError] = useState(null); + const [elapsedMs, setElapsedMs] = useState(null); + + const selectPair = (i: number) => { + if (i === selected) return; + translateStop?.(); + setSelected(i); + setText(sampleFor(PAIRS[i]!.value)); + setOutput(''); + setRunError(null); + setElapsedMs(null); + }; + + const run = async () => { + if (!translate || !text.trim()) return; + setBusy(true); + setRunError(null); + setOutput(''); + setElapsedMs(null); + try { + const startedAt = Date.now(); + const result = await translate(text, (piece) => setOutput((prev) => prev + piece)); + setElapsedMs(Date.now() - startedAt); + setOutput(result); + } catch (e: any) { + const msg = e?.message ?? String(e); + if (!/disposed/i.test(msg)) setRunError(msg); + } finally { + setBusy(false); + } + }; + + return ( + + + + Translation + + On-device Opus-MT translation. Each model covers one language pair and translates the + text a sentence at a time, streaming the result as it is decoded. + + + Language pair + + {PAIRS.map((pair, i) => ( + selectPair(i)} + > + + {pair.label} + + + ))} + + + + + + {runError && ( + + {runError} + + )} + + + Text to translate + + +