feat(translation): on-device message translation
Add per-message translation that runs entirely on-device via the Chromium built-in Translator + LanguageDetector APIs. Message text never leaves the machine and never touches a cloud service, preserving the E2EE guarantee. When the on-device engine is unavailable (non-Chromium / mobile) the feature simply hides itself; there is no network fallback. - Engine abstraction (utils/translation): TranslationEngine interface plus a chromeTranslationEngine implementation (feature-detected, caches translator/detector instances, download-progress monitor). Pure lang-code helpers (normalize/sameLanguage/curated targets) with unit tests. - Settings: translateTargetLang (default English) + autoTranslate (opt-in), with a Messages settings tile — a target-language select and an auto-translate switch, disabled with a note where unsupported. - useMessageTranslation hook + shared per-event toggle atom-family and a persisted LRU cache so scrollback never re-translates. - UI: a Translate / Show Original message-menu action, an inline "Translated from <lang> - Show original" chip, and a body swap in m.text/m.emote/m.notice that renders the translated text through the plain-text path (linkify + emoji) inside a dir=auto span for RTL. - Auto-translate flips foreign messages whose model is already downloaded; first-time downloads keep the manual chip (user gesture). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -101,6 +101,12 @@ import { BG_OPTIONS, getChatBg } from '../../lotus/chatBackground';
|
||||
import { resetBootSequence, runLotusBootSequence } from '../../../../lotus-boot';
|
||||
import { useMessageLayoutItems } from '../../../hooks/useMessageLayout';
|
||||
import { useMessageSpacingItems } from '../../../hooks/useMessageSpacing';
|
||||
import {
|
||||
TRANSLATE_TARGET_LANGUAGES,
|
||||
normalizeLang,
|
||||
isSupportedTargetLang,
|
||||
} from '../../../utils/translation/langUtils';
|
||||
import { chromeTranslationEngine } from '../../../utils/translation/chromeEngine';
|
||||
import { SequenceCardStyle } from '../styles.css';
|
||||
import { useTauriUpdater } from '../../../hooks/useTauriUpdater';
|
||||
import { isTauri as isTauriEnv, invokeTauri, tauriInvoke } from '../../../hooks/useTauri';
|
||||
@@ -2311,6 +2317,15 @@ function Messages() {
|
||||
settingsAtom,
|
||||
'enforceRetentionLocally',
|
||||
);
|
||||
const [translateTargetLang, setTranslateTargetLang] = useSetting(
|
||||
settingsAtom,
|
||||
'translateTargetLang',
|
||||
);
|
||||
const [autoTranslate, setAutoTranslate] = useSetting(settingsAtom, 'autoTranslate');
|
||||
const translationSupported = chromeTranslationEngine.isSupported();
|
||||
const selectedTargetLang = isSupportedTargetLang(translateTargetLang)
|
||||
? normalizeLang(translateTargetLang)
|
||||
: 'en';
|
||||
|
||||
return (
|
||||
<Box direction="Column" gap="100">
|
||||
@@ -2430,6 +2445,38 @@ function Messages() {
|
||||
}
|
||||
/>
|
||||
</SequenceCard>
|
||||
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
|
||||
<SettingTile
|
||||
title="Translate Messages Into"
|
||||
description={
|
||||
translationSupported
|
||||
? 'Translate messages written in other languages into this one. Runs entirely on your device — message text never leaves your computer or touches a cloud service.'
|
||||
: 'On-device translation isn’t available in this browser. Use a Chromium desktop browser (Chrome/Edge 138+) or the Lotus desktop app.'
|
||||
}
|
||||
after={
|
||||
<select
|
||||
aria-label="Translate messages into"
|
||||
disabled={!translationSupported}
|
||||
value={selectedTargetLang}
|
||||
onChange={(e) => setTranslateTargetLang(e.target.value)}
|
||||
style={pickerInputStyle(color, config)}
|
||||
>
|
||||
{TRANSLATE_TARGET_LANGUAGES.map((l) => (
|
||||
<option key={l.code} value={l.code}>
|
||||
{l.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
}
|
||||
/>
|
||||
{translationSupported && (
|
||||
<SettingTile
|
||||
title="Auto-translate Incoming Messages"
|
||||
description="Automatically translate messages that aren’t already in your language. When off, translate individual messages from their menu. The first time a language is used, its model downloads once (a few MB)."
|
||||
after={<Switch variant="Primary" value={autoTranslate} onChange={setAutoTranslate} />}
|
||||
/>
|
||||
)}
|
||||
</SequenceCard>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user