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("—");
+ });
+});