Как форматировать относительное время в TanStack Start v1
Форматируйте временные метки как фразы вроде «2 дня назад»
Проблема
Отображение временных меток в виде относительных фраз, таких как «2 дня назад» или «через 3 часа», делает информацию о времени более понятной для пользователей. Однако такие фразы подчиняются сложным грамматическим правилам, которые различаются в зависимости от языка. В английском «ago» ставится после количества в прошлом, а «in» — перед будущим, но в других языках может быть другой порядок слов, изменяться форма единиц времени или использоваться совершенно иные грамматические конструкции. Ручная сборка таких фраз с помощью конкатенации строк приводит к неправильному результату на любом языке, кроме того, который вы захардкодили.
Базовый API форматирования требует числовое значение и единицу времени, но временные метки обычно приходят как объекты Date или значения в миллисекундах. Чтобы преобразовать временную метку в нужную единицу и значение, нужно вычислить разницу с текущим моментом и выбрать наиболее естественную единицу для выражения этой разницы.
Решение
Вычислите разницу между временной меткой и текущим моментом, выберите наиболее подходящую единицу для этой разницы, а затем отформатируйте результат с помощью локализованного форматирования относительного времени. Это преобразует сырые временные метки в грамматически правильные фразы, которые адаптируются под язык пользователя.
Используйте форматирование относительного времени из react-intl с вспомогательной функцией, которая определяет лучшую единицу. Хелпер сравнивает разницу во времени с порогами для каждой единицы и возвращает числовое значение и название единицы. Форматтер затем применяет правильные грамматические правила для выбранной локали.
Шаги
1. Создайте хелпер для выбора единицы
Напишите функцию, которая вычисляет разницу во времени и выбирает наиболее естественную единицу для её выражения.
type RelativeTimeUnit =
| "second"
| "minute"
| "hour"
| "day"
| "week"
| "month"
| "year";
interface RelativeTimeValue {
value: number;
unit: RelativeTimeUnit;
}
export function selectRelativeTimeUnit(
timestamp: Date | number,
baseTime: Date | number = Date.now(),
): RelativeTimeValue {
const date = typeof timestamp === "number" ? timestamp : timestamp.getTime();
const base = typeof baseTime === "number" ? baseTime : baseTime.getTime();
const diffMs = date - base;
const absDiff = Math.abs(diffMs);
const minute = 60 * 1000;
const hour = 60 * minute;
const day = 24 * hour;
const week = 7 * day;
const month = 30 * day;
const year = 365 * day;
if (absDiff < minute) {
return { value: Math.round(diffMs / 1000), unit: "second" };
}
if (absDiff < hour) {
return { value: Math.round(diffMs / minute), unit: "minute" };
}
if (absDiff < day) {
return { value: Math.round(diffMs / hour), unit: "hour" };
}
if (absDiff < week) {
return { value: Math.round(diffMs / day), unit: "day" };
}
if (absDiff < month) {
return { value: Math.round(diffMs / week), unit: "week" };
}
if (absDiff < year) {
return { value: Math.round(diffMs / month), unit: "month" };
}
return { value: Math.round(diffMs / year), unit: "year" };
}
Эта функция преобразует временную метку в относительное значение и единицу, сравнивая разницу во времени с фиксированными порогами. Для прошедшего времени возвращаются отрицательные значения, для будущего — положительные, и форматтер правильно их интерпретирует.
2. Создайте компонент относительного времени
Создайте компонент, который объединяет вспомогательную функцию выбора единицы с форматированием от react-intl.
import { FormattedRelativeTime } from "react-intl";
import { selectRelativeTimeUnit } from "./selectRelativeTimeUnit";
interface RelativeTimeProps {
date: Date | number;
numeric?: "always" | "auto";
style?: "long" | "short" | "narrow";
}
export function RelativeTime({
date,
numeric = "auto",
style = "long",
}: RelativeTimeProps) {
const { value, unit } = selectRelativeTimeUnit(date);
return (
<FormattedRelativeTime
value={value}
unit={unit}
numeric={numeric}
style={style}
/>
);
}
Этот компонент принимает временную метку и параметры форматирования, вычисляет относительное значение и единицу, а затем передаёт их компоненту react-intl для локализованного отображения. Опция numeric="auto" позволяет выводить фразы вроде «вчера» вместо «1 день назад», когда это уместно.
3. Используйте компонент в своих маршрутах
Импортируйте и рендерьте компонент там, где нужно показать относительные временные метки.
import { createFileRoute } from "@tanstack/react-router";
import { RelativeTime } from "../components/RelativeTime";
export const Route = createFileRoute("/posts/$postId")({
component: PostPage,
});
function PostPage() {
const post = {
title: "Understanding Relative Time",
publishedAt: new Date("2024-11-15T10:30:00Z"),
updatedAt: new Date(Date.now() - 2 * 60 * 60 * 1000),
};
return (
<article>
<h1>{post.title}</h1>
<p>
Published <RelativeTime date={post.publishedAt} />
</p>
<p>
Updated <RelativeTime date={post.updatedAt} style="short" />
</p>
</article>
);
}
Компонент работает как на сервере, так и на клиенте. На сервере он генерирует начальную фразу с относительным временем, а на клиенте отображает ту же фразу с учётом локали пользователя из IntlProvider.
4. Добавьте императивный вариант форматирования
В случаях, когда вам нужна отформатированная строка напрямую, создайте вспомогательную функцию на основе хука.
import { useIntl } from "react-intl";
import { selectRelativeTimeUnit } from "./selectRelativeTimeUnit";
export function useRelativeTime() {
const intl = useIntl();
return (
date: Date | number,
options?: {
numeric?: "always" | "auto";
style?: "long" | "short" | "narrow";
},
) => {
const { value, unit } = selectRelativeTimeUnit(date);
return intl.formatRelativeTime(value, unit, options);
};
}
Этот хук возвращает функцию для императивного форматирования временных меток — удобно для установки текстовых атрибутов или вычисления значений вне JSX.
5. Используйте хук вне компонентов
Вызывайте хук в компонентах, где нужны отформатированные строки для атрибутов или логики.
import { createFileRoute } from "@tanstack/react-router";
import { useRelativeTime } from "../hooks/useRelativeTime";
export const Route = createFileRoute("/events/$eventId")({
component: EventPage,
});
function EventPage() {
const formatRelativeTime = useRelativeTime();
const event = {
name: "Product Launch",
startTime: new Date(Date.now() + 3 * 24 * 60 * 60 * 1000),
};
const timeUntil = formatRelativeTime(event.startTime);
return (
<div>
<h1>{event.name}</h1>
<time dateTime={event.startTime.toISOString()} title={timeUntil}>
{timeUntil}
</time>
</div>
);
}
Хук предоставляет ту же логику форматирования в функциональном виде, позволяя использовать относительные временные строки в атрибутах, вычисляемых значениях или там, где нельзя отрендерить компонент.