79 lines
3.2 KiB
TypeScript
79 lines
3.2 KiB
TypeScript
/**
|
|
* 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);
|
|
}
|