Относительное время

Относительное время — один из ключевых элементов интерфейсов, ориентированных на динамические данные: ленты новостей, комментарии, уведомления, мессенджеры, журналы событий. Библиотека FormatJS предоставляет стандартизированный слой для работы с локализованным представлением времени через API, основанный на стандарте ECMAScript Internationalization API — Intl.RelativeTimeFormat.


Относительное время выражает разницу между двумя моментами не в абсолютных значениях (дата и время), а в человеческих понятиях:

  • «5 минут назад»
  • «через 2 часа»
  • «3 дня назад»
  • «на следующей неделе»

В основе лежит объект Intl.RelativeTimeFormat, который предоставляет форматирование с учётом языка и локали.

В контексте FormatJS используется полифилл @formatjs/intl-relativetimeformat, обеспечивающий одинаковое поведение в разных окружениях.


Базовая концепция относительных единиц

Форматирование опирается на две сущности:

  • число (value) — величина разницы
  • единица измерения (unit) — секунды, минуты, часы, дни, недели, месяцы, годы

Пример логики:

  • -1 minute → «1 минуту назад»
  • 5 minutes → «через 5 минут»

Знак числа определяет направление времени:

  • отрицательное значение — прошлое
  • положительное значение — будущее

Использование Intl.RelativeTimeFormat

Стандартный API Jav * aScript:

const rtf = new Intl.RelativeTimeFormat('ru', { numeric: 'auto' });

rtf.format(-1, 'minute'); // "минуту назад"
rtf.format(5, 'minute');  // "через 5 минут"

Параметр numeric: 'auto' позволяет использовать естественные формы языка:

  • вместо «0 дней назад» → «сегодня»
  • вместо «1 день назад» → «вчера»

Интеграция FormatJS и polyfill

В FormatJS используется пакет:

@formatjs/intl-relativetimeformat

Установка:

npm install @formatjs/intl-relativetimeformat

Инициализация polyfill:

import '@formatjs/intl-relativetimeformat/polyfill';
import '@formatjs/intl-relativetimeformat/locale-data/ru';

После подключения поведение Intl.RelativeTimeFormat становится предсказуемым во всех средах.


Нормализация временной разницы

Для отображения относительного времени требуется преобразование абсолютной даты в разницу относительно текущего времени.

Пример базовой функции:

const rtf = new Intl.RelativeTimeFormat('ru', { numeric: 'auto' });

function formatRelative(date) {
  const now = Date.now();
  const diff = date - now;

  const seconds = Math.round(diff / 1000);
  const minutes = Math.round(seconds / 60);
  const hours = Math.round(minutes / 60);
  const days = Math.round(hours / 24);

  if (Math.abs(days) > 0) return rtf.format(days, 'day');
  if (Math.abs(hours) > 0) return rtf.format(hours, 'hour');
  if (Math.abs(minutes) > 0) return rtf.format(minutes, 'minute');
  return rtf.format(seconds, 'second');
}

Выбор подходящей единицы измерения

Корректное отображение требует адаптивного выбора единицы:

  • менее 60 секунд → секунды
  • менее 60 минут → минуты
  • менее 24 часов → часы
  • менее 30 дней → дни
  • более → месяцы или годы

Оптимизация повышает читаемость и снижает когнитивную нагрузку.


Локализация и языковые особенности

Intl.RelativeTimeFormat учитывает:

  • грамматические формы
  • множественные числа
  • порядок слов
  • особенности склонения

Пример различий:

new Intl.RelativeTimeFormat('en').format(-1, 'day'); // "1 day ago"
new Intl.RelativeTimeFormat('ru').format(-1, 'day'); // "вчера"

Некоторые языки используют специальные слова вместо числовых форм при numeric: 'auto'.


Форматирование с разными стилями

Параметр style управляет детализацией:

new Intl.RelativeTimeFormat('ru', { style: 'long' });
new Intl.RelativeTimeFormat('ru', { style: 'short' });
new Intl.RelativeTimeFormat('ru', { style: 'narrow' });

Различия:

  • long → «через 5 минут»
  • short → «через 5 мин.»
  • narrow → «+5 мин»

Выбор стиля зависит от плотности интерфейса.


Кэширование форматтера

Создание экземпляра Intl.RelativeTimeFormat относительно дорогое, поэтому применяется кэширование:

const cache = new Map();

function getFormatter(locale) {
  if (!cache.has(locale)) {
    cache.set(locale, new Intl.RelativeTimeFormat(locale, { numeric: 'auto' }));
  }
  return cache.get(locale);
}

Это особенно важно в SPA и высоконагруженных интерфейсах.


Обработка границ времени

При переходах между единицами возникают пограничные эффекты:

  • 59 секунд → «59 секунд назад»
  • 60 секунд → «1 минута назад»

Решение заключается в строгом округлении:

const seconds = Math.round(diff / 1000);

Альтернативой является Math.floor, но он может создавать задержку обновления текста.


Сравнение с абсолютным временем

Относительное время используется в динамических интерфейсах, но имеет ограничения:

  • теряет точность при больших интервалах
  • требует регулярного обновления UI
  • зависит от текущего времени клиента

Абсолютное время предпочтительно для:

  • журналов аудита
  • юридически значимых событий
  • исторических данных

Поведение при длительных интервалах

Intl.RelativeTimeFormat поддерживает большие значения:

rtf.format(-3, 'month'); // "3 месяца назад"
rtf.format(2, 'year');   // "через 2 года"

Однако при больших промежутках интерфейсы часто переходят на абсолютные даты.


Оптимизация обновления интерфейса

Относительное время требует периодического обновления отображения:

  • секунды — каждую секунду
  • минуты — каждую минуту
  • часы — каждый час

Пример стратегии:

setInterval(updateRelativeTimes, 60 * 1000);

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


Работа с различными локалями

Поддержка локалей критична для корректного отображения:

new Intl.RelativeTimeFormat('fr');
new Intl.RelativeTimeFormat('de');
new Intl.RelativeTimeFormat('zh');
new Intl.RelativeTimeFormat('ru');

FormatJS использует набор локализационных данных, которые расширяются через отдельные пакеты locale-data.


Типичные ошибки реализации

  • пересоздание форматтера при каждом рендере
  • отсутствие округления временной разницы
  • использование только секунд без перехода к минутам и часам
  • игнорирование локали пользователя
  • отсутствие polyfill в старых окружениях

Использование в UI-компонентах

В компонентных системах функция форматирования выносится в утилиту:

export function createRelativeTime(locale) {
  const rtf = new Intl.RelativeTimeFormat(locale, { numeric: 'auto' });

  return (date) => {
    const diff = date - Date.now();
    const seconds = Math.round(diff / 1000);
    const minutes = Math.round(seconds / 60);

    if (Math.abs(minutes) < 60) {
      return rtf.format(minutes, 'minute');
    }

    const hours = Math.round(minutes / 60);
    if (Math.abs(hours) < 24) {
      return rtf.format(hours, 'hour');
    }

    const days = Math.round(hours / 24);
    return rtf.format(days, 'day');
  };
}

Поведение в серверном рендеринге

В SSR-окружениях важно:

  • фиксировать локаль на сервере
  • синхронизировать время сервера и клиента
  • избегать различий гидратации

Polyfill FormatJS обеспечивает одинаковый результат рендеринга между сервером и браузером.


Поддержка старых браузеров

При отсутствии нативной поддержки:

  • подключается @formatjs/intl-relativetimeformat
  • загружаются locale-data
  • используется fallback на более простое форматирование при необходимости

Это позволяет сохранять единое поведение интерфейса без деградации UX.