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>
76 lines
2.8 KiB
TypeScript
76 lines
2.8 KiB
TypeScript
// Public translation-engine abstraction. One implementation today
|
|
// (`chromeEngine` — the Chromium on-device Translator/LanguageDetector API);
|
|
// a Bergamot-WASM engine can implement the same interface later with no
|
|
// call-site changes. Privacy invariant: an engine MUST run on-device only —
|
|
// message text may never leave the machine. There is intentionally no network
|
|
// engine.
|
|
|
|
export type TranslateAvailability = 'unavailable' | 'downloadable' | 'downloading' | 'available';
|
|
|
|
export interface TranslationEngine {
|
|
/** Is this engine usable in the current browser right now? */
|
|
isSupported(): boolean;
|
|
/** Detect the source language of `text` — a normalized base code (e.g. "de") or undefined. */
|
|
detect(text: string): Promise<string | undefined>;
|
|
/** Can this source→target pair be translated (and is the model downloaded)? */
|
|
availability(source: string, target: string): Promise<TranslateAvailability>;
|
|
/**
|
|
* Translate `text` from `source` to `target`. May trigger a one-time on-device
|
|
* model download (reported via `onDownloadProgress`, 0..1). Must be invoked
|
|
* from a user gesture the first time a pair's model needs downloading.
|
|
*/
|
|
translate(
|
|
text: string,
|
|
source: string,
|
|
target: string,
|
|
onDownloadProgress?: (progress: number) => void,
|
|
): Promise<string>;
|
|
}
|
|
|
|
// ── Ambient types for the Chromium on-device APIs ──────────────────────────────
|
|
// Experimental globals not present in TS's lib.dom; declared minimally here.
|
|
// See https://developer.chrome.com/docs/ai/translator-api and the MDN
|
|
// Translator_and_Language_Detector_APIs page.
|
|
|
|
interface CreateMonitor {
|
|
addEventListener(type: 'downloadprogress', listener: (event: { loaded: number }) => void): void;
|
|
}
|
|
|
|
export interface ChromeTranslatorInstance {
|
|
translate(input: string): Promise<string>;
|
|
destroy?(): void;
|
|
}
|
|
|
|
export interface ChromeTranslatorFactory {
|
|
availability(opts: {
|
|
sourceLanguage: string;
|
|
targetLanguage: string;
|
|
}): Promise<TranslateAvailability>;
|
|
create(opts: {
|
|
sourceLanguage: string;
|
|
targetLanguage: string;
|
|
monitor?: (m: CreateMonitor) => void;
|
|
}): Promise<ChromeTranslatorInstance>;
|
|
}
|
|
|
|
export interface ChromeDetectorResult {
|
|
detectedLanguage: string;
|
|
confidence: number;
|
|
}
|
|
|
|
export interface ChromeLanguageDetectorInstance {
|
|
detect(input: string): Promise<ChromeDetectorResult[]>;
|
|
destroy?(): void;
|
|
}
|
|
|
|
export interface ChromeLanguageDetectorFactory {
|
|
create(opts?: { monitor?: (m: CreateMonitor) => void }): Promise<ChromeLanguageDetectorInstance>;
|
|
}
|
|
|
|
declare global {
|
|
// eslint-disable-next-line vars-on-top
|
|
var Translator: ChromeTranslatorFactory | undefined;
|
|
// eslint-disable-next-line vars-on-top
|
|
var LanguageDetector: ChromeLanguageDetectorFactory | undefined;
|
|
}
|