137 lines
6.8 KiB
TypeScript
137 lines
6.8 KiB
TypeScript
import { useTranslation } from "react-i18next";
|
|
import { defaultLocale, type SupportedLocale } from "./i18n";
|
|
|
|
// Locale-aware Intl wrappers. Server code calls the pure functions with an
|
|
// explicit locale (resolved by getTranslations); client React code uses the
|
|
// useLocaleFormatters() hook which reads the active language from i18next.
|
|
|
|
const numberFormatters = new Map<string, Intl.NumberFormat>();
|
|
const dateTimeFormatters = new Map<string, Intl.DateTimeFormat>();
|
|
const relativeTimeFormatters = new Map<string, Intl.RelativeTimeFormat>();
|
|
|
|
function formatterKey(locale: SupportedLocale | string | undefined, options?: Record<string, unknown>): string {
|
|
return `${locale ?? defaultLocale}:${JSON.stringify(options ?? {})}`;
|
|
}
|
|
|
|
function getNumberFormatter(locale: SupportedLocale | string, options?: Intl.NumberFormatOptions): Intl.NumberFormat {
|
|
const key = formatterKey(locale, options as Record<string, unknown> | undefined);
|
|
const cached = numberFormatters.get(key);
|
|
if (cached) return cached;
|
|
const formatter = new Intl.NumberFormat(locale ?? defaultLocale, options);
|
|
numberFormatters.set(key, formatter);
|
|
return formatter;
|
|
}
|
|
|
|
function getDateTimeFormatter(locale: SupportedLocale | string, options?: Intl.DateTimeFormatOptions): Intl.DateTimeFormat {
|
|
const key = formatterKey(locale, options as Record<string, unknown> | undefined);
|
|
const cached = dateTimeFormatters.get(key);
|
|
if (cached) return cached;
|
|
const formatter = new Intl.DateTimeFormat(locale ?? defaultLocale, options);
|
|
dateTimeFormatters.set(key, formatter);
|
|
return formatter;
|
|
}
|
|
|
|
function getRelativeTimeFormatter(locale: SupportedLocale | string, options?: Intl.RelativeTimeFormatOptions): Intl.RelativeTimeFormat {
|
|
const key = formatterKey(locale, options as Record<string, unknown> | undefined);
|
|
const cached = relativeTimeFormatters.get(key);
|
|
if (cached) return cached;
|
|
const formatter = new Intl.RelativeTimeFormat(locale ?? defaultLocale, options);
|
|
relativeTimeFormatters.set(key, formatter);
|
|
return formatter;
|
|
}
|
|
|
|
export function formatNumber(value: number, locale: SupportedLocale | string, options?: Intl.NumberFormatOptions): string {
|
|
return getNumberFormatter(locale, options).format(value);
|
|
}
|
|
|
|
export function formatCurrency(value: number, currency: string, locale: SupportedLocale | string, options?: Intl.NumberFormatOptions): string {
|
|
return getNumberFormatter(locale, { style: "currency", currency, ...options }).format(value);
|
|
}
|
|
|
|
export function formatCurrencyFromCents(cents: number, currency: string, locale: SupportedLocale | string, options?: Intl.NumberFormatOptions): string {
|
|
return formatCurrency(cents / 100, currency, locale, options);
|
|
}
|
|
|
|
export function formatPercent(value: number, locale: SupportedLocale | string, options?: Intl.NumberFormatOptions): string {
|
|
return getNumberFormatter(locale, { style: "percent", ...options }).format(value);
|
|
}
|
|
|
|
export function formatCompactNumber(value: number, locale: SupportedLocale | string, options?: Intl.NumberFormatOptions): string {
|
|
return getNumberFormatter(locale, { notation: "compact", ...options }).format(value);
|
|
}
|
|
|
|
export function formatDate(value: Date | number | string, locale: SupportedLocale | string, options?: Intl.DateTimeFormatOptions): string {
|
|
const date = value instanceof Date ? value : new Date(value);
|
|
return getDateTimeFormatter(locale, options).format(date);
|
|
}
|
|
|
|
export function formatDateTime(value: Date | number | string, locale: SupportedLocale | string, options?: Intl.DateTimeFormatOptions): string {
|
|
return formatDate(value, locale, { dateStyle: "medium", timeStyle: "short", ...options });
|
|
}
|
|
|
|
export function formatRelativeTime(
|
|
value: number,
|
|
unit: Intl.RelativeTimeFormatUnit,
|
|
locale: SupportedLocale | string,
|
|
options?: Intl.RelativeTimeFormatOptions
|
|
): string {
|
|
return getRelativeTimeFormatter(locale, options).format(value, unit);
|
|
}
|
|
|
|
export function formatRelativeDate(
|
|
value: Date | number | string,
|
|
locale: SupportedLocale | string,
|
|
options?: Intl.RelativeTimeFormatOptions & { relativeTo?: Date | number }
|
|
): string {
|
|
const date = value instanceof Date ? value.getTime() : new Date(value).getTime();
|
|
const relativeTo = options?.relativeTo instanceof Date ? options.relativeTo.getTime() : (options?.relativeTo ?? Date.now());
|
|
const delta = date - relativeTo;
|
|
const units: Array<{ unit: Intl.RelativeTimeFormatUnit; ms: number }> = [
|
|
{ unit: "year", ms: 365.25 * 24 * 60 * 60 * 1000 },
|
|
{ unit: "month", ms: 30 * 24 * 60 * 60 * 1000 },
|
|
{ unit: "day", ms: 24 * 60 * 60 * 1000 },
|
|
{ unit: "hour", ms: 60 * 60 * 1000 },
|
|
{ unit: "minute", ms: 60 * 1000 },
|
|
{ unit: "second", ms: 1000 },
|
|
];
|
|
const formatterOptions = { ...(options ?? {}) };
|
|
delete formatterOptions.relativeTo;
|
|
const formatter = getRelativeTimeFormatter(locale, { numeric: "auto", ...formatterOptions });
|
|
const unit = units.find((item) => Math.abs(delta) >= item.ms) ?? units[units.length - 1];
|
|
return formatter.format(Math.round(delta / unit.ms), unit.unit);
|
|
}
|
|
|
|
export type LocaleFormatters = {
|
|
locale: string;
|
|
number: (value: number, options?: Intl.NumberFormatOptions) => string;
|
|
currency: (value: number, currency: string, options?: Intl.NumberFormatOptions) => string;
|
|
currencyFromCents: (cents: number, currency: string, options?: Intl.NumberFormatOptions) => string;
|
|
percent: (value: number, options?: Intl.NumberFormatOptions) => string;
|
|
compact: (value: number, options?: Intl.NumberFormatOptions) => string;
|
|
date: (value: Date | number | string, options?: Intl.DateTimeFormatOptions) => string;
|
|
dateTime: (value: Date | number | string, options?: Intl.DateTimeFormatOptions) => string;
|
|
relativeTime: (value: number, unit: Intl.RelativeTimeFormatUnit, options?: Intl.RelativeTimeFormatOptions) => string;
|
|
relativeDate: (value: Date | number | string, options?: Intl.RelativeTimeFormatOptions & { relativeTo?: Date | number }) => string;
|
|
};
|
|
|
|
export function createLocaleFormatters(locale: SupportedLocale | string): LocaleFormatters {
|
|
return {
|
|
locale,
|
|
number: (value, options) => formatNumber(value, locale, options),
|
|
currency: (value, currency, options) => formatCurrency(value, currency, locale, options),
|
|
currencyFromCents: (cents, currency, options) => formatCurrencyFromCents(cents, currency, locale, options),
|
|
percent: (value, options) => formatPercent(value, locale, options),
|
|
compact: (value, options) => formatCompactNumber(value, locale, options),
|
|
date: (value, options) => formatDate(value, locale, options),
|
|
dateTime: (value, options) => formatDateTime(value, locale, options),
|
|
relativeTime: (value, unit, options) => formatRelativeTime(value, unit, locale, options),
|
|
relativeDate: (value, options) => formatRelativeDate(value, locale, options),
|
|
};
|
|
}
|
|
|
|
export function useLocaleFormatters(): LocaleFormatters {
|
|
const { i18n } = useTranslation();
|
|
const locale = (i18n?.language ?? defaultLocale) as SupportedLocale | string;
|
|
return createLocaleFormatters(locale);
|
|
}
|