// 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.";