Создание динамических временных меток

Отображение времени в пользовательских интерфейсах требует не только форматирования даты, но и адаптации к контексту: значение должно изменяться в зависимости от разницы между текущим временем и моментом события. Статические строки вида «2026-05-26 14:30» плохо передают восприятие актуальности. Гораздо информативнее выражения «только что», «5 минут назад», «вчера», «через 2 часа». Для реализации подобных представлений в JavaScript используется семейство инструментов Intl API.

Базовое форматирование даты через Intl.DateTimeFormat

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

const date = new Date('2026-05-26T12:00:00Z');

const formatter = new Intl.DateTimeFormat('ru-RU', {
  year: 'numeric',
  month: 'long',
  day: 'numeric',
  hour: '2-digit',
  minute: '2-digit'
});

formatter.format(date);

Результат зависит от локали пользователя и может выглядеть как «26 мая 2026 г., 15:00». Однако такой формат не отражает относительность события.

Концепция относительного времени через Intl.RelativeTimeFormat

Для динамических временных меток используется Intl.RelativeTimeFormat. Этот инструмент предназначен для преобразования числовой разницы во времени в человекочитаемую строку.

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

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

Ключевой принцип работы — передача не даты, а разницы между текущим моментом и целевым временем. Первый аргумент — числовое значение, второй — единица времени: second, minute, hour, day, week, month, year.

Вычисление разницы между датами

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

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

function formatRelative(date) {
  const now = new Date();
  const diffInSeconds = (date - now) / 1000;

  const absDiff = Math.abs(diffInSeconds);

  if (absDiff < 60) {
    return rtf.format(Math.round(diffInSeconds), 'second');
  }

  const diffInMinutes = diffInSeconds / 60;
  if (Math.abs(diffInMinutes) < 60) {
    return rtf.format(Math.round(diffInMinutes), 'minute');
  }

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

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

Такой подход создаёт многоуровневую систему отображения времени, автоматически подстраивающуюся под масштаб разницы.

Динамическое обновление временных меток

Одной из особенностей относительного времени является необходимость периодического обновления. Значение «59 секунд назад» быстро становится «1 минуту назад», поэтому интерфейс требует синхронизации с текущим временем.

const timestamps = document.querySelectorAll('[data-time]');

function update() {
  timestamps.forEach(el => {
    const date = new Date(el.dataset.time);
    el.textContent = formatRelative(date);
  });
}

update();
setInterval(update, 1000);

Частота обновления зависит от требуемой точности. В большинстве интерфейсов достаточно интервала 30–60 секунд, поскольку изменения в секундах не всегда критичны для восприятия.

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

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

function getNextUpdateDelay() {
  const now = Date.now();
  const secondsUntilNextMinute = 60 - (Math.floor(now / 1000) % 60);
  return secondsUntilNextMinute * 1000;
}

Такой подход позволяет синхронизировать обновление с границами минут, снижая нагрузку на DOM.

Использование Intl.RelativeTimeFormat с разными локалями

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

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

en.format(-3, 'day'); // "3 days ago"
de.format(-3, 'day'); // "vor 3 Tagen"
fr.format(-3, 'day'); // "il y a 3 jours"

Единая логика остаётся неизменной, изменяется только языковое представление.

Гранулярность временных единиц

Выбор единицы измерения определяет естественность результата. Использование слишком мелкой единицы может ухудшить читаемость.

rtf.format(-90, 'second'); // менее удобно
rtf.format(-1, 'minute');  // более естественно

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

Интеграция с Intl.DateTimeFormat для fallback

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

function smartFormat(date) {
  const diff = Date.now() - date.getTime();
  const days = diff / (1000 * 60 * 60 * 24);

  if (days > 7) {
    return new Intl.DateTimeFormat('ru-RU', {
      year: 'numeric',
      month: 'short',
      day: 'numeric'
    }).format(date);
  }

  return formatRelative(date);
}

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

Работа с временными зонами

Intl API учитывает временные зоны через timeZone в DateTimeFormat. Это особенно важно при работе с глобальными сервисами.

const formatter = new Intl.DateTimeFormat('ru-RU', {
  timeZone: 'Europe/Moscow',
  hour: '2-digit',
  minute: '2-digit'
});

formatter.format(new Date());

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

Форматирование сложных диапазонов времени

При отображении интервалов используется formatRange, доступный в Intl.DateTimeFormat.

const formatter = new Intl.DateTimeFormat('ru', {
  year: 'numeric',
  month: 'short',
  day: 'numeric'
});

formatter.formatRange(
  new Date('2026-05-01'),
  new Date('2026-05-10')
);

Результат: «1–10 мая 2026 г.». В динамических интерфейсах такие диапазоны часто комбинируются с относительными метками для событий с продолжительностью.

Поведение округления и нюансы локализации

Intl.RelativeTimeFormat использует встроенные правила округления, которые могут отличаться от математических ожиданий. Например, значения 1.5 минуты могут интерпретироваться как 2 минуты в зависимости от реализации.

rtf.format(-1.5, 'minute'); // округление зависит от спецификации окружения

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

Согласованность отображения в интерфейсе

При построении интерфейсов с большим количеством временных меток важно избегать несогласованности форматов. Смешение «5 мин назад», «2 часа назад» и «вчера» в разных компонентах ухудшает восприятие.

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

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

  format(date) {
    const diff = (date - Date.now()) / 1000;

    const abs = Math.abs(diff);

    if (abs < 60) return this.rtf.format(Math.round(diff), 'second');
    if (abs < 3600) return this.rtf.format(Math.round(diff / 60), 'minute');
    if (abs < 86400) return this.rtf.format(Math.round(diff / 3600), 'hour');

    return this.rtf.format(Math.round(diff / 86400), 'day');
  }
};

Ограничения и особенности применения

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

Также следует учитывать производительность при большом количестве элементов. Intl API оптимизирован, но частые пересоздания форматтеров могут быть затратными, поэтому экземпляры Intl.RelativeTimeFormat и Intl.DateTimeFormat обычно переиспользуются.

Стабильность отображения при смене локали

Изменение локали в приложении требует пересоздания всех форматтеров, так как Intl API не изменяет существующие экземпляры.

function createFormatters(locale) {
  return {
    rtf: new Intl.RelativeTimeFormat(locale, { numeric: 'auto' }),
    dtf: new Intl.DateTimeFormat(locale)
  };
}

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