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:
@@ -3,14 +3,14 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import type { ReactNode } from 'react';
|
||||
import { Link } from '@/i18n/Link';
|
||||
import { useT } from '@/i18n/I18nProvider';
|
||||
import { useT, useLocale } from '@/i18n/I18nProvider';
|
||||
import type { Dictionary } from '@/i18n/dictionaries/fa';
|
||||
import api from '@/lib/api';
|
||||
import { useAuthStore } from '@/lib/store';
|
||||
import type { Application } from '@/types';
|
||||
import { Rocket, Package, Circle, Hexagon, Wallet, Clock, Database, Plus } from 'lucide-react';
|
||||
import type { ResourceCredit } from '@/types';
|
||||
import { formatExpiresAtLocal } from '@/lib/format-utils';
|
||||
import { formatExpiresAtLocal, formatRemainingForLocale } from '@/lib/format-utils';
|
||||
import { managedServiceTypeLabel } from '@/lib/optional-service-defaults';
|
||||
import { filterApplications, filterManagedServices } from '@/lib/product-type';
|
||||
|
||||
@@ -65,6 +65,7 @@ function serviceSubtitle(app: Application): string {
|
||||
|
||||
export default function DashboardPage() {
|
||||
const t = useT();
|
||||
const locale = useLocale();
|
||||
const h = t.dashboard.home;
|
||||
const user = useAuthStore((s) => s.user);
|
||||
|
||||
@@ -116,10 +117,10 @@ export default function DashboardPage() {
|
||||
</p>
|
||||
<p className="mt-2 inline-flex items-center gap-1 text-indigo-700 font-medium">
|
||||
<Clock className="w-3.5 h-3.5" />
|
||||
{h.remaining.replace('{label}', credit.remainingLabel)}
|
||||
{h.remaining.replace('{label}', formatRemainingForLocale(credit.expiresAt, locale))}
|
||||
</p>
|
||||
<p className="mt-1 text-xs text-gray-500">
|
||||
{h.expires.replace('{date}', formatExpiresAtLocal(credit.expiresAt))}
|
||||
{h.expires.replace('{date}', formatExpiresAtLocal(credit.expiresAt, locale))}
|
||||
</p>
|
||||
<ul className="mt-3 grid grid-cols-2 gap-x-3 gap-y-1 text-xs text-gray-600">
|
||||
<li>{h.cpu}: {credit.cpuLimit}</li>
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user