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:
@@ -8,11 +8,15 @@ import { render, screen, cleanup } from "@testing-library/react";
|
||||
import "../../i18n"; // side-effect: initialise the shared i18next instance
|
||||
import i18n from "i18next";
|
||||
import ItemCount from "./ItemCount";
|
||||
import { formatNumber } from "../../lib/format";
|
||||
|
||||
beforeAll(async () => {
|
||||
await i18n.changeLanguage("en");
|
||||
});
|
||||
afterEach(cleanup);
|
||||
afterEach(async () => {
|
||||
cleanup();
|
||||
await i18n.changeLanguage("en"); // a case that switches language must not leak it to the next
|
||||
});
|
||||
|
||||
const text = () => screen.getByTestId("item-count").textContent;
|
||||
|
||||
@@ -24,17 +28,31 @@ describe("ItemCount", () => {
|
||||
|
||||
it("pluralises and thousands-separates a normal count", () => {
|
||||
render(<ItemCount count={192045} />);
|
||||
// Separators come from toLocaleString (runtime-locale dependent) — compare against it, not a fixed
|
||||
// en-US string, so the test asserts ItemCount's formatting path rather than the CI locale.
|
||||
expect(text()).toBe(`${(192045).toLocaleString()} items`);
|
||||
// Separators come from the shared formatNumber — compare against it, not a fixed en-US string,
|
||||
// so the test asserts ItemCount's formatting path rather than the CI locale.
|
||||
expect(text()).toBe(`${formatNumber(192045)} items`);
|
||||
});
|
||||
|
||||
// The pill used `count.toLocaleString()`, which groups in the BROWSER's locale: a Hungarian UI on
|
||||
// an English browser showed "192,045" instead of "192 045". The title carries the pure number (no
|
||||
// plural string), and both languages are asserted — comparing against the runtime's own default
|
||||
// would pass under the old bug on a machine whose locale happens to match the UI language.
|
||||
it("groups in the UI language, not the browser's", async () => {
|
||||
const title = () => screen.getByTestId("item-count").getAttribute("title");
|
||||
render(<ItemCount count={192045} />);
|
||||
const asEnglish = title();
|
||||
cleanup();
|
||||
await i18n.changeLanguage("hu");
|
||||
render(<ItemCount count={192045} />);
|
||||
expect(asEnglish).toBe(new Intl.NumberFormat("en").format(192045));
|
||||
expect(title()).toBe(new Intl.NumberFormat("hu").format(192045));
|
||||
expect(title()).not.toBe(asEnglish);
|
||||
});
|
||||
|
||||
it("collapses a count of a million or more to 1M+, with the exact figure in the tooltip", () => {
|
||||
render(<ItemCount count={1_000_000} />);
|
||||
expect(text()).toBe("1M+ items"); // the "1M+" collapse is locale-independent
|
||||
expect(screen.getByTestId("item-count").getAttribute("title")).toBe(
|
||||
(1_000_000).toLocaleString(),
|
||||
);
|
||||
expect(screen.getByTestId("item-count").getAttribute("title")).toBe(formatNumber(1_000_000));
|
||||
});
|
||||
|
||||
it("shows the icon-only fixed box (empty label) while the count is undefined", () => {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Layers } from "lucide-react";
|
||||
import { formatNumber } from "../../lib/format";
|
||||
|
||||
// At a million and above we stop printing the exact figure — six digits is the readable ceiling, so
|
||||
// the pill shows "1M+" and the tooltip carries the precise number.
|
||||
@@ -20,12 +21,12 @@ export default function ItemCount({ count }: { count?: number }) {
|
||||
? ""
|
||||
: t("common.itemCount", {
|
||||
count,
|
||||
formattedCount: count >= OVERFLOW_AT ? "1M+" : count.toLocaleString(),
|
||||
formattedCount: count >= OVERFLOW_AT ? "1M+" : formatNumber(count),
|
||||
});
|
||||
return (
|
||||
<span
|
||||
data-testid="item-count"
|
||||
title={count?.toLocaleString()}
|
||||
title={count == null ? undefined : formatNumber(count)}
|
||||
className="glass-card inline-flex shrink-0 items-center justify-center gap-1.5 rounded-full border border-border px-3 py-1 text-sm text-muted min-w-[9rem] whitespace-nowrap"
|
||||
>
|
||||
<Layers className="h-3.5 w-3.5 shrink-0 text-muted" aria-hidden="true" />
|
||||
|
||||
Reference in New Issue
Block a user