Как форматировать относительное время в React Router v7

Форматируйте временные метки как фразы вроде «2 дня назад»

Проблема

Отображение временных меток в виде относительных фраз, например «2 дня назад» или «через 3 часа», делает контент более живым и понятным. Однако такие фразы подчиняются сложным грамматическим правилам, которые сильно различаются в разных языках. В английском «ago» ставится после количества для прошедшего времени, а «in» — перед количеством для будущего, но в других языках может меняться сама форма времени, порядок слов или даже вся грамматическая структура. Если собирать такие фразы вручную через конкатенацию строк, получится корректно только для одного языка, который вы захардкодили, а для остальных пользователей опыт будет сломан.

Решение

Используйте форматирование относительного времени с учётом локали, чтобы преобразовать разницу между временными метками в грамматически правильные фразы. Сначала вычислите разницу между заданной временной меткой и текущим моментом, затем отформатируйте её по правилам языка пользователя. Так вы получите корректные выражения для прошедшего и будущего времени с правильной грамматикой, порядком слов и формами для каждого языка — без ручной сборки строк.

Шаги

1. Создайте хелпер для вычисления относительных значений времени

Компонент FormattedRelativeTime требует числовой value и пропа unit. Напишите вспомогательную функцию, которая вычисляет разницу во времени и выбирает подходящую единицу измерения.

export function getRelativeTimeValue(date: Date | number) {
  const now = Date.now();
  const timestamp = typeof date === "number" ? date : date.getTime();
  const diffInSeconds = Math.round((timestamp - now) / 1000);

  const minute = 60;
  const hour = minute * 60;
  const day = hour * 24;
  const week = day * 7;
  const month = day * 30;
  const year = day * 365;

  const absDiff = Math.abs(diffInSeconds);

  if (absDiff < minute) {
    return { value: diffInSeconds, unit: "second" as const };
  } else if (absDiff < hour) {
    return {
      value: Math.round(diffInSeconds / minute),
      unit: "minute" as const,
    };
  } else if (absDiff < day) {
    return { value: Math.round(diffInSeconds / hour), unit: "hour" as const };
  } else if (absDiff < week) {
    return { value: Math.round(diffInSeconds / day), unit: "day" as const };
  } else if (absDiff < month) {
    return { value: Math.round(diffInSeconds / week), unit: "week" as const };
  } else if (absDiff < year) {
    return { value: Math.round(diffInSeconds / month), unit: "month" as const };
  } else {
    return { value: Math.round(diffInSeconds / year), unit: "year" as const };
  }
}

Эта функция преобразует временную метку в числовое значение с знаком и подбирает наиболее подходящую единицу времени в зависимости от величины разницы.

2. Создайте компонент относительного времени с помощью FormattedRelativeTime

Используйте компонент FormattedRelativeTime из react-intl, который отображает отформатированное относительное время и может автоматически обновляться через заданные интервалы.

import { FormattedRelativeTime } from "react-intl";
import { getRelativeTimeValue } from "./getRelativeTimeValue";

interface RelativeTimeProps {
  date: Date | number;
  updateIntervalInSeconds?: number;
}

export function RelativeTime({
  date,
  updateIntervalInSeconds,
}: RelativeTimeProps) {
  const { value, unit } = getRelativeTimeValue(date);

  return (
    <FormattedRelativeTime
      value={value}
      unit={unit}
      numeric="auto"
      updateIntervalInSeconds={updateIntervalInSeconds}
    />
  );
}

Опция numeric="auto" позволяет форматтеру использовать такие фразы, как «вчера» вместо «1 день назад», когда это уместно. Необязательный проп updateIntervalInSeconds управляет тем, как часто компонент перерисовывается, чтобы поддерживать актуальность относительного времени.

3. Используйте компонент в маршруте React Router

Рендерьте компонент относительного времени в любом компоненте маршрута, где нужно показывать временные метки.

import { RelativeTime } from "./RelativeTime";

interface Post {
  id: string;
  title: string;
  content: string;
  createdAt: number;
}

export function PostDetail({ post }: { post: Post }) {
  return (
    <article>
      <h1>{post.title}</h1>
      <time dateTime={new Date(post.createdAt).toISOString()}>
        <RelativeTime date={post.createdAt} />
      </time>
      <p>{post.content}</p>
    </article>
  );
}

Компонент автоматически форматирует временную метку в соответствии с локалью пользователя, создавая фразы вроде «2 дня назад» на английском, «il y a 2 jours» на французском или «hace 2 días» на испанском.

4. Императивное форматирование относительного времени с помощью useIntl

В случаях, когда вам нужна отформатированная строка напрямую (например, для установки атрибутов элементов), используйте функцию formatRelativeTime из хука useIntl.

import { useIntl } from "react-intl";
import { getRelativeTimeValue } from "./getRelativeTimeValue";

interface CommentProps {
  author: string;
  text: string;
  timestamp: number;
}

export function Comment({ author, text, timestamp }: CommentProps) {
  const intl = useIntl();
  const { value, unit } = getRelativeTimeValue(timestamp);
  const relativeTime = intl.formatRelativeTime(value, unit, {
    numeric: "auto",
  });

  return (
    <div aria-label={`Comment by ${author}, posted ${relativeTime}`}>
      <strong>{author}</strong>
      <p>{text}</p>
    </div>
  );
}

Этот подход позволяет получить отформатированную строку, которую можно использовать в атрибутах, объединять с другим текстом или передавать в не-React API, при этом полностью поддерживается локализация.