Files
cinny/src/app/utils/translation/langUtils.test.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

56 lines
2.0 KiB
TypeScript

import { test } from 'node:test';
import assert from 'node:assert/strict';
import {
normalizeLang,
sameLanguage,
languageName,
isSupportedTargetLang,
TRANSLATE_TARGET_LANGUAGES,
} from './langUtils';
test('normalizeLang: base subtag, lowercased', () => {
assert.equal(normalizeLang('en-US'), 'en');
assert.equal(normalizeLang('ZH'), 'zh');
assert.equal(normalizeLang('pt_BR'), 'pt');
assert.equal(normalizeLang(' De '), 'de');
assert.equal(normalizeLang(''), '');
assert.equal(normalizeLang(undefined), '');
assert.equal(normalizeLang(null), '');
});
test('normalizeLang: keeps Chinese script subtag', () => {
assert.equal(normalizeLang('zh-Hant'), 'zh-hant');
assert.equal(normalizeLang('zh-Hans-CN'), 'zh-hans');
assert.equal(normalizeLang('zh-CN'), 'zh'); // region-only collapses to base
});
test('sameLanguage: compares normalized bases', () => {
assert.equal(sameLanguage('en', 'en-US'), true);
assert.equal(sameLanguage('EN', 'en'), true);
assert.equal(sameLanguage('en', 'de'), false);
assert.equal(sameLanguage('', 'en'), false); // empty is never "same"
assert.equal(sameLanguage(undefined, undefined), false);
});
test('languageName: resolves common codes', () => {
// Intl.DisplayNames is available in node; assert it returns a real name (not
// the bare code) for a known language.
const de = languageName('de');
assert.ok(de.length > 0 && de.toLowerCase() !== 'de');
// Unknown/garbage code falls back to the code itself.
assert.equal(languageName('zz-not-a-lang'), 'zz-not-a-lang'.split('-')[0]);
});
test('isSupportedTargetLang: curated membership', () => {
assert.equal(isSupportedTargetLang('en'), true);
assert.equal(isSupportedTargetLang('DE'), true);
assert.equal(isSupportedTargetLang('pt-BR'), true);
assert.equal(isSupportedTargetLang('xx'), false);
});
test('curated list: unique codes, English present', () => {
const codes = TRANSLATE_TARGET_LANGUAGES.map((l) => l.code);
assert.equal(new Set(codes).size, codes.length);
assert.ok(codes.includes('en'));
});