import type { TFunction } from "i18next"; import type { PlexTrackNaming } from "./api"; // The closed vocabularies the server stores in ENGLISH — a TMDB crew job or department, an // auto-tag, a built-in download profile, a YouTube topic, a Plex certificate's word ("Not Rated"; a // code such as "PG-13" reads the same everywhere and falls through as stored) — named in the reader's language. The data // stays language-neutral (it is filtered and matched on); only the NAME is translated, here, from the // `vocab` locale area. A word the area does not hold shows as stored, so a new server value is never // hidden — and the backend's own lists are held key-for-key to the area by backendIds.contract. type Group = "crewJob" | "department" | "autoTag" | "profile" | "topic" | "certificate"; /** A vocabulary word's key: its letters and digits ("Health & Fitness" → "HealthFitness"). */ export function vocabKey(word: string): string { return word.replace(/[^A-Za-z0-9]/g, ""); } /** The i18n key of one word of `group` ("" when the word has no letters or digits) — for a caller * whose `t` is not i18next's own (a pure builder handed a narrow `t`). */ export function vocabI18nKey(group: Group, word: string): string { const key = vocabKey(word); return key ? `vocab.${group}.${key}` : ""; } /** One stored English word of `group`, in the reader's language (`fallback`, by default the word * itself, when the area does not hold it). */ export function vocab(t: TFunction, group: Group, word: string, fallback = word): string { const key = vocabI18nKey(group, word); return key ? t(key, { defaultValue: fallback }) : fallback; } /** A download profile's name: a built-in one is seeded under an English name and named here; a user's * own is theirs, as typed. */ export function profileName(t: TFunction, p: { name: string; is_builtin: boolean }): string { return p.is_builtin ? vocab(t, "profile", p.name) : p.name; } function intlName(code: string, lang: string, type: "region" | "language"): string | undefined { try { return new Intl.DisplayNames([lang], { type, fallback: "none" }).of(code); } catch { return undefined; // not a well-formed code } } /** A country from its ISO 3166 code. A code no longer in use (Yugoslavia, the Soviet Union) is * named from `vocab.region` — Intl renames those to today's state (YU → Serbia) or cannot name them * (XI, TMDB's Northern Ireland); `fallback` (the stored English name) when neither can. */ export function regionName( t: TFunction, code: string | null | undefined, lang: string, fallback = "", ): string { if (!code) return fallback; const up = code.toUpperCase(); return ( t(`vocab.region.${up}`, { defaultValue: "" }) || intlName(up, lang, "region") || fallback || up ); } /** A language from its BCP 47 tag ("en-US" → "American English"), capitalized to stand alone as a * label ("magyar" → "Magyar"); `fallback` for a tag Intl cannot name ("und", a typo). */ export function languageName(tag: string | null | undefined, lang: string, fallback = ""): string { const name = tag ? intlName(tag.replace(/_/g, "-"), lang, "language") : undefined; if (!name || name === tag) return fallback; return name.charAt(0).toLocaleUpperCase(lang) + name.slice(1); } /** An audio or subtitle track's name in the reader's language: "Angol (AC3 5.1)", "Magyar, * kényszerített". Built from the language tag; a track without one keeps Plex's own title, and a * track with neither is numbered. */ export function trackLabel( t: TFunction, track: PlexTrackNaming, kind: "audio" | "subtitle", lang: string, ): string { // The language alone: Plex tags one track "en-US" and the next "en", and both are just English here. const name = languageName(track.lang_tag?.split(/[-_]/)[0], lang); if (!name) return track.label || t(`vocab.track.${kind}N`, { n: track.ord + 1 }); const flags = [ track.forced ? t("vocab.track.forced") : null, track.hearing_impaired ? t("vocab.track.sdh") : null, ].filter(Boolean); // The codec tail is technical and language-neutral but for its channel word ("AC3 Stereo"). const detail = track.detail ?.replace(/\bStereo\b/i, t("vocab.track.stereo")) .replace(/\bMono\b/i, t("vocab.track.mono")); return [name, ...flags].join(", ") + (detail ? ` (${detail})` : ""); }