Как форматировать относительное время в 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 дня назад" на русском, "2 days ago" на английском или "il y a 2 jours" на французском.

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={`Комментарий от ${author}, опубликован ${relativeTime}`}>
      <strong>{author}</strong>
      <p>{text}</p>
    </div>
  );
}

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