sendou.ink/app/hooks/intl/useDateTimeFormat.ts

54 lines
1.7 KiB
TypeScript

import { databaseTimestampToDate } from "~/utils/dates";
import { useUserIntlPreference } from "./useUserIntlPreference";
/**
* Zero-width space rendered (hidden via `invisible`) during SSR so the element
* reserves exactly one normal text line, avoiding layout shift on hydration
* without an empty box's baseline quirks.
*/
const SSR_PLACEHOLDER = "\u200b";
const SSR_FORMATTER = {
format: (_date: Date | number) => SSR_PLACEHOLDER,
formatRange: (_from: Date | number, _to: Date | number) => SSR_PLACEHOLDER,
};
/**
* SSR-safe wrapper around `Intl.DateTimeFormat`.
*
* Uses the user's locale and hour cycle preferences via `useUserIntlPreference`.
* Before hydration the returned formatter's methods return a zero-width space
* placeholder so that server output matches the initial client render while
* reserving one text line.
*
* Inputs accept either a `Date` or a database timestamp (`number`); numbers
* are converted via `databaseTimestampToDate`.
*/
export function useDateTimeFormat(options: Intl.DateTimeFormatOptions) {
const { language, hourCycle, isLoaded } = useUserIntlPreference();
const formatter = new Intl.DateTimeFormat(language, {
...options,
...(options.hour && hourCycle ? { hourCycle } : {}),
});
const realFormatter = {
format: (date: Date | number) => {
return formatter.format(
typeof date === "number" ? databaseTimestampToDate(date) : date,
);
},
formatRange: (from: Date | number, to: Date | number) => {
return formatter.formatRange(
typeof from === "number" ? databaseTimestampToDate(from) : from,
typeof to === "number" ? databaseTimestampToDate(to) : to,
);
},
};
return {
formatter: isLoaded ? realFormatter : SSR_FORMATTER,
isLoaded,
};
}