Files
cinny/src/app/utils/translation/langUtils.ts
T
Lotus CI b07eb35159
CI / Build & Quality Checks (pull_request) Canceled after 0s
CI / Trigger Desktop Build (pull_request) Canceled after 0s
CI / Secret scan (gitleaks) (pull_request) Canceled after 0s
CI / Docker image build & smoke test (pull_request) Canceled after 0s
CI / Playwright smoke (e2e) (pull_request) Canceled after 0s
Merge remote-tracking branch 'origin/lotus' into fix/translate-unavailable-copy
# Conflicts:
#	src/app/utils/translation/langUtils.ts
2026-10-18 23:37:54 -04:00

104 lines
4.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Pure language-code helpers for the on-device translation feature. No DOM / no
// engine — unit-testable in isolation (langUtils.test.ts).
/**
* Normalize a BCP-47 tag to the base language subtag, lowercased:
* "en-US" -> "en", "ZH" -> "zh", "pt-BR" -> "pt".
* Chinese is a common exception where script matters (zh-Hant vs zh-Hans); we
* keep the script subtag for `zh` so a Traditional/Simplified distinction isn't
* flattened. Everything else collapses to the primary subtag.
*/
export function normalizeLang(code: string | undefined | null): string {
if (!code) return '';
const lower = code.trim().toLowerCase();
if (!lower) return '';
const parts = lower.split(/[-_]/);
const base = parts[0];
if (base === 'zh' && (parts[1] === 'hant' || parts[1] === 'hans')) {
return `zh-${parts[1]}`;
}
return base;
}
/** True when two language tags refer to the same base language. */
export function sameLanguage(a: string | undefined | null, b: string | undefined | null): boolean {
const na = normalizeLang(a);
const nb = normalizeLang(b);
return na !== '' && na === nb;
}
/**
* Curated list of target languages offered in the "Translate messages into"
* setting — the common languages the on-device engine can produce. Codes are the
* normalized base tags passed to the engine.
*/
export const TRANSLATE_TARGET_LANGUAGES: ReadonlyArray<{ code: string; name: string }> = [
{ code: 'en', name: 'English' },
{ code: 'es', name: 'Spanish' },
{ code: 'fr', name: 'French' },
{ code: 'de', name: 'German' },
{ code: 'it', name: 'Italian' },
{ code: 'pt', name: 'Portuguese' },
{ code: 'nl', name: 'Dutch' },
{ code: 'pl', name: 'Polish' },
{ code: 'ru', name: 'Russian' },
{ code: 'uk', name: 'Ukrainian' },
{ code: 'tr', name: 'Turkish' },
{ code: 'ar', name: 'Arabic' },
{ code: 'he', name: 'Hebrew' },
{ code: 'hi', name: 'Hindi' },
{ code: 'bn', name: 'Bengali' },
{ code: 'ja', name: 'Japanese' },
{ code: 'ko', name: 'Korean' },
{ code: 'zh', name: 'Chinese' },
{ code: 'vi', name: 'Vietnamese' },
{ code: 'th', name: 'Thai' },
{ code: 'id', name: 'Indonesian' },
{ code: 'sv', name: 'Swedish' },
{ code: 'cs', name: 'Czech' },
{ code: 'ro', name: 'Romanian' },
{ code: 'el', name: 'Greek' },
{ code: 'fa', name: 'Persian' },
];
const CURATED_NAME = new Map(TRANSLATE_TARGET_LANGUAGES.map((l) => [l.code, l.name]));
/**
* Human-readable language name for a code, e.g. "de" -> "German". Prefers
* `Intl.DisplayNames` (localized) and falls back to the curated map, then the
* raw code. `uiLocale` picks the display locale for the name.
*/
export function languageName(code: string, uiLocale = 'en'): string {
const norm = normalizeLang(code);
if (!norm) return code;
try {
const dn = new Intl.DisplayNames([uiLocale], { type: 'language' });
const name = dn.of(norm);
if (name && name.toLowerCase() !== norm) return name;
} catch {
/* Intl.DisplayNames unsupported or bad code — fall through */
}
return CURATED_NAME.get(norm) ?? norm;
}
/** Is `code` in the curated target-language list? (validates a persisted setting) */
export function isSupportedTargetLang(code: string): boolean {
return CURATED_NAME.has(normalizeLang(code));
}
/**
* [Gitea #349] Why translation is unavailable, by platform. The desktop app
* on Linux/macOS (WebKit) lacks the on-device APIs too, so it must not send
* people to "the Lotus desktop app" they're already using.
*/
export const translationUnavailableText = (desktopApp: boolean): string =>
desktopApp
? 'On-device translation isn’t available in this version of the desktop app. It works in Chrome or Edge 138+ on a computer.'
: 'On-device translation isn’t available in this browser. Use Chrome or Edge 138+ on a computer.';
/** Pure: why a manual Translate did nothing (#287). */
export const translateSkipMessage = (from: string | undefined, targetLang: string): string =>
from
? `This message is already in ${languageName(targetLang) || targetLang}.`
: "Couldn't tell what language this message is in, so it wasn't translated.";