initial commit
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
/* ============================================================================
|
||||
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]}`;
|
||||
}
|
||||
Reference in New Issue
Block a user