Files
HiveGrid-Wireframes/hivegrid-cloud/apps/web/app/locale/format.ts
T

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);
}