Files

104 lines
3.3 KiB
TypeScript
Raw Permalink Normal View History

2026-09-11 02:46:56 +02:00
/* ============================================================================
LOCALE-AWARE FORMATTING
----------------------------------------------------------------------------
Thin `Intl` wrappers so a route never hard-codes a date pattern. Arabic uses
ar-LY with the `latn` numbering system: the approved draft copy writes
Arabic dates and figures in Western digits ("30 يونيو", "2025"), so Eastern
Arabic numerals would contradict the source content.
========================================================================= */
import { INTL_LOCALE, type Locale } from "./config";
const NUMBERING_SYSTEM = "latn";
/** The BCP-47 tag (with numbering system) handed to `Intl`. */
export function intlLocale(locale: Locale): string {
return `${INTL_LOCALE[locale]}-u-nu-${NUMBERING_SYSTEM}`;
}
function toDate(value: Date | string | number): Date | null {
const date = value instanceof Date ? value : new Date(value);
return Number.isNaN(date.getTime()) ? null : date;
}
export type DateStyle = "full" | "long" | "medium" | "short";
/** "12 March 2026" / "12 مارس 2026". Returns "" for an unparseable input. */
export function formatDate(
value: Date | string | number | null | undefined,
locale: Locale,
options: Intl.DateTimeFormatOptions = { dateStyle: "long" },
): string {
if (value === null || value === undefined) return "";
const date = toDate(value);
if (!date) return "";
return new Intl.DateTimeFormat(intlLocale(locale), options).format(date);
}
/** Date plus time of day, for an event card or an event detail page. */
export function formatDateTime(
value: Date | string | number | null | undefined,
locale: Locale,
options: Intl.DateTimeFormatOptions = {
dateStyle: "long",
timeStyle: "short",
},
): string {
return formatDate(value, locale, options);
}
/** "12–14 March 2026". Falls back to a single date when `end` is missing. */
export function formatDateRange(
start: Date | string | number,
end: Date | string | number | null | undefined,
locale: Locale,
options: Intl.DateTimeFormatOptions = { dateStyle: "long" },
): string {
const from = toDate(start);
if (!from) return "";
const to = end === null || end === undefined ? null : toDate(end);
if (!to || from.getTime() === to.getTime()) {
return formatDate(from, locale, options);
}
return new Intl.DateTimeFormat(intlLocale(locale), options).formatRange(
from,
to,
);
}
/** The machine-readable value for `<time dateTime>` - always ISO, never localised. */
export function toIsoDate(
value: Date | string | number | null | undefined,
): string | undefined {
if (value === null || value === undefined) return undefined;
return toDate(value)?.toISOString();
}
export function formatNumber(
value: number,
locale: Locale,
options?: Intl.NumberFormatOptions,
): string {
return new Intl.NumberFormat(intlLocale(locale), options).format(value);
}
/** "1.2 MB" - used by the resources list for a download's file size. */
export function formatFileSize(bytes: number, locale: Locale): string {
const units = ["B", "KB", "MB", "GB"] as const;
let size = Math.max(0, bytes);
let unit = 0;
while (size >= 1024 && unit < units.length - 1) {
size /= 1024;
unit += 1;
}
const formatted = formatNumber(size, locale, {
maximumFractionDigits: unit === 0 ? 0 : 1,
});
return `${formatted} ${units[unit]}`;
}