feat(ui): format dashboard numbers, money and dates through i18next (#50443)

* feat(ui): format dashboard values through i18next formatters

Register named formats on i18next's formatter (usd, compact, duration,
relativetime, number and five date presets) with a cache keyed on the
language and options, add format.* keys to en.json, and move the usage
stat strip onto them.

* fix(ui): keep number formats in USD and the format cache bounded

Force USD on the number and compact formats, clean underscore language
codes like i18next does, fall back when options cannot be serialized,
keep formatted placeholders alone in format keys, and pin the remaining
stat strip values in the German test.

* refactor(ui): format durations with Intl.DurationFormat and load its polyfill on demand

Browsers without Intl.DurationFormat (Chrome and Edge before 129, Firefox before 136) load the pinned FormatJS polyfill through a dynamic import and re-render translations once it is installed. The milliseconds-to-units split and the 0s fallback stay, so the output matches the previous hand-built formatter.

* test(ui): cover duration edge cases on the polyfill path

* refactor(ui): drop the unused duration format and its polyfill

Nothing outside tests called format.duration, and its 1m 15s style doesn't match the decimal-second latencies the dashboard shows today. A duration format will come with the pages that need it, in the style they already use.
This commit is contained in:
yuneng-jiang 2026-11-06 14:59:15 -08:00 • committed by GitHub
parent e555a79cee
commit 77b4644523
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 333 additions and 22 deletions

View file

@ -1,6 +1,7 @@
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it } from "vitest";
import { afterEach, describe, expect, it } from "vitest";
import i18n from "@/i18n/i18n";
import type { OverviewTotals } from "./overviewData";
import UsageStatStrip from "./UsageStatStrip";
@ -19,6 +20,15 @@ const totals: OverviewTotals = {
successRate: 80,
};
const asRendered = (text: string) => text.replace(/\s+/g, " ");
const SECONDS = {
style: "unit",
unit: "second",
unitDisplay: "narrow",
minimumFractionDigits: 1,
maximumFractionDigits: 1,
} as const;
const defaultProps = {
results: [],
totals,
@ -58,3 +68,58 @@ describe("UsageStatStrip token breakdown", () => {
expect(screen.queryByText("failed")).not.toBeInTheDocument();
});
});
describe("UsageStatStrip in another UI language", () => {
afterEach(async () => {
await i18n.changeLanguage("en");
i18n.removeResourceBundle("de", "translation");
});
it("should format every value in the selected language and keep money in US dollars", async () => {
i18n.addResourceBundle("de", "translation", {});
await i18n.changeLanguage("de");
const german = new Intl.NumberFormat("de");
const dollars = new Intl.NumberFormat("de", { style: "currency", currency: "USD" });
const euros = new Intl.NumberFormat("de", { style: "currency", currency: "EUR" });
const dollarsToFourPlaces = new Intl.NumberFormat("de", {
style: "currency",
currency: "USD",
minimumFractionDigits: 4,
});
const oneDecimal = new Intl.NumberFormat("de", { minimumFractionDigits: 1 });
const milliseconds = new Intl.NumberFormat("de", { style: "unit", unit: "millisecond", unitDisplay: "narrow" });
render(<UsageStatStrip {...defaultProps} budget={1500} />);
expect(screen.getByText(asRendered(dollars.format(42.5)))).toBeInTheDocument();
expect(screen.getByText(asRendered(`of ${dollars.format(1500)} budget`))).toBeInTheDocument();
expect(screen.getByText(asRendered(dollarsToFourPlaces.format(4.25)))).toBeInTheDocument();
expect(screen.getByText(asRendered(milliseconds.format(250)))).toBeInTheDocument();
expect(screen.getByText(`${oneDecimal.format(80)}% success rate`)).toBeInTheDocument();
expect(screen.getByTitle(`${german.format(9123456)} tokens`)).toBeInTheDocument();
expect(screen.queryByText(asRendered(euros.format(42.5)))).not.toBeInTheDocument();
});
it("should format large counts, compact tokens, seconds and the token breakdown in the selected language", async () => {
const user = userEvent.setup();
i18n.addResourceBundle("de", "translation", {});
await i18n.changeLanguage("de");
const german = new Intl.NumberFormat("de");
const compact = new Intl.NumberFormat("de", { notation: "compact", maximumFractionDigits: 1 });
const large = { ...totals, requests: 8123456, successful: 7654321, failed: 469135, avgLatencyMs: 1234 };
render(<UsageStatStrip {...defaultProps} totals={large} />);
expect(screen.getByText(german.format(8123456))).toBeInTheDocument();
expect(screen.getByText(german.format(7654321))).toBeInTheDocument();
expect(screen.getByText(german.format(469135))).toBeInTheDocument();
expect(screen.getByText(asRendered(compact.format(9123456)))).toBeInTheDocument();
expect(screen.getByText(asRendered(new Intl.NumberFormat("de", SECONDS).format(1.234)))).toBeInTheDocument();
await user.click(screen.getByRole("button", { name: "View token breakdown" }));
const breakdown = await screen.findByRole("dialog", { name: "What counts toward total tokens?" });
[8123456, 5123456, 2000000, 1000000, 9123456].forEach((count) =>
expect(breakdown).toHaveTextContent(german.format(count)),
);
});
});

View file

@ -1,14 +1,19 @@
"use client";
import React, { useMemo } from "react";
import type { TFunction } from "i18next";
import { Trans, useTranslation } from "react-i18next";
import type { DailyData } from "@/components/UsagePage/types";
import { cn } from "@/lib/cva.config";
import { Popover, PopoverContent, PopoverTitle, PopoverTrigger } from "@/components/ui/popover";
import { dailyTotals, formatCompact, formatLatency, formatUsd, type OverviewTotals } from "./overviewData";
import { dailyTotals, type OverviewTotals } from "./overviewData";
import { PANEL_INSET_X, Sparkline, Stat } from "./Primitives";
const BRAND = "#2b3fd6";
const ONE_DECIMAL = { minimumFractionDigits: 1, maximumFractionDigits: 1 } as const;
const FOUR_DECIMALS = { minimumFractionDigits: 4, maximumFractionDigits: 4 } as const;
const MILLISECONDS = { style: "unit", unit: "millisecond", unitDisplay: "narrow", maximumFractionDigits: 0 } as const;
const SECONDS = { ...ONE_DECIMAL, style: "unit", unit: "second", unitDisplay: "narrow" } as const;
interface UsageStatStripProps {
results: readonly DailyData[];
@ -33,15 +38,19 @@ export default function UsageStatStrip({
<StatCell trend={<Sparkline data={daily} dataKey="spend" color={BRAND} className="h-14" />}>
<Stat
label={t("usage.statStrip.spend")}
value={formatUsd(totals.spend)}
value={t("format.usd", { val: totals.spend })}
pending={loading}
hint={budget ? t("usage.statStrip.budgetHint", { budget: formatUsd(budget) }) : t("usage.statStrip.noBudget")}
hint={
budget
? t("usage.statStrip.budgetHint", { budget: t("format.usd", { val: budget }) })
: t("usage.statStrip.noBudget")
}
/>
</StatCell>
<StatCell trend={<Sparkline data={daily} dataKey="requests" color={BRAND} className="h-14" />}>
<Stat
label={t("usage.statStrip.totalRequests")}
value={totals.requests.toLocaleString()}
value={t("format.number", { val: totals.requests })}
pending={requestCountsPending}
/>
{!requestCountsPending && (
@ -50,7 +59,9 @@ export default function UsageStatStrip({
<Trans
i18nKey="usage.statStrip.successfulCount"
components={{
count: <span className="text-success tabular-nums">{totals.successful.toLocaleString()}</span>,
count: (
<span className="text-success tabular-nums">{t("format.number", { val: totals.successful })}</span>
),
}}
/>
</span>
@ -60,7 +71,7 @@ export default function UsageStatStrip({
components={{
count: (
<span className={totals.failed > 0 ? "text-destructive tabular-nums" : "tabular-nums"}>
{totals.failed.toLocaleString()}
{t("format.number", { val: totals.failed })}
</span>
),
}}
@ -72,11 +83,15 @@ export default function UsageStatStrip({
<StatCell trend={<Sparkline data={daily} dataKey="tokens" color={BRAND} className="h-14" />}>
<Stat
label={t("usage.statStrip.totalTokens")}
value={loading ? null : formatCompact(totals.tokens)}
exact={t("usage.statStrip.exactTokens", { tokens: totals.tokens.toLocaleString() })}
value={loading ? null : t("format.compactNumber", { val: totals.tokens })}
exact={t("usage.statStrip.exactTokens", { tokens: t("format.number", { val: totals.tokens }) })}
pending={loading}
hint={
loading ? undefined : t("usage.statStrip.cacheReadHint", { tokens: formatCompact(totals.cacheReadTokens) })
loading
? undefined
: t("usage.statStrip.cacheReadHint", {
tokens: t("format.compactNumber", { val: totals.cacheReadTokens }),
})
}
/>
{!loading && (
@ -89,23 +104,23 @@ export default function UsageStatStrip({
<dl className="space-y-2 text-sm tabular-nums">
<div className="flex justify-between gap-4">
<dt>{t("usage.statStrip.tokenPopover.inputTokens")}</dt>
<dd>{totals.inputTokens.toLocaleString()}</dd>
<dd>{t("format.number", { val: totals.inputTokens })}</dd>
</div>
<div className="flex justify-between gap-4 pl-3 text-xs text-muted-foreground">
<dt>{t("usage.statStrip.tokenPopover.cacheReads")}</dt>
<dd>{totals.cacheReadTokens.toLocaleString()}</dd>
<dd>{t("format.number", { val: totals.cacheReadTokens })}</dd>
</div>
<div className="flex justify-between gap-4 pl-3 text-xs text-muted-foreground">
<dt>{t("usage.statStrip.tokenPopover.cacheWrites")}</dt>
<dd>{totals.cacheWriteTokens.toLocaleString()}</dd>
<dd>{t("format.number", { val: totals.cacheWriteTokens })}</dd>
</div>
<div className="flex justify-between gap-4">
<dt>{t("usage.statStrip.tokenPopover.outputTokens")}</dt>
<dd>{totals.outputTokens.toLocaleString()}</dd>
<dd>{t("format.number", { val: totals.outputTokens })}</dd>
</div>
<div className="flex justify-between gap-4 border-t pt-2 font-medium">
<dt>{t("usage.statStrip.tokenPopover.totalTokens")}</dt>
<dd>{totals.tokens.toLocaleString()}</dd>
<dd>{t("format.number", { val: totals.tokens })}</dd>
</div>
</dl>
<p className="text-xs leading-5 text-muted-foreground">{t("usage.statStrip.tokenPopover.pricingNote")}</p>
@ -118,17 +133,19 @@ export default function UsageStatStrip({
<div className="grid gap-3">
<Stat
label={t("usage.statStrip.avgCostPerRequest")}
value={formatUsd(totals.avgCostPerRequest, 4)}
value={t("format.usd", { val: totals.avgCostPerRequest, ...FOUR_DECIMALS })}
pending={loading}
/>
<Stat
label={t("usage.statStrip.avgLatency")}
value={totals.avgLatencyMs === null ? "—" : formatLatency(totals.avgLatencyMs)}
value={totals.avgLatencyMs === null ? "—" : latency(totals.avgLatencyMs, t)}
pending={loading}
hint={
totals.successRate === null
? undefined
: t("usage.statStrip.successRateHint", { rate: totals.successRate.toFixed(1) })
: t("usage.statStrip.successRateHint", {
rate: t("format.number", { val: totals.successRate, ...ONE_DECIMAL }),
})
}
/>
</div>
@ -137,6 +154,12 @@ export default function UsageStatStrip({
);
}
function latency(ms: number, t: TFunction): string {
return ms < 1000
? t("format.number", { val: ms, ...MILLISECONDS })
: t("format.number", { val: ms / 1000, ...SECONDS });
}
function StatCell({ children, trend }: { children: React.ReactNode; trend?: React.ReactNode }) {
return (
<div

View file

@ -249,8 +249,6 @@ export const formatCompactUsd = (value: number): string => {
export const formatCompact = (value: number): string =>
Intl.NumberFormat("en-US", { notation: "compact", maximumFractionDigits: 1 }).format(value);
export const formatLatency = (ms: number): string => (ms < 1000 ? `${Math.round(ms)}ms` : `${(ms / 1000).toFixed(1)}s`);
export interface OverviewTotals {
spend: number;
requests: number;

View file

@ -0,0 +1,131 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { cachedFormat } from "./formats";
import i18n from "./i18n";
import en from "./locales/en.json";
type Messages = { readonly [key: string]: string | Messages };
const entries = (node: Messages, prefix: string): ReadonlyArray<readonly [string, string]> =>
Object.entries(node).flatMap(([key, value]) =>
typeof value === "string" ? [[`${prefix}${key}`, value] as const] : entries(value, `${prefix}${key}.`),
);
const strings = (node: Messages): readonly string[] => entries(node, "").map(([, text]) => text);
const formatsInEnglish = [
...new Set(strings(en).flatMap((text) => [...text.matchAll(/\{\{\s*\w+\s*,\s*([^}]+?)\s*\}\}/g)].map(([, f]) => f))),
];
const useLanguage = async (language: string) => {
i18n.addResourceBundle(language, "translation", {});
await i18n.changeLanguage(language);
};
afterEach(async () => {
await i18n.changeLanguage("en");
["de", "zh-CN", "ja", "fr", "en-GB"].forEach((language) => i18n.removeResourceBundle(language, "translation"));
});
describe("format keys in English", () => {
it("formats numbers and USD the way the dashboard shows them today", () => {
expect(i18n.t("format.number", { val: 9123456 })).toBe("9,123,456");
expect(i18n.t("format.number", { val: 80, minimumFractionDigits: 1, maximumFractionDigits: 1 })).toBe("80.0");
expect(i18n.t("format.compactNumber", { val: 75000 })).toBe("75K");
expect(i18n.t("format.compactNumber", { val: 41_730_000 })).toBe("41.7M");
expect(i18n.t("format.compactNumber", { val: 75000, notation: "standard" })).toBe("75K");
expect(i18n.t("format.usd", { val: 1234.5 })).toBe("$1,234.50");
expect(i18n.t("format.usd", { val: 0.0838, minimumFractionDigits: 4, maximumFractionDigits: 4 })).toBe("$0.0838");
});
it("formats dates in local time with a 24-hour clock that starts at 00", () => {
const morning = new Date(2026, 8, 5, 4, 3, 2);
expect(i18n.t("format.date", { val: morning })).toBe("Sep 5, 2026");
expect(i18n.t("format.date", { val: morning, month: "long" })).toBe("Sep 5, 2026");
expect(i18n.t("format.monthDay", { val: morning })).toBe("Sep 5");
expect(i18n.t("format.monthDayTime", { val: morning })).toBe("Sep 5, 04:03:02");
expect(i18n.t("format.timestamp", { val: morning })).toBe("Sep 5, 2026, 04:03:02");
expect(i18n.t("format.numericDateTime", { val: morning })).toBe("9/5/2026, 4:03:02 AM");
expect(i18n.t("format.monthDayTime", { val: new Date(2026, 0, 2, 0, 0, 5) })).toBe("Jan 2, 00:00:05");
});
it("accepts epoch milliseconds and ISO strings as dates, and a time zone from the caller", () => {
const iso = "2026-09-05T12:30:00Z";
const expected = i18n.t("format.timestamp", { val: new Date(iso) });
expect(i18n.t("format.timestamp", { val: iso })).toBe(expected);
expect(i18n.t("format.timestamp", { val: Date.parse(iso) })).toBe(expected);
expect(i18n.t("format.timestamp", { val: iso, timeZone: "UTC" })).toBe("Sep 5, 2026, 12:30:00");
});
it("formats relative time in the unit the caller names", () => {
expect(i18n.t("format.relativeTime", { val: -3 })).toBe("3 days ago");
expect(i18n.t("format.relativeTime", { val: 2, range: "hour" })).toBe("in 2 hours");
});
it("only names formatters that are registered", () => {
expect(formatsInEnglish.length).toBeGreaterThan(0);
const unformatted = formatsInEnglish.filter(
(format) => typeof i18n.services.formatter?.format(0, format, "en", {}) !== "string",
);
expect(unformatted).toEqual([]);
});
it("keeps every formatted placeholder alone in a format key", () => {
const formatted = entries(en, "").filter(([, text]) => /\{\{[^}]*,[^}]*\}\}/.test(text));
expect(formatted.length).toBeGreaterThan(0);
const mixed = formatted.filter(([key, text]) => !key.startsWith("format.") || !/^\{\{val, [a-z]+\}\}$/.test(text));
expect(mixed).toEqual([]);
});
it("keeps a currency asked of the number formats in US dollars", () => {
expect(i18n.t("format.number", { val: 1234.5, style: "currency", currency: "EUR" })).toBe("$1,234.50");
expect(i18n.t("format.compactNumber", { val: 1_234_567, style: "currency", currency: "EUR" })).toBe("$1.2M");
});
it("reads underscore language codes the way i18next does", () => {
expect(i18n.t("format.number", { val: 1234.5, lng: "de_DE" })).toBe(new Intl.NumberFormat("de-DE").format(1234.5));
});
it("still formats when the options cannot be serialized", () => {
const circular: { self?: unknown } = {};
circular.self = circular;
expect(i18n.t("format.number", { val: 1234.5, circular })).toBe("1,234.5");
});
it("builds one formatter per language and options, not one per value", () => {
const build = vi.fn((lng: string) => (value: number) => `${lng} ${value}`);
i18n.services.formatter?.add("probe", cachedFormat(build));
const render = (val: number, lng: string) =>
i18n.services.interpolator.interpolate("{{val, probe}}", { val }, lng, {});
expect([1, 2, 3].map((val) => render(val, "en"))).toEqual(["en 1", "en 2", "en 3"]);
expect(build).toHaveBeenCalledTimes(1);
expect(render(4, "de")).toBe("de 4");
expect(build).toHaveBeenCalledTimes(2);
});
});
describe("format keys in another UI language", () => {
it("follows the selected UI language even when its bundle has no format keys", async () => {
const english = i18n.t("format.number", { val: 9123456.5 });
await useLanguage("de");
const german = i18n.t("format.number", { val: 9123456.5 });
expect(german).toBe(new Intl.NumberFormat("de").format(9123456.5));
expect(german).not.toBe(english);
expect(i18n.t("format.relativeTime", { val: -3 })).toBe(new Intl.RelativeTimeFormat("de").format(-3, "day"));
});
it.each([
["de", "EUR"],
["zh-CN", "CNY"],
["ja", "JPY"],
["fr", "EUR"],
["en-GB", "GBP"],
])("keeps money in US dollars in %s", async (language, localCurrency) => {
await useLanguage(language);
const shown = i18n.t("format.usd", { val: 1234.5, currency: localCurrency });
expect(shown).toBe(new Intl.NumberFormat(language, { style: "currency", currency: "USD" }).format(1234.5));
expect(shown).not.toBe(
new Intl.NumberFormat(language, { style: "currency", currency: localCurrency }).format(1234.5),
);
});
});

View file

@ -0,0 +1,80 @@
import type { Formatter } from "i18next";
import { DEFAULT_LANGUAGE } from "./languages";
type DateInput = Date | number | string;
type InterpolationOptions = { readonly interpolationkey?: string };
type Build<Value, Options> = (lng: string, options: Options) => (value: Value) => string;
const cacheKey = (language: string, options: InterpolationOptions): string | null => {
try {
return (
language + JSON.stringify(options, (name, option) => (name === options.interpolationkey ? undefined : option))
);
} catch {
return null;
}
};
export const cachedFormat = <Value, Options extends object>(build: Build<Value, Options>) => {
const renders = new Map<string, (value: Value) => string>();
return (value: Value, lng: string | undefined, options: Options & InterpolationOptions): string => {
const language = (lng ?? DEFAULT_LANGUAGE).replaceAll("_", "-");
const key = cacheKey(language, options);
if (key === null) return build(language, options)(value);
const existing = renders.get(key);
if (existing) return existing(value);
const render = build(language, options);
renders.set(key, render);
return render(value);
};
};
const dateFormat = (preset: Intl.DateTimeFormatOptions) =>
cachedFormat((lng, options: Intl.DateTimeFormatOptions) => {
const format = new Intl.DateTimeFormat(lng, { ...options, ...preset });
return (value: DateInput) => format.format(new Date(value));
});
const USD = { style: "currency", currency: "USD", currencyDisplay: "symbol" } as const;
const COMPACT = { notation: "compact", maximumFractionDigits: 1 } as const;
const MONTH_DAY = { month: "short", day: "numeric" } as const;
const DATE = { ...MONTH_DAY, year: "numeric" } as const;
const CLOCK = { hour: "2-digit", minute: "2-digit", second: "2-digit", hourCycle: "h23" } as const;
const NUMERIC_DATE_TIME = {
year: "numeric",
month: "numeric",
day: "numeric",
hour: "numeric",
minute: "2-digit",
second: "2-digit",
} as const;
const FORMATS = {
number: cachedFormat((lng, options: Intl.NumberFormatOptions) => {
const format = new Intl.NumberFormat(lng, { ...options, currency: USD.currency });
return (value: number) => format.format(value);
}),
compact: cachedFormat((lng, options: Intl.NumberFormatOptions) => {
const format = new Intl.NumberFormat(lng, { ...options, ...COMPACT, currency: USD.currency });
return (value: number) => format.format(value);
}),
usd: cachedFormat((lng, options: Intl.NumberFormatOptions) => {
const format = new Intl.NumberFormat(lng, { ...options, ...USD });
return (value: number) => format.format(value);
}),
relativetime: cachedFormat(
(lng, options: Intl.RelativeTimeFormatOptions & { range?: Intl.RelativeTimeFormatUnit }) => {
const format = new Intl.RelativeTimeFormat(lng, options);
return (value: number) => format.format(value, options.range ?? "day");
},
),
date: dateFormat(DATE),
monthday: dateFormat(MONTH_DAY),
monthdaytime: dateFormat({ ...MONTH_DAY, ...CLOCK }),
timestamp: dateFormat({ ...DATE, ...CLOCK }),
numericdatetime: dateFormat(NUMERIC_DATE_TIME),
};
export const registerFormats = (formatter: Formatter): void =>
Object.entries(FORMATS).forEach(([name, format]) => formatter.add(name, format));

View file

@ -1,6 +1,7 @@
import i18n, { type InitOptions } from "i18next";
import { initReactI18next } from "react-i18next";
import { registerFormats } from "./formats";
import en from "./locales/en.json";
declare module "i18next" {
@ -20,4 +21,6 @@ const options: InitOptions = {
void i18n.use(initReactI18next).init(options);
if (i18n.services.formatter) registerFormats(i18n.services.formatter);
export default i18n;

View file

@ -29,11 +29,11 @@ describe("en.json", () => {
expect(entries.filter(([, value]) => typeof value !== "string" || value === "").map(([key]) => key)).toEqual([]);
});
it("only uses placeholders named like identifiers", () => {
it("only uses placeholders named like identifiers, with an optional lowercase format", () => {
expect(entries.some(([, value]) => String(value).includes("{{"))).toBe(true);
const malformed = entries.filter(([, value]) => {
const text = String(value);
const wellFormed = text.match(/\{\{[A-Za-z_][A-Za-z0-9_]*\}\}/g)?.length ?? 0;
const wellFormed = text.match(/\{\{[A-Za-z_][A-Za-z0-9_]*(?:, [a-z]+)?\}\}/g)?.length ?? 0;
return text.split("{{").length - 1 !== wellFormed || text.split("}}").length - 1 !== wellFormed;
});
expect(malformed.map(([key]) => key)).toEqual([]);

View file

@ -1,4 +1,15 @@
{
"format": {
"number": "{{val, number}}",
"compactNumber": "{{val, compact}}",
"usd": "{{val, usd}}",
"relativeTime": "{{val, relativetime}}",
"date": "{{val, date}}",
"monthDay": "{{val, monthday}}",
"monthDayTime": "{{val, monthdaytime}}",
"timestamp": "{{val, timestamp}}",
"numericDateTime": "{{val, numericdatetime}}"
},
"agents": {
"killSwitchConfig": {
"authTypes": {