fix: numbers follow the UI language, not the browser's
`n.toLocaleString()` groups in the BROWSER's locale, so an English UI in a
Hungarian browser printed "1 234 567" while a Hungarian UI in an English
browser printed "1,234,567" — the number half of the date defect fixed in
618ef812. One `formatNumber` in lib/format.ts (a per-language cached
Intl.NumberFormat, like the relative-time formatters beside it) now serves
every grouped figure: Stats, Scheduler, the channel columns and the stats
popover, VideoCard's view tooltip, the ItemCount pill, and formatTotalHours.
ReleaseNotes' `toLocaleDateString(undefined, …)` was the one DATE site the
previous sweep missed; it moves onto formatDate.
The new cases assert both languages and that the two differ, instead of
comparing against the runtime's own default: that default is Hungarian on the
dev box and English on a CI image, so a one-language assertion would pass
under the old bug on half the machines. Verified in Chromium on :5173 with the
browser forced to hu-HU and the UI in English — Stats, Scheduler and the
channel table all keep comma grouping.
This commit is contained in:
@@ -1,5 +1,12 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { RELATIVE_UNITS, formatRuntime, relativeTime, relativeFromMs } from "./format";
|
||||
import {
|
||||
RELATIVE_UNITS,
|
||||
formatNumber,
|
||||
formatRuntime,
|
||||
formatTotalHours,
|
||||
relativeTime,
|
||||
relativeFromMs,
|
||||
} from "./format";
|
||||
import en from "../i18n/locales/en/time.json";
|
||||
import hu from "../i18n/locales/hu/time.json";
|
||||
|
||||
@@ -114,6 +121,48 @@ describe("relativeFromMs", () => {
|
||||
// implementation: 45s floored to "0m", so a playlist of Shorts reported no runtime while the rail
|
||||
// beside it read "0:45". The table pins every tier boundary, so the sub-minute branch cannot be
|
||||
// dropped again as "dead code" — it is the one branch a whole-playlist figure reaches.
|
||||
// A bare `n.toLocaleString()` groups in the BROWSER's locale, so an English UI in a Hungarian browser
|
||||
// printed "1 234 567" (and a Hungarian UI in an English browser "1,234,567"). The case below asserts
|
||||
// BOTH languages and that the two differ, rather than comparing against the runtime's own default:
|
||||
// the default is whatever locale the machine running the suite has (Hungarian on the dev box, English
|
||||
// on a CI image), so a one-language assertion passes under the old bug on half the machines.
|
||||
describe("formatNumber", () => {
|
||||
afterEach(() => {
|
||||
i18nStub.language = "en";
|
||||
});
|
||||
|
||||
it("groups in the UI language, not in the runtime's", () => {
|
||||
const asEnglish = formatNumber(1234567);
|
||||
i18nStub.language = "hu";
|
||||
const asHungarian = formatNumber(1234567);
|
||||
expect(asEnglish).toBe(new Intl.NumberFormat("en").format(1234567));
|
||||
expect(asHungarian).toBe(new Intl.NumberFormat("hu").format(1234567));
|
||||
// The load-bearing half: under `toLocaleString()` both come out in the runtime's one locale.
|
||||
expect(asEnglish).not.toBe(asHungarian);
|
||||
});
|
||||
|
||||
it("follows a language switch at runtime (the per-language cache is keyed, not frozen)", () => {
|
||||
expect(formatNumber(1000)).toBe(new Intl.NumberFormat("en").format(1000));
|
||||
i18nStub.language = "hu";
|
||||
expect(formatNumber(1000)).toBe(new Intl.NumberFormat("hu").format(1000));
|
||||
i18nStub.language = "en";
|
||||
expect(formatNumber(1000)).toBe(new Intl.NumberFormat("en").format(1000));
|
||||
});
|
||||
|
||||
it("renders nothing for a value it cannot read, so a caller's dash fallback covers it", () => {
|
||||
expect(formatNumber(null)).toBe("");
|
||||
expect(formatNumber(undefined)).toBe("");
|
||||
expect(formatNumber(NaN)).toBe("");
|
||||
expect(formatNumber(Infinity)).toBe("");
|
||||
expect(formatNumber(0)).toBe("0"); // a real zero is a value, not a missing one
|
||||
});
|
||||
|
||||
it("carries the UI language into the channel total-hours label", () => {
|
||||
i18nStub.language = "hu";
|
||||
expect(formatTotalHours(12345 * 3600)).toBe(`${new Intl.NumberFormat("hu").format(12345)} h`);
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatRuntime", () => {
|
||||
const CASES: [number | null | undefined, string][] = [
|
||||
[null, ""],
|
||||
|
||||
@@ -132,7 +132,28 @@ export function formatEta(seconds: number): string {
|
||||
export function formatTotalHours(sec: number): string {
|
||||
if (!sec) return "—";
|
||||
const h = Math.round(sec / 3600);
|
||||
return h >= 1 ? `${h.toLocaleString()} h` : `${Math.max(1, Math.round(sec / 60))} m`;
|
||||
return h >= 1 ? `${formatNumber(h)} h` : `${Math.max(1, Math.round(sec / 60))} m`;
|
||||
}
|
||||
|
||||
// One grouped-number formatter per language, built lazily — the same reason as the relative-time
|
||||
// formatters above: the stats tables and the feed format a number per row, and constructing an Intl
|
||||
// formatter is the expensive part. Keyed by language because the user can switch it at runtime.
|
||||
const numberFormatters = new Map<string, Intl.NumberFormat>();
|
||||
|
||||
/** A grouped number in the UI language ("1,234,567" in English, "1 234 567" in Hungarian). A bare
|
||||
* `n.toLocaleString()` groups in the BROWSER's locale instead, so an English UI in a Hungarian
|
||||
* browser showed space-grouped figures and vice versa — the number half of the same defect
|
||||
* `formatDateTime` fixes for dates. Empty string for a missing/non-finite value, so a caller's
|
||||
* `|| "—"` fallback covers it like a missing one. */
|
||||
export function formatNumber(n: number | null | undefined): string {
|
||||
if (n == null || !Number.isFinite(n)) return "";
|
||||
const lang = i18n.language;
|
||||
let formatter = numberFormatters.get(lang);
|
||||
if (!formatter) {
|
||||
formatter = new Intl.NumberFormat(lang);
|
||||
numberFormatters.set(lang, formatter);
|
||||
}
|
||||
return formatter.format(n);
|
||||
}
|
||||
|
||||
export function formatViews(n: number | null): string {
|
||||
|
||||
Reference in New Issue
Block a user