fix(dashboard): Persian Jalali formatting for resource credit dates

Render the prepaid resource credit "remaining" and "expires" text in
Persian with Jalali calendar when locale is fa: localized digits and
day/hour/minute units for time left, and weekday + Jalali date + time
for the expiry. Parts are assembled explicitly so order is stable
regardless of the runtime's ICU pattern data.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
keyhan
2026-06-20 00:15:43 +03:30
parent 49f1ab07bd
commit bc224a2291
2 changed files with 64 additions and 7 deletions
+59 -3
View File
@@ -31,11 +31,67 @@ export function formatRemainingDurationMs(remainingMs: number): string {
return 'less than 1m';
}
/** Expiry timestamp in the user's locale and timezone. */
export function formatExpiresAtLocal(expiresAt: string | Date): string {
/** Persian (با ارقام فارسی) human-readable time left, e.g. «۲۹ روز و ۱۹ ساعت و ۵۱ دقیقه». */
function formatRemainingDurationFa(remainingMs: number): string {
const ms = Math.max(0, remainingMs);
const days = Math.floor(ms / 86400000);
const hours = Math.floor((ms % 86400000) / 3600000);
const minutes = Math.floor((ms % 3600000) / 60000);
const fa = (n: number) => n.toLocaleString('fa-IR', { useGrouping: false });
const parts: string[] = [];
if (days > 0) parts.push(`${fa(days)} روز`);
if (hours > 0) parts.push(`${fa(hours)} ساعت`);
if (minutes > 0) parts.push(`${fa(minutes)} دقیقه`);
if (parts.length === 0) return 'کمتر از ۱ دقیقه';
return parts.join(' و ');
}
/** Locale-aware time left from an expiry timestamp. */
export function formatRemainingForLocale(
expiresAt: string | Date,
locale: string,
): string {
const date = typeof expiresAt === 'string' ? new Date(expiresAt) : expiresAt;
if (Number.isNaN(date.getTime())) return '—';
return date.toLocaleString(undefined, {
const remainingMs = date.getTime() - Date.now();
return locale === 'fa'
? formatRemainingDurationFa(remainingMs)
: formatRemainingDurationMs(remainingMs);
}
/**
* Expiry timestamp in the user's locale and timezone. For Persian it renders the
* Jalali date with weekday and time, e.g. «یکشنبه، ۲۹ تیر ۱۴۰۵، ساعت ۱۹:۵۸».
*/
export function formatExpiresAtLocal(
expiresAt: string | Date,
locale?: string,
): string {
const date = typeof expiresAt === 'string' ? new Date(expiresAt) : expiresAt;
if (Number.isNaN(date.getTime())) return '—';
if (locale === 'fa') {
// Assemble parts explicitly so the order is «روز هفته، روز ماه سال» regardless
// of the runtime's ICU pattern data.
const parts = new Intl.DateTimeFormat('fa-IR-u-ca-persian', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
}).formatToParts(date);
const get = (type: Intl.DateTimeFormatPartTypes) =>
parts.find((p) => p.type === type)?.value ?? '';
const weekday = get('weekday');
const dayNum = get('day');
const month = get('month');
const year = get('year');
const time = new Intl.DateTimeFormat('fa-IR-u-ca-persian', {
hour: '2-digit',
minute: '2-digit',
hour12: false,
}).format(date);
return `${weekday}، ${dayNum} ${month} ${year}، ساعت ${time}`;
}
return date.toLocaleString(locale || undefined, {
dateStyle: 'medium',
timeStyle: 'short',
});