Files
silo-server/web/src/lib/languageNames.ts
T
QuickandGitHub 73488d1bfa feat(metadata): add original-language preferences (#526)
* feat(metadata): add original-language preferences

* fix(settings): show metadata exceptions immediately

* docs(settings): add metadata language screenshot

* fix(settings): make language exceptions responsive

* fix(settings): standardize language display names
2026-07-31 15:23:30 -04:00

91 lines
3.0 KiB
TypeScript

import "@formatjs/intl-displaynames/polyfill-force.js";
import "@formatjs/intl-displaynames/locale-data/en.js";
const englishLanguageNames = new Intl.DisplayNames(["en"], {
type: "language",
fallback: "none",
});
const englishScriptNames = new Intl.DisplayNames(["en"], {
type: "script",
fallback: "none",
});
const englishRegionNames = new Intl.DisplayNames(["en"], {
type: "region",
fallback: "none",
});
function displayName(names: Intl.DisplayNames, value: string): string | null {
try {
return names.of(value) ?? null;
} catch {
return null;
}
}
/** Canonical BCP 47 identity used only for comparison; wire values stay untouched. */
export function canonicalLanguageTag(value: string): string | null {
const trimmed = value.trim();
if (!trimmed) return null;
try {
return new Intl.Locale(trimmed.replace(/_/g, "-")).toString();
} catch {
return null;
}
}
/** Stable identity that de-duplicates ISO aliases without collapsing script or region subtags. */
export function languageIdentity(value: string): string {
return canonicalLanguageTag(value) ?? value.trim().toLowerCase();
}
/**
* Returns a deterministic English CLDR name for an ISO/BCP 47 value.
*
* Some CLDR releases omit a precomposed language-and-script label. In that
* case, compose the independently standardized language, script, and region
* names so tags such as `sr-Latn` remain distinct from `sr`.
*/
export function englishLanguageName(value: string): string | null {
const canonical = canonicalLanguageTag(value);
if (!canonical) return null;
let locale: Intl.Locale;
try {
locale = new Intl.Locale(canonical);
} catch {
return null;
}
const language = displayName(englishLanguageNames, locale.language);
if (!language) return null;
const exact = displayName(englishLanguageNames, canonical);
const qualifiers = [
locale.script ? displayName(englishScriptNames, locale.script) : null,
locale.region ? displayName(englishRegionNames, locale.region) : null,
].filter((part): part is string => Boolean(part));
if (exact && (qualifiers.length === 0 || exact !== language)) return exact;
return qualifiers.length > 0 ? `${language} (${qualifiers.join(", ")})` : language;
}
/** User-facing name with an explicit fallback for unassigned or invalid tags. */
export function getLanguageName(value: string): string {
const trimmed = value.trim();
if (!trimmed) return "Unknown";
return englishLanguageName(trimmed) ?? `Unknown language (${trimmed})`;
}
/** Empty-preserving variant for optional metadata and filter labels. */
export function formatLanguage(value: string): string {
const trimmed = value.trim();
return trimmed ? getLanguageName(trimmed) : "";
}
/** Canonical ISO language subtag used for language matching and override keys. */
export function normalizeLanguageCode(value: string | null | undefined): string {
const canonical = canonicalLanguageTag(value ?? "");
if (!canonical) return "";
return new Intl.Locale(canonical).language;
}