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:
@@ -267,6 +267,10 @@ export interface Settings {
|
||||
| 'earthday'
|
||||
| 'deepspace'
|
||||
| 'arcade';
|
||||
|
||||
// On-device message translation
|
||||
translateTargetLang: string; // BCP-47 base code, default 'en'
|
||||
autoTranslate: boolean; // auto-translate incoming foreign messages (opt-in)
|
||||
}
|
||||
|
||||
const defaultSettings: Settings = {
|
||||
@@ -366,6 +370,9 @@ const defaultSettings: Settings = {
|
||||
soundboardVolume: 80,
|
||||
|
||||
seasonalThemeOverride: 'auto',
|
||||
|
||||
translateTargetLang: 'en',
|
||||
autoTranslate: false,
|
||||
};
|
||||
|
||||
export const getSettings = (): Settings => {
|
||||
@@ -402,6 +409,16 @@ export const getSettings = (): Settings => {
|
||||
saved.ringtoneId === 'none'
|
||||
? saved.ringtoneId
|
||||
: defaultSettings.ringtoneId,
|
||||
// Coerce persisted target language to a non-empty string; anything else
|
||||
// (missing/wrong type) falls back to the default.
|
||||
translateTargetLang:
|
||||
typeof saved.translateTargetLang === 'string' && saved.translateTargetLang.trim()
|
||||
? saved.translateTargetLang
|
||||
: defaultSettings.translateTargetLang,
|
||||
autoTranslate:
|
||||
typeof saved.autoTranslate === 'boolean'
|
||||
? saved.autoTranslate
|
||||
: defaultSettings.autoTranslate,
|
||||
composerToolbarButtons: {
|
||||
...DEFAULT_COMPOSER_TOOLBAR,
|
||||
...(saved.composerToolbarButtons ?? {}),
|
||||
|
||||
Reference in New Issue
Block a user