* 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
91 lines
3.0 KiB
TypeScript
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;
|
|
}
|