Intl.RelativeTimeFormat

Intl.RelativeTimeFormat — нативный браузерный API для форматирования относительного времени. В ряде случаев он полностью заменяет timeago.js без дополнительных зависимостей.


Основной API

const rtf = new Intl.RelativeTimeFormat('ru', {
  numeric: 'auto',  // 'auto' или 'always'
  style:   'long',  // 'long', 'short', 'narrow'
});

// Прошлое
rtf.format(-30,  'second'); // "30 секунд назад"
rtf.format(-5,   'minute'); // "5 минут назад"
rtf.format(-2,   'hour');   // "2 часа назад"
rtf.format(-1,   'day');    // "вчера"
rtf.format(-3,   'day');    // "3 дня назад"
rtf.format(-2,   'week');   // "2 недели назад"
rtf.format(-1,   'month');  // "в прошлом месяце"
rtf.format(-1,   'year');   // "в прошлом году"

// Будущее
rtf.format(5,    'minute'); // "через 5 минут"
rtf.format(1,    'day');    // "завтра"
rtf.format(1,    'week');   // "на следующей неделе"

numeric: ‘auto’ vs ‘always’

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

auto.format(-1, 'day');    // "вчера"
always.format(-1, 'day'); // "1 день назад"

auto.format(0, 'day');    // "сегодня"
always.format(0, 'day');  // "через 0 дней"

style: ‘long’, ‘short’, ‘narrow’

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

ru('long').format(-5, 'minute');    // "5 минут назад"
ru('short').format(-5, 'minute');   // "5 мин. назад"
ru('narrow').format(-5, 'minute');  // "5 мин. назад" (иногда идентично short)

Полная функция форматирования

function relativeFormat(date, locale = 'ru') {
  const rtf    = new Intl.RelativeTimeFormat(locale, { numeric: 'auto' });
  const diffMs = new Date(date).getTime() - Date.now();
  const absSec = Math.abs(diffMs / 1000);
  const sign   = diffMs < 0 ? -1 : 1;

  if (absSec < 45)            return rtf.format(sign * Math.round(absSec),        'second');
  if (absSec < 45 * 60)       return rtf.format(sign * Math.round(absSec / 60),   'minute');
  if (absSec < 22 * 3600)     return rtf.format(sign * Math.round(absSec / 3600), 'hour');
  if (absSec < 26 * 86400)    return rtf.format(sign * Math.round(absSec / 86400),'day');
  if (absSec < 320 * 86400)   return rtf.format(sign * Math.round(absSec / (30 * 86400)), 'month');
  return                             rtf.format(sign * Math.round(absSec / (365 * 86400)), 'year');
}

Кеширование экземпляров

Создание Intl.RelativeTimeFormat — относительно дорогая операция. При частом вызове нужно кешировать:

const rtfCache = new Map();

function getRTF(locale, options = {}) {
  const key = `${locale}:${JSON.stringify(options)}`;
  if (!rtfCache.has(key)) {
    rtfCache.set(key, new Intl.RelativeTimeFormat(locale, options));
  }
  return rtfCache.get(key);
}

Автообновление DOM с Intl.RTF

Intl.RelativeTimeFormat не предоставляет render/cancel. Реализовать самостоятельно:

function startRelativeTimeUpdates(selector = '[datetime]', locale = 'ru') {
  const elements = document.querySelectorAll(selector);

  function update() {
    elements.forEach(el => {
      const datetime = el.getAttribute('datetime');
      if (!datetime) return;
      const next = relativeFormat(datetime, locale);
      if (el.textContent !== next) el.textContent = next;
    });
  }

  update(); // Сразу

  const id = setInterval(update, 30_000);
  return () => clearInterval(id); // cleanup
}

Сравнение с timeago.js

import { format } from 'timeago.js';

const date    = new Date(Date.now() - 2 * 3600_000); // 2 часа назад

// timeago.js
format(date, 'ru');                // "2 часа назад"

// Intl.RelativeTimeFormat
relativeFormat(date, 'ru');       // "2 часа назад"

Для стандартных случаев вывод идентичен. Различия:

  • Intl.RTF использует ICU данные браузера — более точные правила склонения.
  • timeago.js применяет собственные правила из файлов локалей.

formatToParts для кастомного отображения

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

rtf.formatToParts(-5, 'minute');
// [
//   { type: 'integer',  value: '5' },
//   { type: 'literal',  value: ' ' },
//   { type: 'literal',  value: 'минут' },
//   { type: 'literal',  value: ' назад' },
// ]

formatToParts позволяет обернуть число в <span> или применить стили:

function formatWithHighlight(value, unit, locale = 'ru') {
  const rtf   = new Intl.RelativeTimeFormat(locale, { numeric: 'always' });
  const parts = rtf.formatToParts(value, unit);

  return parts.map(part =>
    part.type === 'integer'
      ? `<strong>${part.value}</strong>`
      : part.value
  ).join('');
}

Когда Intl.RTF не заменяет timeago.js

  • Нужна поддержка браузеров до Chrome 71 / Safari 14.
  • Используются нестандартные правила локализации (специфические для проекта).
  • Нужна функция render для автоматического обновления DOM через встроенные таймеры.
  • Необходим полный контроль над формами множественного числа через LocaleFunc.