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:
2026-09-29 22:39:57 +02:00
parent 67c9f10153
commit aac79ffa53
105 changed files with 1001 additions and 494 deletions
+34 -14
View File
@@ -46,7 +46,7 @@ describe("YT_SORT_KEYS", () => {
describe("sortYtResults", () => {
it("leaves the provider's ranking alone on `relevance`", () => {
expect(titles(sortYtResults(RESULTS, "relevance", "desc"))).toEqual([
expect(titles(sortYtResults(RESULTS, "relevance", "desc", "en"))).toEqual([
"b-old-short",
"a-new-long",
"c-mid-mid",
@@ -54,12 +54,12 @@ describe("sortYtResults", () => {
});
it("orders by upload date in both directions", () => {
expect(titles(sortYtResults(RESULTS, "date", "desc"))).toEqual([
expect(titles(sortYtResults(RESULTS, "date", "desc", "en"))).toEqual([
"a-new-long",
"c-mid-mid",
"b-old-short",
]);
expect(titles(sortYtResults(RESULTS, "date", "asc"))).toEqual([
expect(titles(sortYtResults(RESULTS, "date", "asc", "en"))).toEqual([
"b-old-short",
"c-mid-mid",
"a-new-long",
@@ -67,14 +67,18 @@ describe("sortYtResults", () => {
});
it("orders by views and by length", () => {
expect(titles(sortYtResults(RESULTS, "popular", "desc"))[0]).toBe("c-mid-mid");
expect(titles(sortYtResults(RESULTS, "duration", "desc"))[0]).toBe("a-new-long");
expect(titles(sortYtResults(RESULTS, "duration", "asc"))[0]).toBe("b-old-short");
expect(titles(sortYtResults(RESULTS, "popular", "desc", "en"))[0]).toBe("c-mid-mid");
expect(titles(sortYtResults(RESULTS, "duration", "desc", "en"))[0]).toBe("a-new-long");
expect(titles(sortYtResults(RESULTS, "duration", "asc", "en"))[0]).toBe("b-old-short");
});
it("orders by title alphabetically, not by code point", () => {
const accented = [v({ title: "Zebra" }), v({ title: "Ábel" }), v({ title: "alma" })];
expect(titles(sortYtResults(accented, "title", "asc"))).toEqual(["Ábel", "alma", "Zebra"]);
expect(titles(sortYtResults(accented, "title", "asc", "en"))).toEqual([
"Ábel",
"alma",
"Zebra",
]);
});
it("sinks results with no value to the END in both directions", () => {
@@ -84,8 +88,16 @@ describe("sortYtResults", () => {
v({ title: "long", duration_seconds: 900 }),
v({ title: "short", duration_seconds: 60 }),
];
expect(titles(sortYtResults(mixed, "duration", "desc"))).toEqual(["long", "short", "unknown"]);
expect(titles(sortYtResults(mixed, "duration", "asc"))).toEqual(["short", "long", "unknown"]);
expect(titles(sortYtResults(mixed, "duration", "desc", "en"))).toEqual([
"long",
"short",
"unknown",
]);
expect(titles(sortYtResults(mixed, "duration", "asc", "en"))).toEqual([
"short",
"long",
"unknown",
]);
});
it("keeps the provider's ranking inside a tie, in both directions", () => {
@@ -95,19 +107,27 @@ describe("sortYtResults", () => {
v({ title: "second", view_count: 100 }),
v({ title: "third", view_count: 5 }),
];
expect(titles(sortYtResults(tied, "popular", "desc"))).toEqual(["first", "second", "third"]);
expect(titles(sortYtResults(tied, "popular", "asc"))).toEqual(["third", "first", "second"]);
expect(titles(sortYtResults(tied, "popular", "desc", "en"))).toEqual([
"first",
"second",
"third",
]);
expect(titles(sortYtResults(tied, "popular", "asc", "en"))).toEqual([
"third",
"first",
"second",
]);
});
it("does not reorder anything for a key it cannot honour", () => {
// A pref stored before this view offered a control can name `shuffle` or `priority`.
expect(titles(sortYtResults(RESULTS, "priority", "desc"))).toEqual(titles(RESULTS));
expect(titles(sortYtResults(RESULTS, "shuffle", "desc"))).toEqual(titles(RESULTS));
expect(titles(sortYtResults(RESULTS, "priority", "desc", "en"))).toEqual(titles(RESULTS));
expect(titles(sortYtResults(RESULTS, "shuffle", "desc", "en"))).toEqual(titles(RESULTS));
});
it("does not mutate the array it was given", () => {
const input = [...RESULTS];
sortYtResults(input, "date", "asc");
sortYtResults(input, "date", "asc", "en");
expect(titles(input)).toEqual(titles(RESULTS));
});
});