fix(i18n): numbers, units, dates and order in the UI language, and after a switch (E30 S3)
Every locale formatter takes the language as an argument. Under the React
Compiler a formatter that read the i18n singleton was memoized on its other
arguments, so a card kept "2 hr 4 min" after a switch to Hungarian; plexLang()
had the same flaw and left Plex queries in the old language. Components now
take the language from useTranslation, and the signature makes forgetting it
a type error.
- ratings, file sizes, stream stats: the language's decimal mark ("8,3")
- view and subscriber counts compact in the language ("1,2 E")
- seconds as the language's unit ("6 mp") in the sliders and the stats
- a person's birth and death dates as dates, not ISO strings
- A-Z lists by the shown name in the language's alphabet (compareText),
the tag catalog re-sorted by its translated names
- lint local/locale-format (was date-format) also refuses Intl.NumberFormat
outside lib/format.ts, localeCompare, and a component reading the language
off the i18n singleton
This commit is contained in:
@@ -58,6 +58,7 @@ import { Pressable } from "./ui/Pressable";
|
||||
import { SURFACE } from "./ui/surfaces";
|
||||
import { PageHeading } from "./ui/Headings";
|
||||
import { vocab } from "../lib/vocab";
|
||||
import { formatDate, formatDecimal } from "../lib/format";
|
||||
|
||||
// The Plex sub-router's view branches, split out of PlexBrowse. Each is invoked by PlexBrowse with
|
||||
// shell callbacks as props (onBack/onPlay/onFilter/…) — no view reaches back into the shell's state,
|
||||
@@ -82,8 +83,8 @@ export function PlexInfoView({
|
||||
onFilter: (patch: Partial<PlexFilters>) => void;
|
||||
onOpenPerson: (name: string) => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const lang = plexLang();
|
||||
const { t, i18n } = useTranslation();
|
||||
const lang = plexLang(i18n.language);
|
||||
const q = useQuery({ queryKey: qk.plexItem(id, lang), queryFn: () => api.plexItem(id, lang) });
|
||||
return (
|
||||
<div className="w-[90%] max-w-400 mx-auto">
|
||||
@@ -137,13 +138,13 @@ export function PlexPersonView({
|
||||
onInfoCard: (card: PlexCard) => void;
|
||||
onFilter: (patch: Partial<PlexFilters>) => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const { t, i18n } = useTranslation();
|
||||
const qc = useQueryClient();
|
||||
// ONE menu for this whole page — the "known for" rail and the two filmography grids are three
|
||||
// lists on one screen, and the popover anchors where it opened, so a second controller would only
|
||||
// mean a second closed popover to keep shut (see ui/ContextMenu).
|
||||
const menu = useContextMenu<PlexCard>();
|
||||
const lang = plexLang();
|
||||
const lang = plexLang(i18n.language);
|
||||
const personKey = qk.plexPerson(name, lang);
|
||||
const q = useQuery({ queryKey: personKey, queryFn: () => api.plexPerson(name, lang) });
|
||||
// E5 S8: cross-dissolve the backdrop through the person's films' art (the touch the roadmap calls
|
||||
@@ -255,12 +256,13 @@ export function PlexPersonView({
|
||||
{(d.birthday || d.place_of_birth) && (
|
||||
<p>
|
||||
<span className="text-fg/70">{t("plex.person.born")}:</span>{" "}
|
||||
{[d.birthday, d.place_of_birth].filter(Boolean).join(" · ")}
|
||||
{[formatDate(d.birthday, lang), d.place_of_birth].filter(Boolean).join(" · ")}
|
||||
</p>
|
||||
)}
|
||||
{d.deathday && (
|
||||
<p>
|
||||
<span className="text-fg/70">{t("plex.person.died")}:</span> {d.deathday}
|
||||
<span className="text-fg/70">{t("plex.person.died")}:</span>{" "}
|
||||
{formatDate(d.deathday, lang)}
|
||||
</p>
|
||||
)}
|
||||
{d.also_known_as.length > 0 && (
|
||||
@@ -345,10 +347,10 @@ export function PlexShowView({
|
||||
onFilter?: (patch: Partial<PlexFilters>) => void;
|
||||
onOpenPerson?: (name: string) => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const { t, i18n } = useTranslation();
|
||||
const qc = useQueryClient();
|
||||
const isAdmin = qc.getQueryData<{ role?: string }>(qk.me())?.role === "admin";
|
||||
const lang = plexLang();
|
||||
const lang = plexLang(i18n.language);
|
||||
const q = useQuery({
|
||||
queryKey: qk.plexShow(showId, lang),
|
||||
queryFn: () => api.plexShow(showId, lang),
|
||||
@@ -535,7 +537,7 @@ export function PlexShowView({
|
||||
>
|
||||
{/* the rating gold — its own token, not --warn: a score is not a warning */}
|
||||
<Star className="w-3.5 h-3.5 text-rating" fill="currentColor" />
|
||||
{score.toFixed(1)}
|
||||
{formatDecimal(score, lang)}
|
||||
</Pressable>
|
||||
);
|
||||
})()}
|
||||
@@ -671,9 +673,9 @@ export function PlexSeasonView({
|
||||
onBack: () => void;
|
||||
onPlay: (epRk: string, queue: string[], fromStart?: boolean) => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const { t, i18n } = useTranslation();
|
||||
const qc = useQueryClient();
|
||||
const lang = plexLang();
|
||||
const lang = plexLang(i18n.language);
|
||||
const q = useQuery({
|
||||
queryKey: qk.plexShow(showId, lang),
|
||||
queryFn: () => api.plexShow(showId, lang),
|
||||
|
||||
Reference in New Issue
Block a user