/** * Date display helpers. Timestamps cross the wire as UTC ISO-8601; Shamsi (Persian * calendar) display is a client concern (see money-and-types.md). We render via the * Intl Persian calendar — no date library needed. */ const SHAMSI_LOCALE = 'fa-IR-u-ca-persian'; /** Resolves the Intl locale for date formatting from the app locale. */ function intlLocale(locale: string): string { return locale === 'fa' ? SHAMSI_LOCALE : 'en-US'; } /** Formats a UTC ISO timestamp as a localized date (Shamsi for `fa`, Gregorian for `en`). */ export function formatShamsiDate( iso: string | Date, locale: string = 'fa', options: Intl.DateTimeFormatOptions = { year: 'numeric', month: 'long', day: 'numeric' } ): string { const date = iso instanceof Date ? iso : new Date(iso); if (Number.isNaN(date.getTime())) return ''; return new Intl.DateTimeFormat(intlLocale(locale), options).format(date); } /** Formats a UTC ISO timestamp as a localized date + time. */ export function formatShamsiDateTime(iso: string | Date, locale: string = 'fa'): string { return formatShamsiDate(iso, locale, { year: 'numeric', month: 'long', day: 'numeric', hour: '2-digit', minute: '2-digit', }); } /** * Month + year only (Shamsi for `fa`, Gregorian for `en`), e.g. «تیر ۱۴۰۳» — doubles as a stable * group key for a month-grouped timeline (two timestamps in the same calendar month always format * to the same string), so callers can group directly on this without separate Jalali arithmetic. */ export function formatShamsiMonthYear(iso: string | Date, locale: string = 'fa'): string { return formatShamsiDate(iso, locale, { year: 'numeric', month: 'long' }); } /** Time only (hh:mm) — a chat bubble's stamp once its date lives on a separator instead (§3.2). */ export function formatShamsiTime(iso: string | Date, locale: string = 'fa'): string { return formatShamsiDate(iso, locale, { hour: '2-digit', minute: '2-digit' }); } /** * Whole calendar days between two timestamps, comparing the **formatted date string** (not raw ms * subtraction, which is wrong across a DST/timezone boundary) — two timestamps format identically iff * they fall on the same calendar day in the active calendar system. */ function isSameCalendarDay(a: Date, b: Date, locale: string): boolean { return formatShamsiDate(a, locale) === formatShamsiDate(b, locale); } /** * A chat-style day-separator label: the caller's «امروز»/«دیروز» for the last two calendar days, else the * plain Shamsi date (e.g. «۲۵ تیر ۱۴۰۵»). Calendar-agnostic — it never does its own Jalali arithmetic, * only compares already-formatted date strings (see `isSameCalendarDay`). */ export function formatDaySeparator( iso: string | Date, locale: string, todayLabel: string, yesterdayLabel: string, ): string { const date = iso instanceof Date ? iso : new Date(iso); if (Number.isNaN(date.getTime())) return ''; const now = new Date(); if (isSameCalendarDay(date, now, locale)) return todayLabel; const yesterday = new Date(now); yesterday.setDate(yesterday.getDate() - 1); if (isSameCalendarDay(date, yesterday, locale)) return yesterdayLabel; return formatShamsiDate(date, locale); }