Files
cinny/src/app/utils/translation/engine.ts
T
jaredandClaude Opus 4.8 ecb7b1a7fb 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>
2026-07-18 16:08:40 -04:00

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