diff --git a/gui/src/components/codex-account-pool-helpers.tsx b/gui/src/components/codex-account-pool-helpers.tsx index 56c439b48..9c67d1ce8 100644 --- a/gui/src/components/codex-account-pool-helpers.tsx +++ b/gui/src/components/codex-account-pool-helpers.tsx @@ -1,10 +1,10 @@ -import type { TFn } from "../i18n/shared"; +import type { Locale, TFn } from "../i18n/shared"; import { IconTicket } from "../icons"; import type { CodexAccountEntry } from "./codex-account-pool-types"; -import { daysUntil, formatCreditDate } from "./codex-account-pool-utils"; +import { daysUntil, formatCreditDate, formatCreditDateTime } from "./codex-account-pool-utils"; -export function CodexCreditItem({ index, grantedAt, expiresAt, isNext, t }: { - index: number; grantedAt: string; expiresAt: string; isNext: boolean; t: TFn; +export function CodexCreditItem({ index, grantedAt, expiresAt, isNext, locale, t }: { + index: number; grantedAt: string; expiresAt: string; isNext: boolean; locale: Locale; t: TFn; }) { const days = daysUntil(expiresAt); const urgent = days <= 7; @@ -18,8 +18,8 @@ export function CodexCreditItem({ index, grantedAt, expiresAt, isNext, t }: { {isNext && {t("codexAuth.creditNextBadge")}}
- {t("codexAuth.creditGranted", { date: formatCreditDate(grantedAt) })} - {t("codexAuth.creditExpires", { date: formatCreditDate(expiresAt), days: String(days) })} + {t("codexAuth.creditGranted", { date: formatCreditDate(grantedAt, locale) })} + {t("codexAuth.creditExpires", { date: formatCreditDateTime(expiresAt, locale), days: String(days) })}
); diff --git a/gui/src/components/codex-account-pool-utils.ts b/gui/src/components/codex-account-pool-utils.ts index 52d091eba..4462199cc 100644 --- a/gui/src/components/codex-account-pool-utils.ts +++ b/gui/src/components/codex-account-pool-utils.ts @@ -1,7 +1,14 @@ -import { formatCreditDate as formatCreditDateIntl } from "../intl-formatters"; +import { + formatCreditDate as formatCreditDateIntl, + formatCreditDateTime as formatCreditDateTimeIntl, +} from "../intl-formatters"; -export function formatCreditDate(iso: string): string { - return formatCreditDateIntl(iso); +export function formatCreditDate(iso: string, locale?: string): string { + return formatCreditDateIntl(iso, locale); +} + +export function formatCreditDateTime(iso: string, locale?: string): string { + return formatCreditDateTimeIntl(iso, locale); } export function daysUntil(iso: string): number { diff --git a/gui/src/components/codex-account-reset-modal.tsx b/gui/src/components/codex-account-reset-modal.tsx index 102c3e9ba..cc518029b 100644 --- a/gui/src/components/codex-account-reset-modal.tsx +++ b/gui/src/components/codex-account-reset-modal.tsx @@ -1,5 +1,5 @@ import { useCallback, useEffect, useRef } from "react"; -import { useT } from "../i18n/shared"; +import { useI18n } from "../i18n/shared"; import { IconAlert, IconTicket } from "../icons"; import type { CodexAccountEntry } from "./codex-account-pool-types"; import { CodexCreditItem } from "./codex-account-pool-helpers"; @@ -26,7 +26,7 @@ export function CodexAccountResetModal({ onCancelConfirm: () => void; onRedeem: () => void; }) { - const t = useT(); + const { locale, t } = useI18n(); const dialogRef = useRef(null); useEffect(() => { @@ -61,7 +61,7 @@ export function CodexAccountResetModal({ {creditDetails && creditDetails.length > 0 && (
{creditDetails.map((c, i) => ( - + ))}
)} @@ -87,7 +87,7 @@ export function CodexAccountResetModal({

{t("codexAuth.confirmResetDesc", { count: String(resetPopup.quota?.resetCredits ?? 0) })}

{creditDetails && creditDetails[0] && (

- {t("codexAuth.confirmWhichCredit", { date: formatCreditDate(creditDetails[0].granted_at) })} + {t("codexAuth.confirmWhichCredit", { date: formatCreditDate(creditDetails[0].granted_at, locale) })}

)}

{t("codexAuth.irreversible")}

diff --git a/gui/src/intl-formatters.ts b/gui/src/intl-formatters.ts index 4693170c7..e096931b7 100644 --- a/gui/src/intl-formatters.ts +++ b/gui/src/intl-formatters.ts @@ -19,16 +19,42 @@ export function cachedNumberFormat( return fmt; } -const CREDIT_DATE_FORMAT = new Intl.DateTimeFormat(undefined, { +const CREDIT_DATE_OPTIONS: Intl.DateTimeFormatOptions = { month: "short", day: "numeric", year: "numeric", -}); +}; -export function formatCreditDate(iso: string): string { +const CREDIT_DATE_TIME_OPTIONS: Intl.DateTimeFormatOptions = { + month: "short", + day: "numeric", + year: "numeric", + hour: "2-digit", + minute: "2-digit", +}; + +const dateFormatters = new Map(); + +function cachedDateFormatter(locale: string | undefined, options: Intl.DateTimeFormatOptions): Intl.DateTimeFormat { + const key = cacheKey(locale, options); + let fmt = dateFormatters.get(key); + if (!fmt) { + fmt = new Intl.DateTimeFormat(locale, options); + dateFormatters.set(key, fmt); + } + return fmt; +} + +export function formatCreditDate(iso: string, locale?: string): string { + const date = new Date(iso); + if (Number.isNaN(date.getTime())) return "\u2014"; + return cachedDateFormatter(locale, CREDIT_DATE_OPTIONS).format(date); +} + +export function formatCreditDateTime(iso: string, locale?: string): string { const date = new Date(iso); if (Number.isNaN(date.getTime())) return "\u2014"; - return CREDIT_DATE_FORMAT.format(date); + return cachedDateFormatter(locale, CREDIT_DATE_TIME_OPTIONS).format(date); } /** Format a USD cost estimate for display. Returns "—" when unavailable. */ diff --git a/gui/tests/intl-formatters.test.ts b/gui/tests/intl-formatters.test.ts new file mode 100644 index 000000000..fa38faace --- /dev/null +++ b/gui/tests/intl-formatters.test.ts @@ -0,0 +1,24 @@ +import { describe, expect, test } from "bun:test"; +import { formatCreditDate, formatCreditDateTime } from "../src/intl-formatters"; + +describe("credit date formatting", () => { + test("keeps the compact date format for grant dates", () => { + const iso = "2026-07-31T12:34:56Z"; + const time = new Intl.DateTimeFormat("de-DE", { hour: "2-digit", minute: "2-digit" }).format(new Date(iso)); + + expect(formatCreditDate(iso, "de-DE")).not.toContain(time); + }); + + test("includes the local time for expiration dates", () => { + const iso = "2026-07-31T12:34:56Z"; + + const time = new Intl.DateTimeFormat("de-DE", { hour: "2-digit", minute: "2-digit" }).format(new Date(iso)); + + expect(formatCreditDateTime(iso, "de-DE")).toContain(time); + expect(formatCreditDateTime(iso, "de-DE")).not.toBe("—"); + }); + + test("handles invalid dates consistently", () => { + expect(formatCreditDateTime("invalid")).toBe("—"); + }); +});