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:
@@ -1,7 +1,7 @@
|
||||
import { useState } from "react";
|
||||
import { withActiveAccount } from "../lib/activeAccount";
|
||||
import { qk } from "../lib/queryKeys";
|
||||
import { formatDate, formatDateTime } from "../lib/format";
|
||||
import { formatDate, formatDateTime, formatUnit } from "../lib/format";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { Activity, Bell, Check, Loader2, Monitor, RefreshCw, Trash2, User } from "lucide-react";
|
||||
@@ -245,7 +245,8 @@ function Appearance({ prefs }: { prefs: PrefsController }) {
|
||||
}
|
||||
|
||||
function Notifications({ prefs }: { prefs: PrefsController }) {
|
||||
const { t } = useTranslation();
|
||||
const { t, i18n } = useTranslation();
|
||||
const lang = i18n.language;
|
||||
// Controlled by PrefsProvider (live preview via configureNotifications there), which auto-saves
|
||||
// it. The "send test" button below fires immediately.
|
||||
const { notif: cfg, setNotif } = prefs;
|
||||
@@ -270,7 +271,9 @@ function Notifications({ prefs }: { prefs: PrefsController }) {
|
||||
<div className="py-1.5 text-sm">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-muted text-xs">{t("settings.notifications.dismissAfter")}</span>
|
||||
<span className="text-muted text-xs">{(cfg.durationMs / 1000).toFixed(0)}s</span>
|
||||
<span className="text-muted text-xs">
|
||||
{formatUnit(Math.round(cfg.durationMs / 1000), "second", lang)}
|
||||
</span>
|
||||
</div>
|
||||
<Slider
|
||||
className="mt-1"
|
||||
@@ -522,7 +525,8 @@ function SignInMethods({ me }: { me: Me }) {
|
||||
// and does a one-time "Plex is master" import. Admin-only (rides the server admin token) and only
|
||||
// shown when the Plex module is enabled. Two-way push/pull arrive in later phases.
|
||||
function PlexWatchSync() {
|
||||
const { t } = useTranslation();
|
||||
const { t, i18n } = useTranslation();
|
||||
const lang = i18n.language;
|
||||
const qc = useQueryClient();
|
||||
const link = useQuery({ queryKey: qk.plexWatchLink(), queryFn: () => api.plexWatchLink() });
|
||||
const enabled = link.data?.sync_enabled ?? false;
|
||||
@@ -565,7 +569,9 @@ function PlexWatchSync() {
|
||||
// Reflect the toggle's target immediately (optimistic) so the switch responds on click rather than
|
||||
// only flipping once the multi-second import returns — otherwise it reads as "nothing happened".
|
||||
const shown = toggle.isPending ? Boolean(toggle.variables) : enabled;
|
||||
const last = link.data?.last_watch_sync_at ? formatDateTime(link.data.last_watch_sync_at) : null;
|
||||
const last = link.data?.last_watch_sync_at
|
||||
? formatDateTime(link.data.last_watch_sync_at, lang)
|
||||
: null;
|
||||
|
||||
return (
|
||||
<Section title={t("settings.plexSync.title")}>
|
||||
@@ -765,7 +771,8 @@ export function DownloadAuth() {
|
||||
/** Who is signed in: name, address, role — and the latest sign-in on any device, so one the user does
|
||||
* not recognise is something they can see. */
|
||||
export function AccountHeader({ me }: { me: Me }) {
|
||||
const { t } = useTranslation();
|
||||
const { t, i18n } = useTranslation();
|
||||
const lang = i18n.language;
|
||||
return (
|
||||
<Section title={t("settings.account.title")}>
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
@@ -782,7 +789,7 @@ export function AccountHeader({ me }: { me: Me }) {
|
||||
{!me.is_demo && (
|
||||
<div className="text-xs text-muted" data-testid="account-last-login">
|
||||
{me.last_login_at
|
||||
? t("settings.account.lastLogin", { time: formatDateTime(me.last_login_at) })
|
||||
? t("settings.account.lastLogin", { time: formatDateTime(me.last_login_at, lang) })
|
||||
: t("settings.account.lastLoginNone")}
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user