Files
baya-monorepo/client/src/utils/date.ts
T
2026-07-19 17:13:32 +03:30

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);
}