From e16c92eb8de29d51d1635d0489573bad652f6ea6 Mon Sep 17 00:00:00 2001 From: Charly M Date: Tue, 28 Jul 2026 12:15:57 +0200 Subject: [PATCH 1/2] feat(gui): show reset credit expiration time --- .../components/codex-account-pool-helpers.tsx | 4 ++-- .../components/codex-account-pool-utils.ts | 9 ++++++++- gui/src/intl-formatters.ts | 14 ++++++++++++++ gui/tests/intl-formatters.test.ts | 19 +++++++++++++++++++ 4 files changed, 43 insertions(+), 3 deletions(-) create mode 100644 gui/tests/intl-formatters.test.ts diff --git a/gui/src/components/codex-account-pool-helpers.tsx b/gui/src/components/codex-account-pool-helpers.tsx index 56c439b48..f8be4c13e 100644 --- a/gui/src/components/codex-account-pool-helpers.tsx +++ b/gui/src/components/codex-account-pool-helpers.tsx @@ -1,7 +1,7 @@ import type { 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; @@ -19,7 +19,7 @@ export function CodexCreditItem({ index, grantedAt, expiresAt, isNext, t }: {
{t("codexAuth.creditGranted", { date: formatCreditDate(grantedAt) })} - {t("codexAuth.creditExpires", { date: formatCreditDate(expiresAt), days: String(days) })} + {t("codexAuth.creditExpires", { date: formatCreditDateTime(expiresAt), 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..24581bd37 100644 --- a/gui/src/components/codex-account-pool-utils.ts +++ b/gui/src/components/codex-account-pool-utils.ts @@ -1,9 +1,16 @@ -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 formatCreditDateTime(iso: string): string { + return formatCreditDateTimeIntl(iso); +} + export function daysUntil(iso: string): number { return Math.max(0, Math.ceil((new Date(iso).getTime() - Date.now()) / 86_400_000)); } diff --git a/gui/src/intl-formatters.ts b/gui/src/intl-formatters.ts index 4693170c7..cc0bc39b3 100644 --- a/gui/src/intl-formatters.ts +++ b/gui/src/intl-formatters.ts @@ -25,12 +25,26 @@ const CREDIT_DATE_FORMAT = new Intl.DateTimeFormat(undefined, { year: "numeric", }); +const CREDIT_DATE_TIME_FORMAT = new Intl.DateTimeFormat(undefined, { + month: "short", + day: "numeric", + year: "numeric", + hour: "2-digit", + minute: "2-digit", +}); + export function formatCreditDate(iso: string): string { const date = new Date(iso); if (Number.isNaN(date.getTime())) return "\u2014"; return CREDIT_DATE_FORMAT.format(date); } +export function formatCreditDateTime(iso: string): string { + const date = new Date(iso); + if (Number.isNaN(date.getTime())) return "\u2014"; + return CREDIT_DATE_TIME_FORMAT.format(date); +} + /** Format a USD cost estimate for display. Returns "—" when unavailable. */ export function formatEstimatedUsdValue(value: number, locale?: string): string { if (!Number.isFinite(value) || value < 0) return "\u2014"; diff --git a/gui/tests/intl-formatters.test.ts b/gui/tests/intl-formatters.test.ts new file mode 100644 index 000000000..75cd21f20 --- /dev/null +++ b/gui/tests/intl-formatters.test.ts @@ -0,0 +1,19 @@ +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", () => { + expect(formatCreditDate("2026-07-31T12:34:56Z")).not.toContain("12:34"); + }); + + test("includes the local time for expiration dates", () => { + const iso = "2026-07-31T12:34:56Z"; + + expect(formatCreditDateTime(iso)).not.toBe(formatCreditDate(iso)); + expect(formatCreditDateTime(iso)).not.toBe("—"); + }); + + test("handles invalid dates consistently", () => { + expect(formatCreditDateTime("invalid")).toBe("—"); + }); +}); From cece55618108f3c9512a2bbb12a698328ef505ec Mon Sep 17 00:00:00 2001 From: Charly M Date: Tue, 28 Jul 2026 12:45:10 +0200 Subject: [PATCH 2/2] fix(gui): respect selected locale for credit dates --- .../components/codex-account-pool-helpers.tsx | 10 +++---- .../components/codex-account-pool-utils.ts | 8 +++--- .../components/codex-account-reset-modal.tsx | 8 +++--- gui/src/intl-formatters.ts | 28 +++++++++++++------ gui/tests/intl-formatters.test.ts | 11 ++++++-- 5 files changed, 41 insertions(+), 24 deletions(-) diff --git a/gui/src/components/codex-account-pool-helpers.tsx b/gui/src/components/codex-account-pool-helpers.tsx index f8be4c13e..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, 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: formatCreditDateTime(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 24581bd37..4462199cc 100644 --- a/gui/src/components/codex-account-pool-utils.ts +++ b/gui/src/components/codex-account-pool-utils.ts @@ -3,12 +3,12 @@ import { 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): string { - return formatCreditDateTimeIntl(iso); +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 cc0bc39b3..e096931b7 100644 --- a/gui/src/intl-formatters.ts +++ b/gui/src/intl-formatters.ts @@ -19,30 +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", -}); +}; -const CREDIT_DATE_TIME_FORMAT = new Intl.DateTimeFormat(undefined, { +const CREDIT_DATE_TIME_OPTIONS: Intl.DateTimeFormatOptions = { month: "short", day: "numeric", year: "numeric", hour: "2-digit", minute: "2-digit", -}); +}; -export function formatCreditDate(iso: string): string { +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 CREDIT_DATE_FORMAT.format(date); + return cachedDateFormatter(locale, CREDIT_DATE_OPTIONS).format(date); } -export function formatCreditDateTime(iso: string): string { +export function formatCreditDateTime(iso: string, locale?: string): string { const date = new Date(iso); if (Number.isNaN(date.getTime())) return "\u2014"; - return CREDIT_DATE_TIME_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 index 75cd21f20..fa38faace 100644 --- a/gui/tests/intl-formatters.test.ts +++ b/gui/tests/intl-formatters.test.ts @@ -3,14 +3,19 @@ import { formatCreditDate, formatCreditDateTime } from "../src/intl-formatters"; describe("credit date formatting", () => { test("keeps the compact date format for grant dates", () => { - expect(formatCreditDate("2026-07-31T12:34:56Z")).not.toContain("12:34"); + 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"; - expect(formatCreditDateTime(iso)).not.toBe(formatCreditDate(iso)); - expect(formatCreditDateTime(iso)).not.toBe("—"); + 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", () => {