Как форматировать относительное время, например «3 дня назад» или «через 2 часа»?
Используйте Intl.RelativeTimeFormat, чтобы отображать время вроде «3 дня назад» или «через 2 часа» на любом языке с автоматическим учетом склонения и локализации
Введение
В лентах соцсетей, комментариях и журналах активности часто показываются метки времени вроде «5 минут назад», «2 часа назад» или «через 3 дня». Такие относительные метки помогают быстро понять, когда что-то произошло, не разбираясь в точной дате.
Если вы жёстко прописываете эти строки на английском, вы предполагаете, что все пользователи говорят по-английски и следуют английской грамматике. В разных языках относительное время выражается по-разному. Например, на испанском говорят «hace 3 días» вместо «3 days ago». В японском — «3日前», и структура совсем другая. В каждом языке свои правила склонения, которые определяют, когда использовать единственное или множественное число.
В JavaScript есть API Intl.RelativeTimeFormat, который автоматически форматирует относительное время. В этом уроке рассказывается, как правильно отображать относительное время на любом языке с помощью этого встроенного API.
Почему для форматирования относительного времени нужна интернационализация
В разных языках относительное время выражается по-разному. В английском единица времени ставится перед «ago» для прошедшего времени и после «in» для будущего. В других языках может быть другой порядок слов, другие предлоги или вообще другая грамматика.
const rtfEnglish = new Intl.RelativeTimeFormat('en');
console.log(rtfEnglish.format(-3, 'day'));
// "3 days ago"
const rtfSpanish = new Intl.RelativeTimeFormat('es');
console.log(rtfSpanish.format(-3, 'day'));
// "hace 3 días"
const rtfJapanese = new Intl.RelativeTimeFormat('ja');
console.log(rtfJapanese.format(-3, 'day'));
// "3 日前"
Каждый язык формирует естественные фразы по своим правилам. Вам не нужно знать эти правила или поддерживать отдельные переводы — API всё делает сам.
Правила склонения тоже сильно различаются. В английском есть разница между «1 day» и «2 days». В арабском — шесть форм в зависимости от числа. В японском форма не меняется вообще. API Intl.RelativeTimeFormat автоматически применяет нужные правила для каждого языка.
API Intl.RelativeTimeFormat
Конструктор Intl.RelativeTimeFormat создаёт форматтер, который преобразует числовые значения и единицы времени в локализованные строки. В качестве первого аргумента передаётся идентификатор локали, затем вызывается метод format() с числом и единицей времени.
const rtf = new Intl.RelativeTimeFormat('en-US');
console.log(rtf.format(-1, 'day'));
// "1 day ago"
console.log(rtf.format(2, 'hour'));
// "in 2 hours"
Метод format() принимает два параметра. Первый — это число, обозначающее количество времени. Второй — строка, указывающая единицу времени.
Отрицательные числа обозначают прошедшее время, а положительные — будущее. Такая конвенция делает API интуитивно понятным, когда вы понимаете принцип знака.
Форматирование прошедшего и будущего времени
Знак значения определяет, относится ли время к прошлому или будущему. Отрицательные значения дают прошедшее время, положительные — будущее.
const rtf = new Intl.RelativeTimeFormat('en-US');
console.log(rtf.format(-5, 'minute'));
// "5 minutes ago"
console.log(rtf.format(5, 'minute'));
// "in 5 minutes"
console.log(rtf.format(-2, 'week'));
// "2 weeks ago"
console.log(rtf.format(2, 'week'));
// "in 2 weeks"
Эта схема работает одинаково для всех единиц времени и всех языков. API автоматически выбирает правильную грамматическую структуру в зависимости от знака значения.
Доступные единицы времени
API поддерживает восемь единиц времени, которые покрывают большинство задач форматирования относительного времени. Можно использовать как единственное, так и множественное число — оба варианта работают одинаково.
const rtf = new Intl.RelativeTimeFormat('en-US');
console.log(rtf.format(-30, 'second'));
// "30 seconds ago"
console.log(rtf.format(-15, 'minute'));
// "15 minutes ago"
console.log(rtf.format(-6, 'hour'));
// "6 hours ago"
console.log(rtf.format(-3, 'day'));
// "3 days ago"
console.log(rtf.format(-2, 'week'));
// "2 weeks ago"
console.log(rtf.format(-4, 'month'));
// "4 months ago"
console.log(rtf.format(-1, 'quarter'));
// "1 quarter ago"
console.log(rtf.format(-2, 'year'));
// "2 years ago"
API принимает как единственное число, например day, так и множественное, например days. Оба варианта дают одинаковый результат. Единица quarter полезна для бизнес-приложений, связанных с финансовыми периодами.
Использование естественного языка с numeric auto
Опция numeric управляет тем, будет ли форматтер использовать числа или альтернативы на естественном языке. Значение по умолчанию — always, оно всегда показывает числа.
const rtfAlways = new Intl.RelativeTimeFormat('en-US', {
numeric: 'always'
});
console.log(rtfAlways.format(-1, 'day'));
// "1 day ago"
console.log(rtfAlways.format(0, 'day'));
// "in 0 days"
console.log(rtfAlways.format(1, 'day'));
// "in 1 day"
Установка numeric в значение auto делает формулировки более естественными для некоторых значений.
const rtfAuto = new Intl.RelativeTimeFormat('en-US', {
numeric: 'auto'
});
console.log(rtfAuto.format(-1, 'day'));
// "yesterday"
console.log(rtfAuto.format(0, 'day'));
// "today"
console.log(rtfAuto.format(1, 'day'));
// "tomorrow"
Эта опция делает интерфейс более разговорным. Пользователь увидит «вчера» вместо «1 день назад», что звучит естественнее. Опция auto работает для всех единиц времени и всех языков, при этом каждый язык использует свои идиоматические варианты.
Выбор стиля форматирования
Опция style управляет подробностью вывода. Доступно три стиля: long, short и narrow.
const rtfLong = new Intl.RelativeTimeFormat('en-US', {
style: 'long'
});
console.log(rtfLong.format(-2, 'hour'));
// "2 hours ago"
const rtfShort = new Intl.RelativeTimeFormat('en-US', {
style: 'short'
});
console.log(rtfShort.format(-2, 'hour'));
// "2 hr. ago"
const rtfNarrow = new Intl.RelativeTimeFormat('en-US', {
style: 'narrow'
});
console.log(rtfNarrow.format(-2, 'hour'));
// "2h ago"
Стиль long используется по умолчанию и подходит для большинства интерфейсов. Стиль short экономит место в мобильных макетах или таблицах. Стиль narrow обеспечивает максимально компактный вывод для очень ограниченных по пространству дизайнов.
Вычисление разницы во времени
API Intl.RelativeTimeFormat форматирует значения, но не вычисляет их. Необходимо самостоятельно рассчитать разницу во времени и передать результат в форматтер.
Чтобы вычислить разницу во времени, вычтите целевую дату из текущей, затем преобразуйте результат из миллисекунд в нужную единицу.
const rtf = new Intl.RelativeTimeFormat('en-US', { 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 tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
console.log(formatDaysAgo(tomorrow));
// "tomorrow"
Эта функция вычисляет разницу в днях между целевой датой и текущим моментом. Расчёт делит миллисекунды на количество миллисекунд в дне, затем округляет до ближайшего целого.
Вычитание date - now даёт отрицательное значение для прошедших дат и положительное для будущих. Это соответствует знаковой конвенции, которую ожидает метод format().
Создание полноценной утилитной функции
Для универсального форматирования относительного времени нужно выбирать наиболее подходящую единицу времени в зависимости от величины разницы во времени.
const rtf = new Intl.RelativeTimeFormat('en-US', { 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"
const tomorrow = new Date(Date.now() + 24 * 60 * 60 * 1000);
console.log(formatRelativeTime(tomorrow));
// "tomorrow"
Эта функция перебирает единицы времени от самой крупной к самой мелкой, выбирая первую, где абсолютная разница превышает значение единицы в миллисекундах. Запасной вариант с секундами гарантирует, что функция всегда вернёт результат.
Определения единиц используют приблизительные значения. Месяцы считаются как 1/12 года, без учёта разной длины месяцев. Такое приближение хорошо подходит для отображения относительного времени, когда важнее примерная оценка, а не точная.
Форматирование под язык пользователя
Вместо жёсткой привязки к определённой локали можно использовать предпочитаемый язык пользователя из браузера.
const userLocale = navigator.language;
const rtf = new Intl.RelativeTimeFormat(userLocale, { numeric: 'auto' });
const yesterday = new Date(Date.now() - 24 * 60 * 60 * 1000);
console.log(rtf.format(-1, 'day'));
// Output varies by user's locale
// For en-US: "yesterday"
// For es-ES: "ayer"
// For fr-FR: "hier"
// For de-DE: "gestern"
Такой подход отображает относительное время в соответствии с языковыми предпочтениями каждого пользователя, без необходимости вручную выбирать локаль. Браузер предоставляет языковые настройки, а API применяет нужные правила форматирования.
Как одно и то же время выглядит на разных языках
Одна и та же относительная величина времени даёт разный результат для разных локалей. Каждый язык использует свои правила порядка слов, грамматики и склонения.
const threeDaysAgo = -3;
const rtfEnglish = new Intl.RelativeTimeFormat('en-US');
console.log(rtfEnglish.format(threeDaysAgo, 'day'));
// "3 days ago"
const rtfSpanish = new Intl.RelativeTimeFormat('es-ES');
console.log(rtfSpanish.format(threeDaysAgo, 'day'));
// "hace 3 días"
const rtfFrench = new Intl.RelativeTimeFormat('fr-FR');
console.log(rtfFrench.format(threeDaysAgo, 'day'));
// "il y a 3 jours"
const rtfGerman = new Intl.RelativeTimeFormat('de-DE');
console.log(rtfGerman.format(threeDaysAgo, 'day'));
// "vor 3 Tagen"
const rtfJapanese = new Intl.RelativeTimeFormat('ja-JP');
console.log(rtfJapanese.format(threeDaysAgo, 'day'));
// "3 日前"
const rtfArabic = new Intl.RelativeTimeFormat('ar-SA');
console.log(rtfArabic.format(threeDaysAgo, 'day'));
// "قبل 3 أيام"
Каждый язык выдаёт естественно звучащий результат, который носители реально используют в разговоре. API берёт на себя всю сложность разных грамматических структур, письменностей и направлений письма.
Повторное использование форматтеров для производительности
Создание экземпляра Intl.RelativeTimeFormat включает загрузку данных локали и обработку опций. Если форматируешь несколько временных меток, создай форматтер один раз и используй повторно.
const rtf = new Intl.RelativeTimeFormat('en-US', { numeric: 'auto' });
const timestamps = [
new Date(Date.now() - 5 * 60 * 1000), // 5 minutes ago
new Date(Date.now() - 2 * 60 * 60 * 1000), // 2 hours ago
new Date(Date.now() - 24 * 60 * 60 * 1000) // 1 day ago
];
timestamps.forEach(date => {
const diffInMs = date - new Date();
const diffInMinutes = Math.round(diffInMs / (60 * 1000));
console.log(rtf.format(diffInMinutes, 'minute'));
});
Этот подход эффективнее, чем создание нового форматтера для каждого временного штампа. Разница в производительности становится заметной при форматировании сотен или тысяч временных меток в лентах активности или комментариях.
Использование относительного времени в интерфейсах
Вы можете использовать форматирование относительного времени везде, где отображаете временные метки для пользователей. Это включает ленты соцсетей, разделы комментариев, журналы активности, системы уведомлений и любые интерфейсы, где важно показать, как давно что-то произошло, чтобы пользователи лучше понимали контекст.
const rtf = new Intl.RelativeTimeFormat(navigator.language, {
numeric: 'auto'
});
function updateTimestamp(element, date) {
const now = new Date();
const diffInMs = date - now;
const diffInMinutes = Math.round(diffInMs / (60 * 1000));
element.textContent = rtf.format(diffInMinutes, 'minute');
}
const commentDate = new Date('2025-10-15T14:30:00');
const timestampElement = document.getElementById('comment-timestamp');
updateTimestamp(timestampElement, commentDate);
Отформатированные строки работают как любые другие строковые значения. Вы можете вставлять их в текст, атрибуты или любой другой контекст, где отображаете информацию для пользователей.