Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions gui/src/components/codex-account-pool-helpers.tsx
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -18,8 +18,8 @@ export function CodexCreditItem({ index, grantedAt, expiresAt, isNext, t }: {
{isNext && <span className="badge badge-amber text-micro" style={{ padding: "1px 6px" }}>{t("codexAuth.creditNextBadge")}</span>}
</div>
<div className="credit-item-dates">
<span>{t("codexAuth.creditGranted", { date: formatCreditDate(grantedAt) })}</span>
<span className={urgent ? "credit-urgent" : ""}>{t("codexAuth.creditExpires", { date: formatCreditDate(expiresAt), days: String(days) })}</span>
<span>{t("codexAuth.creditGranted", { date: formatCreditDate(grantedAt, locale) })}</span>
<span className={urgent ? "credit-urgent" : ""}>{t("codexAuth.creditExpires", { date: formatCreditDateTime(expiresAt, locale), days: String(days) })}</span>
</div>
</div>
);
Expand Down
13 changes: 10 additions & 3 deletions gui/src/components/codex-account-pool-utils.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down
8 changes: 4 additions & 4 deletions gui/src/components/codex-account-reset-modal.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -26,7 +26,7 @@ export function CodexAccountResetModal({
onCancelConfirm: () => void;
onRedeem: () => void;
}) {
const t = useT();
const { locale, t } = useI18n();
const dialogRef = useRef<HTMLDialogElement>(null);

useEffect(() => {
Expand Down Expand Up @@ -61,7 +61,7 @@ export function CodexAccountResetModal({
{creditDetails && creditDetails.length > 0 && (
<div className="credit-list">
{creditDetails.map((c, i) => (
<CodexCreditItem key={`${c.granted_at}:${c.expires_at}`} index={i} grantedAt={c.granted_at} expiresAt={c.expires_at} isNext={i === 0} t={t} />
<CodexCreditItem key={`${c.granted_at}:${c.expires_at}`} index={i} grantedAt={c.granted_at} expiresAt={c.expires_at} isNext={i === 0} locale={locale} t={t} />
))}
</div>
)}
Expand All @@ -87,7 +87,7 @@ export function CodexAccountResetModal({
<p className="modal-desc">{t("codexAuth.confirmResetDesc", { count: String(resetPopup.quota?.resetCredits ?? 0) })}</p>
{creditDetails && creditDetails[0] && (
<p className="faint text-label">
{t("codexAuth.confirmWhichCredit", { date: formatCreditDate(creditDetails[0].granted_at) })}
{t("codexAuth.confirmWhichCredit", { date: formatCreditDate(creditDetails[0].granted_at, locale) })}
</p>
)}
<p className="faint text-label">{t("codexAuth.irreversible")}</p>
Expand Down
34 changes: 30 additions & 4 deletions gui/src/intl-formatters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, Intl.DateTimeFormat>();

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. */
Expand Down
24 changes: 24 additions & 0 deletions gui/tests/intl-formatters.test.ts
Original file line number Diff line number Diff line change
@@ -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("—");
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.

test("handles invalid dates consistently", () => {
expect(formatCreditDateTime("invalid")).toBe("—");
});
});
Loading