import { ethTimeLabel, toAmharicDisplay } from './ethiopic'; /** Wire values carrying no time of day, e.g. a date column serialised as `2026-08-10`. */ const DATE_ONLY = /^\d{4}-\d{2}-\d{2}$/; /** Midnight UTC — how backends commonly serialise a plain date column. */ const UTC_MIDNIGHT = /T00:00:00(\.0+)?Z$/; /** * The one way a date is shown to a user. * * Timestamps render in the viewer's local timezone — `2026-08-10T06:57:34.507Z` * reads as `Aug 10, 2026 9:57am` in Addis. Values with no real time of day * render as a bare date: a birth date or a licence expiry stamped `12:00am` * reads as precision the data does not have. Those are also formatted in UTC, * because parsing `2026-08-10` yields UTC midnight, and converting that to a * behind-UTC local timezone would show the previous day. * * `language` is a parameter rather than read from i18next because each app runs * a DEDICATED i18n instance, not the global singleton (see `app/i18n/config.ts`) * — the same reason `localized()` in licensing.helpers.ts takes one. Components * should not call this directly; use `useDateDisplayer()` so the text actually * re-renders when the language changes. */ export function dateDisplayer( value: string | number | Date | null | undefined, language = 'en', ): string { if (value === null || value === undefined || value === '') return '—'; const date = new Date(value); if (Number.isNaN(date.getTime())) return '—'; const dateOnly = typeof value === 'string' && (DATE_ONLY.test(value) || UTC_MIDNIGHT.test(value)); if (language.startsWith('am')) { const day = toAmharicDisplay(date); // ሐምሌ 22/2018 return dateOnly ? day : `${day} - ${ethTimeLabel(date)}`; } const day = date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric', ...(dateOnly ? { timeZone: 'UTC' } : {}), }); if (dateOnly) return day; // Intl gives "9:57 AM"; the house format is "9:57am". const time = date .toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' }) .replace(' ', '') .toLowerCase(); return `${day} ${time}`; }