Intl.RelativeTimeFormat API

Форматируйте относительные временные строки в JavaScript с полной поддержкой интернационализации

Введение

Отображение относительных временных меток, таких как «3 часа назад» или «через 2 дня», — частая задача в веб-приложениях. Ленты соцсетей, разделы комментариев, системы уведомлений и журналы активности — все они требуют удобочитаемых временных меток, которые обновляются по мере устаревания контента.

Реализовать такую функциональность с нуля — непросто. Нужно вычислять разницу во времени, выбирать подходящие единицы, учитывать правила склонения во всех языках и поддерживать переводы для каждого локаля. Поэтому разработчики часто использовали библиотеки вроде Moment.js, которые увеличивают размер бандла ради, казалось бы, простой задачи форматирования.

API Intl.RelativeTimeFormat предлагает нативное решение. Он форматирует относительные временные строки с полной поддержкой интернационализации, автоматически учитывая правила склонения и культурные особенности. API работает во всех основных браузерах и покрывает 95% пользователей по всему миру, избавляя от необходимости во внешних зависимостях и обеспечивая естественное звучание на десятках языков.

Базовое использование

Конструктор Intl.RelativeTimeFormat создает экземпляр форматтера, который преобразует числовые значения и единицы времени в локализованные строки.

const rtf = new Intl.RelativeTimeFormat('en');

console.log(rtf.format(-1, 'day'));
// "1 day ago"

console.log(rtf.format(2, 'hour'));
// "in 2 hours"

console.log(rtf.format(-3, 'month'));
// "3 months ago"

Метод format() принимает два параметра:

  • value: число, указывающее количество времени
  • unit: строка, определяющая единицу времени

Отрицательные значения означают прошедшее время, положительные — будущее. API автоматически учитывает склонение, формируя «1 день назад» или «2 дня назад» в зависимости от значения.

Поддерживаемые единицы времени

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

const rtf = new Intl.RelativeTimeFormat('en');

// These produce identical output
console.log(rtf.format(-5, 'second'));
// "5 seconds ago"

console.log(rtf.format(-5, 'seconds'));
// "5 seconds ago"

Доступные единицы от самой маленькой к самой большой:

  • second или seconds
  • minute или minutes
  • hour или hours
  • day или days
  • week или weeks
  • month или months
  • quarter или quarters
  • year или years

Единица «квартал» полезна для бизнес-приложений, отслеживающих финансовые периоды, а остальные охватывают типичные задачи форматирования относительного времени.

Вывод на естественном языке

Опция numeric определяет, будет ли форматер использовать числовые значения или альтернативы на естественном языке.

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

console.log(rtfNumeric.format(-1, 'day'));
// "1 day ago"

console.log(rtfNumeric.format(0, 'day'));
// "in 0 days"

console.log(rtfNumeric.format(1, 'day'));
// "in 1 day"

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

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

console.log(rtfAuto.format(-1, 'day'));
// "yesterday"

console.log(rtfAuto.format(0, 'day'));
// "today"

console.log(rtfAuto.format(1, 'day'));
// "tomorrow"

Такой вывод на естественном языке делает интерфейс более дружелюбным. Опция auto работает для всех единиц времени, но особенно заметна для дней. В других языках есть свои идиоматические варианты, которые API обрабатывает автоматически.

Стили форматирования

Опция style регулирует подробность вывода для разных интерфейсных сценариев:

const rtfLong = new Intl.RelativeTimeFormat('en', {
  style: 'long'
});

console.log(rtfLong.format(-2, 'hour'));
// "2 hours ago"

const rtfShort = new Intl.RelativeTimeFormat('en', {
  style: 'short'
});

console.log(rtfShort.format(-2, 'hour'));
// "2 hr. ago"

const rtfNarrow = new Intl.RelativeTimeFormat('en', {
  style: 'narrow'
});

console.log(rtfNarrow.format(-2, 'hour'));
// "2h ago"

Используйте стиль long (по умолчанию) для стандартных интерфейсов, где важна читаемость. Стиль short подходит для ограниченных по пространству интерфейсов, например, мобильных приложений или таблиц данных. Стиль narrow используйте для сверхкомпактных отображений, где важен каждый символ.

Вычисление разницы во времени

API Intl.RelativeTimeFormat форматирует значения, но не вычисляет их. Разницу во времени нужно рассчитывать самостоятельно и выбирать подходящие единицы. Такой подход позволяет вам контролировать логику вычислений, а сложность форматирования делегировать API.

Базовый расчет разницы во времени

Для определённой единицы времени вычислите разницу между двумя датами:

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

function formatDaysAgo(date) {
  const now = new Date();
  const diffInMs = date - now;
  const diffInDays = Math.round(diffInMs / (1000 * 60 * 60 * 24));

  return rtf.format(diffInDays, 'day');
}

const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);

console.log(formatDaysAgo(yesterday));
// "yesterday"

const nextWeek = new Date();
nextWeek.setDate(nextWeek.getDate() + 7);

console.log(formatDaysAgo(nextWeek));
// "in 7 days"

Этот подход подходит, когда вы заранее знаете, какая единица времени актуальна для вашего случая. Например, для отметок времени в комментариях обычно используют часы или дни, а для планирования событий — дни или недели.

Автоматический выбор единицы

Для универсального форматирования относительного времени выберите наиболее подходящую единицу в зависимости от величины разницы во времени:

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

const units = {
  year: 24 * 60 * 60 * 1000 * 365,
  month: 24 * 60 * 60 * 1000 * 365 / 12,
  week: 24 * 60 * 60 * 1000 * 7,
  day: 24 * 60 * 60 * 1000,
  hour: 60 * 60 * 1000,
  minute: 60 * 1000,
  second: 1000
};

function formatRelativeTime(date) {
  const now = new Date();
  const diffInMs = date - now;
  const absDiff = Math.abs(diffInMs);

  for (const [unit, msValue] of Object.entries(units)) {
    if (absDiff >= msValue || unit === 'second') {
      const value = Math.round(diffInMs / msValue);
      return rtf.format(value, unit);
    }
  }
}

const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000);
console.log(formatRelativeTime(fiveMinutesAgo));
// "5 minutes ago"

const threeDaysAgo = new Date(Date.now() - 3 * 24 * 60 * 60 * 1000);
console.log(formatRelativeTime(threeDaysAgo));
// "3 days ago"

В этой реализации перебираются единицы времени от самой крупной к самой мелкой, выбирая первую, где разница превышает значение единицы в миллисекундах. Переход к секундам гарантирует, что функция всегда вернёт результат.

Пороги для единиц времени используют приблизительные значения. Месяцы считаются как 1/12 года, без учёта разной длины месяцев. Такое приближение хорошо подходит для отображения относительного времени, где важнее читаемость, чем точность.

Поддержка интернационализации

Форматтер учитывает локальные правила отображения относительного времени. В разных языках свои правила склонения, порядок слов и идиоматические выражения.

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

console.log(rtfEnglish.format(-1, 'day'));
// "yesterday"

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

console.log(rtfSpanish.format(-1, 'day'));
// "ayer"

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

console.log(rtfJapanese.format(-1, 'day'));
// "昨日"

Правила склонения сильно различаются между языками. В английском различают одну и много (1 day и 2 days). В арабском шесть форм в зависимости от числа. В японском форма не меняется. API автоматически учитывает эти особенности.

const rtfArabic = new Intl.RelativeTimeFormat('ar');

console.log(rtfArabic.format(-1, 'day'));
// "قبل يوم واحد"

console.log(rtfArabic.format(-2, 'day'));
// "قبل يومين"

console.log(rtfArabic.format(-3, 'day'));
// "قبل 3 أيام"

console.log(rtfArabic.format(-11, 'day'));
// "قبل 11 يومًا"

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

Продвинутое форматирование с помощью formatToParts

Метод formatToParts() возвращает отформатированную строку в виде массива объектов, что позволяет кастомизировать стиль или обрабатывать отдельные части результата.

const rtf = new Intl.RelativeTimeFormat('en');

const parts = rtf.formatToParts(-5, 'second');

console.log(parts);
// [
//   { type: 'integer', value: '5', unit: 'second' },
//   { type: 'literal', value: ' seconds ago' }
// ]

Каждый объект part содержит:

  • type: либо integer для числовых значений, либо literal для текста
  • value: строковое содержимое этой части
  • unit: единица времени (присутствует у целых чисел)

Эта структура позволяет кастомно отображать данные, если нужно, например, стилизовать числа отдельно от текста или выделять отдельные компоненты для отображения:

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

function formatWithStyledNumber(value, unit) {
  const parts = rtf.formatToParts(value, unit);

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

console.log(formatWithStyledNumber(-5, 'hour'));
// "<strong>5</strong> hours ago"

Когда вы используете numeric: 'auto' со значениями, у которых есть альтернативы на естественном языке, formatToParts() возвращает одну строковую часть:

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

const parts = rtf.formatToParts(-1, 'day');

console.log(parts);
// [
//   { type: 'literal', value: 'yesterday' }
// ]

Такое поведение позволяет определить, используется ли естественный язык или числовое форматирование, чтобы применять разное оформление или логику в зависимости от типа вывода.

Оптимизация производительности

Создание экземпляров Intl.RelativeTimeFormat включает загрузку данных локали и инициализацию правил форматирования. Эта операция достаточно затратная, чтобы избегать её повторения без необходимости.

Кэшируйте экземпляры форматтеров

Создайте форматтеры один раз и переиспользуйте их:

const formatterCache = new Map();

function getFormatter(locale, options = {}) {
  const cacheKey = `${locale}-${JSON.stringify(options)}`;

  if (!formatterCache.has(cacheKey)) {
    formatterCache.set(
      cacheKey,
      new Intl.RelativeTimeFormat(locale, options)
    );
  }

  return formatterCache.get(cacheKey);
}

// Reuse cached formatters
const rtf = getFormatter('en', { numeric: 'auto' });
console.log(rtf.format(-1, 'day'));
// "yesterday"

Эта стратегия кэширования особенно важна при форматировании большого количества временных меток, например, при отображении ленты активности или комментариев.

Минимизируйте избыточные вычисления

Храните временные метки вместо постоянного пересчёта относительного времени:

// Store the creation date
const comment = {
  text: "Great article!",
  createdAt: new Date('2025-10-14T10:30:00Z')
};

// Calculate relative time only when rendering
function renderComment(comment, locale) {
  const rtf = getFormatter(locale, { numeric: 'auto' });
  const units = {
    day: 24 * 60 * 60 * 1000,
    hour: 60 * 60 * 1000,
    minute: 60 * 1000,
    second: 1000
  };

  const diffInMs = comment.createdAt - new Date();
  const absDiff = Math.abs(diffInMs);

  for (const [unit, msValue] of Object.entries(units)) {
    if (absDiff >= msValue || unit === 'second') {
      const value = Math.round(diffInMs / msValue);
      return rtf.format(value, unit);
    }
  }
}

Такой подход разделяет хранение данных и отображение, позволяя пересчитывать относительное время при смене локали пользователя или обновлении интерфейса без изменения исходных данных.

Практическая реализация

Объединение логики вычислений и форматирования даёт переиспользуемую утилиту, подходящую для продакшн-приложений:

class RelativeTimeFormatter {
  constructor(locale = 'en', options = { numeric: 'auto' }) {
    this.formatter = new Intl.RelativeTimeFormat(locale, options);

    this.units = [
      { name: 'year', ms: 24 * 60 * 60 * 1000 * 365 },
      { name: 'month', ms: 24 * 60 * 60 * 1000 * 365 / 12 },
      { name: 'week', ms: 24 * 60 * 60 * 1000 * 7 },
      { name: 'day', ms: 24 * 60 * 60 * 1000 },
      { name: 'hour', ms: 60 * 60 * 1000 },
      { name: 'minute', ms: 60 * 1000 },
      { name: 'second', ms: 1000 }
    ];
  }

  format(date) {
    const now = new Date();
    const diffInMs = date - now;
    const absDiff = Math.abs(diffInMs);

    for (const unit of this.units) {
      if (absDiff >= unit.ms || unit.name === 'second') {
        const value = Math.round(diffInMs / unit.ms);
        return this.formatter.format(value, unit.name);
      }
    }
  }
}

// Usage
const formatter = new RelativeTimeFormatter('en');

const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000);
console.log(formatter.format(fiveMinutesAgo));
// "5 minutes ago"

const tomorrow = new Date(Date.now() + 24 * 60 * 60 * 1000);
console.log(formatter.format(tomorrow));
// "tomorrow"

Этот класс инкапсулирует и форматтер, и логику выбора единицы, предоставляя удобный интерфейс, который принимает объекты Date и возвращает отформатированные строки.

Интеграция с фреймворками

В приложениях на React создайте форматтер один раз и передавайте его через context или props:

import { createContext, useContext } from 'react';

const RelativeTimeContext = createContext(null);

export function RelativeTimeProvider({ locale, children }) {
  const formatter = new RelativeTimeFormatter(locale);

  return (
    <RelativeTimeContext.Provider value={formatter}>
      {children}
    </RelativeTimeContext.Provider>
  );
}

export function useRelativeTime() {
  const formatter = useContext(RelativeTimeContext);
  if (!formatter) {
    throw new Error('useRelativeTime must be used within RelativeTimeProvider');
  }
  return formatter;
}

// Component usage
function CommentTimestamp({ date }) {
  const formatter = useRelativeTime();
  return <time>{formatter.format(date)}</time>;
}

Такой подход гарантирует, что форматтеры создаются один раз для каждой локали и используются всеми компонентами, которым нужен формат относительного времени.

Поддержка браузеров

Intl.RelativeTimeFormat работает во всех современных браузерах с охватом 95% по всему миру:

  • Chrome 71+
  • Firefox 65+
  • Safari 14+
  • Edge 79+

Internet Explorer не поддерживает этот API. Для приложений, которым нужна поддержка IE, доступны polyfill'ы, но нативная реализация обеспечивает лучшую производительность и меньший размер бандла.

Когда использовать этот API

Intl.RelativeTimeFormat лучше всего подходит для:

  • Отображения возраста контента в лентах и таймлайнах
  • Показа времени комментариев или постов
  • Форматирования расписания событий относительно текущего времени
  • Создания систем уведомлений с относительными метками времени
  • Ведения журналов активности с человекочитаемым временем

API не подходит для:

  • Абсолютного форматирования даты и времени (используйте Intl.DateTimeFormat)
  • Точного отслеживания времени с миллисекундной точностью
  • Таймеров обратного отсчёта, обновляющихся каждую секунду
  • Датной арифметики или календарных вычислений

Если приложению нужно отображать и относительное, и абсолютное время, комбинируйте Intl.RelativeTimeFormat с Intl.DateTimeFormat. Показывайте относительное время для свежего контента, а для старого переключайтесь на абсолютные даты:

function formatTimestamp(date, locale = 'en') {
  const now = new Date();
  const diffInMs = Math.abs(date - now);
  const sevenDaysInMs = 7 * 24 * 60 * 60 * 1000;

  if (diffInMs < sevenDaysInMs) {
    const rtf = new RelativeTimeFormatter(locale);
    return rtf.format(date);
  } else {
    const dtf = new Intl.DateTimeFormat(locale, {
      year: 'numeric',
      month: 'short',
      day: 'numeric'
    });
    return dtf.format(date);
  }
}

const yesterday = new Date(Date.now() - 24 * 60 * 60 * 1000);
console.log(formatTimestamp(yesterday));
// "yesterday"

const lastMonth = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
console.log(formatTimestamp(lastMonth));
// "Sep 14, 2025"

Такой гибридный подход даёт преимущества естественного языка для недавнего контента и сохраняет понятность для старых меток времени.