feat(i18n): the server's closed English vocabularies named in the reader's language (E30 S2)
- lib/vocab + the `vocab` locale area: TMDB crew jobs and departments, auto-tags (D1 A: the data
stays English, the catalog is named at its one source, useTags), built-in download profiles,
YouTube topics; held key-for-key to the backend's own lists by backendIds.contract.
- Countries by ISO code: TMDB parse stores {code, name}; rows stored before are coded from TMDB's
own country list in one request (run_enrich); Intl names them, historic codes (YU, SU, XI…)
from the catalog.
- Player tracks named from the language tag, forced/SDH flags and codec tail, not Plex's
server-language displayTitle; no invented "Audio N" on the server.
- Download lifecycle notices (shared / expiring / expired / evicted) named from data; the share
notice carries the title.
- Scheduler: the job result rides along as data and is named per key (dashboard + run notice).
- Invite status named; a render crash's notice no longer shows the engine's English message.
- Fix: YouTube links …/wiki/Humour, the tagger looked for "Humor" — Comedy never came from a topic.
This commit is contained in:
@@ -27,7 +27,7 @@ import {
|
||||
Volume2,
|
||||
VolumeX,
|
||||
} from "lucide-react";
|
||||
import { api, HttpError } from "../lib/api";
|
||||
import { api, HttpError, type PlexTrackNaming } from "../lib/api";
|
||||
import { notifyActionError } from "../lib/actionErrors";
|
||||
import {
|
||||
audioOrdForLang,
|
||||
@@ -88,6 +88,7 @@ import { PopoverItem, PopoverSurface } from "./ui/AnchoredPopover";
|
||||
import { closeThenAct, useAnchoredPopover } from "./ui/anchoredPopoverHooks";
|
||||
import { Slider } from "./ui/NativeControls";
|
||||
import { formatUnit } from "../lib/format";
|
||||
import { trackLabel } from "../lib/vocab";
|
||||
|
||||
// The rich info overlay (poster/cast/ratings) reuses the same component as the card's info page.
|
||||
const PlexInfo = lazy(() => import("./PlexInfo"));
|
||||
@@ -131,13 +132,16 @@ const DOUBLE_TAP_MS = 300;
|
||||
type Props = { itemId: string; onClose: () => void; queue?: string[]; fromStart?: boolean };
|
||||
|
||||
export default function PlexPlayer({ itemId, onClose, queue, fromStart = false }: Props) {
|
||||
const { t } = useTranslation();
|
||||
const { t, i18n } = useTranslation();
|
||||
// `t` changes identity on a language switch. loadSession only uses it for error strings, so read it
|
||||
// through a ref — keeping it OUT of loadSession's deps. Otherwise a mid-playback language change
|
||||
// rebuilt loadSession → re-ran the detail effect → reloaded the session from the STALE saved resume
|
||||
// position (losing live progress). See the detail effect's deps.
|
||||
const tRef = useRef(t);
|
||||
tRef.current = t;
|
||||
// E30: a track named in the reader's language (lib/vocab), not in the Plex server's ("English (AC3 5.1)").
|
||||
const audioName = (a: PlexTrackNaming) => trackLabel(t, a, "audio", i18n.language);
|
||||
const subtitleName = (x: PlexTrackNaming) => trackLabel(t, x, "subtitle", i18n.language);
|
||||
const qc = useQueryClient();
|
||||
const [id, setId] = useState(itemId);
|
||||
// Serve the player's title/summary/rich detail in the active language (E5 S6), matching the grid.
|
||||
@@ -1238,16 +1242,21 @@ export default function PlexPlayer({ itemId, onClose, queue, fromStart = false }
|
||||
audio: () => {
|
||||
const ord = cycleAudio();
|
||||
if (ord === undefined) return; // one track (or none): nothing to say
|
||||
const label = detail?.audio_streams.find((a) => a.ord === (ord ?? 0))?.label;
|
||||
flashOsd(`${t("plex.player.audio")} · ${label ?? "—"}`);
|
||||
const track = detail?.audio_streams.find((a) => a.ord === (ord ?? 0));
|
||||
flashOsd(
|
||||
`${t("plex.player.audio")} · ${track ? trackLabel(t, track, "audio", i18n.language) : "—"}`,
|
||||
);
|
||||
},
|
||||
subtitle: () => {
|
||||
const ord = cycleSubtitle();
|
||||
if (ord === undefined) return;
|
||||
const track = ord == null ? null : detail?.subtitle_streams.find((x) => x.ord === ord);
|
||||
const label =
|
||||
ord == null
|
||||
? t("plex.player.subOff")
|
||||
: (detail?.subtitle_streams.find((x) => x.ord === ord)?.label ?? "—");
|
||||
: track
|
||||
? trackLabel(t, track, "subtitle", i18n.language)
|
||||
: "—";
|
||||
flashOsd(`${t("plex.player.subtitles")} · ${label}`);
|
||||
},
|
||||
info: () => setInfoOpen((v) => !v),
|
||||
@@ -1309,6 +1318,7 @@ export default function PlexPlayer({ itemId, onClose, queue, fromStart = false }
|
||||
cycleSubtitle,
|
||||
flashOsd,
|
||||
t,
|
||||
i18n.language,
|
||||
detail,
|
||||
prevId,
|
||||
nextId,
|
||||
@@ -1336,6 +1346,9 @@ export default function PlexPlayer({ itemId, onClose, queue, fromStart = false }
|
||||
const bufPct = duration > 0 ? (seekableEnd / duration) * 100 : 0;
|
||||
// While scrubbing the head follows the finger (preview); otherwise it tracks playback.
|
||||
const headPct = scrub != null && duration > 0 ? (scrub / duration) * 100 : pct;
|
||||
// The tracks playing now, for the stats overlay.
|
||||
const currentAudio = detail?.audio_streams.find((x) => x.ord === (audioOrd ?? 0));
|
||||
const currentSub = detail?.subtitle_streams.find((x) => x.ord === subOrd);
|
||||
|
||||
// Portaled to <body>, like every other full-screen overlay here (Modal, PlayerModal, ChatDock,
|
||||
// BackToTop). A fixed, full-viewport player must not be painted inside the page scroller: the
|
||||
@@ -1412,7 +1425,7 @@ export default function PlexPlayer({ itemId, onClose, queue, fromStart = false }
|
||||
kind="subtitles"
|
||||
src={api.plexSubtitleUrl(id, selectedSub.ord, subTrackOffset, subGen)}
|
||||
srcLang={selectedSub.language ?? undefined}
|
||||
label={selectedSub.label}
|
||||
label={subtitleName(selectedSub)}
|
||||
{...subTrackListeners}
|
||||
default
|
||||
/>
|
||||
@@ -1466,7 +1479,7 @@ export default function PlexPlayer({ itemId, onClose, queue, fromStart = false }
|
||||
) : (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 shrink-0 animate-spin" aria-hidden />
|
||||
<span>{t("plex.player.subPreparing", { label: selectedSub.label })}</span>
|
||||
<span>{t("plex.player.subPreparing", { label: subtitleName(selectedSub) })}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
@@ -1783,14 +1796,14 @@ export default function PlexPlayer({ itemId, onClose, queue, fromStart = false }
|
||||
tone="media"
|
||||
role="menuitemradio"
|
||||
checked={(audioOrd ?? 0) === a.ord}
|
||||
label={a.label}
|
||||
label={audioName(a)}
|
||||
onSelect={() => changeAudio(a.ord)}
|
||||
className={`${TRACK_ROW} ${
|
||||
(audioOrd ?? 0) === a.ord ? "text-accent" : "text-white"
|
||||
}`}
|
||||
>
|
||||
<MenuCheck kind="one" on={(audioOrd ?? 0) === a.ord} />
|
||||
<span className="truncate">{a.label}</span>
|
||||
<span className="truncate">{audioName(a)}</span>
|
||||
</PopoverItem>
|
||||
))}
|
||||
</div>
|
||||
@@ -1815,14 +1828,14 @@ export default function PlexPlayer({ itemId, onClose, queue, fromStart = false }
|
||||
tone="media"
|
||||
role="menuitemradio"
|
||||
checked={subOrd === sub.ord}
|
||||
label={sub.label}
|
||||
label={subtitleName(sub)}
|
||||
onSelect={() => changeSubtitle(sub.ord)}
|
||||
className={`${TRACK_ROW} ${
|
||||
subOrd === sub.ord ? "text-accent" : "text-white"
|
||||
}`}
|
||||
>
|
||||
<MenuCheck kind="one" on={subOrd === sub.ord} />
|
||||
<span className="truncate">{sub.label}</span>
|
||||
<span className="truncate">{subtitleName(sub)}</span>
|
||||
</PopoverItem>
|
||||
))}
|
||||
</div>
|
||||
@@ -1896,13 +1909,13 @@ export default function PlexPlayer({ itemId, onClose, queue, fromStart = false }
|
||||
videoRef={videoRef}
|
||||
hlsRef={hlsRef}
|
||||
fragMeterRef={fragMeterRef}
|
||||
audioLabel={
|
||||
detail?.audio_streams.find((a) => a.ord === (audioOrd ?? 0))?.label ?? undefined
|
||||
}
|
||||
audioLabel={currentAudio ? audioName(currentAudio) : undefined}
|
||||
subLabel={
|
||||
subOrd == null
|
||||
? t("plex.player.subOff")
|
||||
: (detail?.subtitle_streams.find((x) => x.ord === subOrd)?.label ?? undefined)
|
||||
: currentSub
|
||||
? subtitleName(currentSub)
|
||||
: undefined
|
||||
}
|
||||
playable={detail?.playable ?? ""}
|
||||
hwRef={hwRef}
|
||||
|
||||
Reference in New Issue
Block a user