API Intl.RelativeTimeFormat

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

Введение

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

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

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

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

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

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

console.log(rtf.format(-1, 'day'));
// "1 день назад"

console.log(rtf.format(2, 'hour'));
// "через 2 часа"

console.log(rtf.format(-3, 'month'));
// "3 месяца назад"

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

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

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

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

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

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

// Эти примеры дают одинаковый результат
console.log(rtf.format(-5, 'second'));
// "5 секунд назад"

console.log(rtf.format(-5, 'seconds'));
// "5 секунд назад"

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

  • 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 день назад"

console.log(rtfNumeric.format(0, 'day'));
// "через 0 дней"

console.log(rtfNumeric.format(1, 'day'));
// "через 1 день"

Установка numeric в значение auto создает более идиоматичные фразы для общих значений:

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

console.log(rtfAuto.format(-1, 'day'));
// "вчера"

console.log(rtfAuto.format(0, 'day'));
// "сегодня"

console.log(rtfAuto.format(1, 'day'));
// "завтра"

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

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

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

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

console.log(rtfLong.format(-2, 'hour'));
// "2 часа назад"

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

console.log(rtfShort.format(-2, 'hour'));
// "2 ч. назад"

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

console.log(rtfNarrow.format(-2, 'hour'));
// "2ч назад"

Используйте стиль 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 vs 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' }
// ]

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

  • 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() возвращает одну часть типа literal:

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);
}

// Переиспользование кэшированных форматтеров
const rtf = getFormatter('en', { numeric: 'auto' });
console.log(rtf.format(-1, 'day'));
// "вчера"

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

Минимизация вычислительных затрат

Сохраняйте временные метки вместо повторного вычисления относительного времени:

// Сохранение даты создания
const comment = {
  text: "Отличная статья!",
  createdAt: new Date('2025-10-14T10:30:00Z')
};

// Вычисление относительного времени только при рендеринге
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);
      }
    }
  }
}

// Использование
const formatter = new RelativeTimeFormatter('en');

const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000);
console.log(formatter.format(fiveMinutesAgo));
// "5 минут назад"

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

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

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

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

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 должен использоваться внутри RelativeTimeProvider');
  }
  return formatter;
}

// Использование в компоненте
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, доступны полифилы, хотя нативная реализация обеспечивает лучшую производительность и меньший размер сборки.

Когда использовать этот 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));
// "вчера"

const lastMonth = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
console.log(formatTimestamp(lastMonth));
// "14 сент. 2025 г."

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