Files
2026-09-11 02:46:56 +02:00

104 lines
3.3 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* ============================================================================
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]}`;
}