Как форматировать даты в разных часовых поясах

Отображайте даты и время для любого часового пояса с помощью API Intl.DateTimeFormat в JavaScript

Введение

Один и тот же момент времени отображается как разное время на часах по всему миру. Когда встреча начинается в 15:00 в Нью-Йорке, в Лондоне часы показывают 20:00, а в Токио — 5:00 утра следующего дня. Если ваше приложение показывает время без учёта часовых поясов, пользователи видят некорректную информацию.

Пользователь из Калифорнии, бронируя рейс с вылетом в 14:00, ожидает увидеть именно 14:00. Если ваше приложение форматирует всё время по часовому поясу сервера в Вирджинии, пользователь увидит 17:00 и приедет в аэропорт на три часа позже. Такая путаница возникает каждый раз, когда в вашем приложении отображается время.

API JavaScript Intl.DateTimeFormat предоставляет опцию timeZone для форматирования дат и времени для любого часового пояса. В этом уроке объясняется, зачем нужны часовые пояса, как JavaScript работает с ними внутри и как правильно форматировать даты для пользователей из любой точки мира.

Почему существуют часовые пояса

Земля вращается, из-за чего день и ночь наступают в разных местах в разное время. Когда солнце в зените в Нью-Йорке, в Лондоне оно уже зашло, а в Токио ещё не взошло. В каждом месте полдень наступает в свой момент.

До появления стандартных часовых поясов каждый город жил по своему местному времени, ориентируясь на положение солнца. Это создавало проблемы для железных дорог и телеграфа, соединявших отдалённые города. В 1884 году страны договорились разделить мир на часовые пояса шириной примерно 15 градусов долготы, что соответствует одному часу вращения Земли.

Каждый часовой пояс имеет стандартное смещение относительно всемирного координированного времени (UTC). В Нью-Йорке используется UTC-5 или UTC-4 в зависимости от перехода на летнее время. В Лондоне — UTC+0 или UTC+1. В Токио круглый год действует UTC+9.

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

Как JavaScript хранит время внутри

Объекты Date в JavaScript представляют собой один момент времени в виде количества миллисекунд, прошедших с полуночи 1 января 1970 года по UTC. Это внутреннее представление не содержит информации о часовом поясе.

const date = new Date('2025-03-15T20:00:00Z');
console.log(date.getTime());
// Output: 1742331600000

Символ Z в конце строки ISO указывает на время по UTC. Объект Date хранит метку времени 1742331600000, которая обозначает один и тот же момент для всех людей в мире, независимо от их часового пояса.

Когда вы вызываете методы вроде toString() у объекта Date, JavaScript преобразует UTC-метку времени в ваше локальное время для отображения. Это автоматическое преобразование может запутать, если вы хотите показать время для другого часового пояса.

API Intl.DateTimeFormat с опцией timeZone даёт вам явный контроль над тем, какой часовой пояс использовать для форматирования.

Использование опции timeZone

Опция timeZone определяет, какой часовой пояс использовать при форматировании даты. Передавайте эту опцию как часть объекта настроек при создании форматтера.

const date = new Date('2025-03-15T20:00:00Z');

const formatter = new Intl.DateTimeFormat('en-US', {
  timeZone: 'America/New_York',
  dateStyle: 'short',
  timeStyle: 'short'
});

console.log(formatter.format(date));
// Output: "3/15/25, 3:00 PM"

Дата соответствует 20:00 по UTC. В Нью-Йорке действует UTC-5 в стандартное время или UTC-4 в период летнего времени. В марте действует летнее время, поэтому в Нью-Йорке UTC-4. Форматтер преобразует 20:00 по UTC в 16:00 по местному времени, но в примере указано 15:00, что говорит о стандартном времени в этом конкретном случае.

Форматтер автоматически выполняет преобразование. Вы указываете момент времени в UTC и целевую временную зону, а API возвращает правильное локальное время.

Понимание названий временных зон IANA

Параметр timeZone принимает идентификаторы временных зон из базы данных IANA Time Zone. Эти идентификаторы используют формат Region/City, например America/New_York, Europe/London или Asia/Tokyo.

const date = new Date('2025-03-15T20:00:00Z');

const zones = [
  'America/New_York',
  'Europe/London',
  'Asia/Tokyo',
  'Australia/Sydney'
];

zones.forEach(zone => {
  const formatter = new Intl.DateTimeFormat('en-US', {
    timeZone: zone,
    dateStyle: 'short',
    timeStyle: 'long'
  });

  console.log(`${zone}: ${formatter.format(date)}`);
});

// Output:
// America/New_York: 3/15/25, 4:00:00 PM EDT
// Europe/London: 3/15/25, 8:00:00 PM GMT
// Asia/Tokyo: 3/16/25, 5:00:00 AM JST
// Australia/Sydney: 3/16/25, 7:00:00 AM AEDT

В каждой временной зоне для одного и того же момента времени отображается разное локальное время. В Нью-Йорке показывается 16:00 15 марта. В Лондоне — 20:00 15 марта. В Токио и Сиднее уже наступило 16 марта, там показывается соответственно 5:00 и 7:00.

Названия IANA автоматически учитывают переход на летнее время. Форматтер знает, что America/New_York переключается между восточным стандартным временем и восточным летним временем и применяет правильное смещение для любой даты.

Как найти корректные названия временных зон IANA

В базе данных IANA содержатся несколько сотен идентификаторов временных зон. К распространённым шаблонам относятся:

  • America/New_York для Нью-Йорка
  • America/Los_Angeles для Лос-Анджелеса
  • America/Chicago для Чикаго
  • Europe/London для Лондона
  • Europe/Paris для Парижа
  • Europe/Berlin для Берлина
  • Asia/Tokyo для Токио
  • Asia/Shanghai для Шанхая
  • Asia/Kolkata для Индии
  • Australia/Sydney для Сиднея
  • Pacific/Auckland для Окленда

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

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

Использование UTC как часового пояса

Специальный идентификатор UTC форматирует даты по всемирному координированному времени (UTC), которое не имеет смещения относительно нулевого меридиана.

const date = new Date('2025-03-15T20:00:00Z');

const formatter = new Intl.DateTimeFormat('en-US', {
  timeZone: 'UTC',
  dateStyle: 'short',
  timeStyle: 'long'
});

console.log(formatter.format(date));
// Output: "3/15/25, 8:00:00 PM UTC"

Время UTC совпадает с внутренней временной меткой, хранящейся в объекте Date. Использование UTC для форматирования полезно, когда нужно отображать время, которое не должно меняться в зависимости от местоположения пользователя, например, для серверных логов или временных меток в базе данных.

Получение часового пояса пользователя

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

const formatter = new Intl.DateTimeFormat();
const options = formatter.resolvedOptions();

console.log(options.timeZone);
// Output: "America/New_York" (or user's actual time zone)

Это позволяет получить IANA-идентификатор текущего часового пояса пользователя. Его можно использовать для форматирования других дат в той же зоне или для хранения пользовательских предпочтений по часовому поясу.

const userTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;

const formatter = new Intl.DateTimeFormat('en-US', {
  timeZone: userTimeZone,
  dateStyle: 'full',
  timeStyle: 'long'
});

const date = new Date('2025-03-15T20:00:00Z');
console.log(formatter.format(date));
// Output varies based on user's time zone

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

Форматирование одного и того же момента для нескольких часовых поясов

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

const meetingTime = new Date('2025-03-15T20:00:00Z');

const zones = [
  { name: 'New York', zone: 'America/New_York' },
  { name: 'London', zone: 'Europe/London' },
  { name: 'Tokyo', zone: 'Asia/Tokyo' }
];

zones.forEach(({ name, zone }) => {
  const formatter = new Intl.DateTimeFormat('en-US', {
    timeZone: zone,
    dateStyle: 'long',
    timeStyle: 'short'
  });

  console.log(`${name}: ${formatter.format(meetingTime)}`);
});

// Output:
// New York: March 15, 2025 at 4:00 PM
// London: March 15, 2025 at 8:00 PM
// Tokyo: March 16, 2025 at 5:00 AM

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

Форматирование дат без времени в разных часовых поясах

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

const date = new Date('2025-03-16T01:00:00Z');

const formatter1 = new Intl.DateTimeFormat('en-US', {
  timeZone: 'America/Los_Angeles',
  dateStyle: 'long'
});

const formatter2 = new Intl.DateTimeFormat('en-US', {
  timeZone: 'Asia/Tokyo',
  dateStyle: 'long'
});

console.log(`Los Angeles: ${formatter1.format(date)}`);
console.log(`Tokyo: ${formatter2.format(date)}`);

// Output:
// Los Angeles: March 15, 2025
// Tokyo: March 16, 2025

Момент времени 1:00 по UTC 16 марта соответствует 17:00 15 марта в Лос-Анджелесе и 10:00 16 марта в Токио. Календарная дата в этих двух часовых поясах отличается на один день.

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

Использование смещений часовых поясов

Вместо идентификаторов IANA можно использовать строки смещения, такие как +01:00 или -05:00. Они обозначают фиксированные смещения относительно UTC.

const date = new Date('2025-03-15T20:00:00Z');

const formatter = new Intl.DateTimeFormat('en-US', {
  timeZone: '+09:00',
  dateStyle: 'short',
  timeStyle: 'long'
});

console.log(formatter.format(date));
// Output: "3/16/25, 5:00:00 AM GMT+9"

Строки смещения подходят, когда вы знаете точное смещение, но не конкретное местоположение. Однако они не учитывают переход на летнее время. Если вы используете -05:00 для обозначения Нью-Йорка, время будет неверным во время действия летнего времени, когда в Нью-Йорке фактически используется -04:00.

Идентификаторы IANA предпочтительнее, потому что они автоматически учитывают переход на летнее время.

Как работает переход на летнее время

Во многих регионах смещение часов меняется дважды в год из-за перехода на летнее время. Весной часы переводят вперёд на час, осенью — назад на час. Это значит, что смещение UTC для одного и того же места меняется в течение года.

Когда вы используете идентификаторы часовых поясов IANA, API Intl.DateTimeFormat автоматически применяет правильное смещение для любой даты.

const winterDate = new Date('2025-01-15T20:00:00Z');
const summerDate = new Date('2025-07-15T20:00:00Z');

const formatter = new Intl.DateTimeFormat('en-US', {
  timeZone: 'America/New_York',
  dateStyle: 'long',
  timeStyle: 'long'
});

console.log(`Winter: ${formatter.format(winterDate)}`);
console.log(`Summer: ${formatter.format(summerDate)}`);

// Output:
// Winter: January 15, 2025 at 3:00:00 PM EST
// Summer: July 15, 2025 at 4:00:00 PM EDT

В январе в Нью-Йорке действует восточное стандартное время со смещением UTC-5, и показывается 15:00. В июле в Нью-Йорке действует восточное летнее время со смещением UTC-4, и показывается 16:00. Одно и то же время по UTC даёт разные локальные значения в зависимости от того, действует ли летнее время.

Вам не нужно отслеживать, какие даты используют какой сдвиг. API делает это автоматически.

Форматирование времени для планирования событий

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

const eventTime = new Date('2025-03-15T18:00:00Z');
const eventTimeZone = 'Europe/Paris';
const userTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;

const eventFormatter = new Intl.DateTimeFormat('en-US', {
  timeZone: eventTimeZone,
  dateStyle: 'full',
  timeStyle: 'short'
});

const userFormatter = new Intl.DateTimeFormat('en-US', {
  timeZone: userTimeZone,
  dateStyle: 'full',
  timeStyle: 'short'
});

console.log(`Event time: ${eventFormatter.format(eventTime)} (Paris)`);
console.log(`Your time: ${userFormatter.format(eventTime)}`);

// Output (for a user in New York):
// Event time: Saturday, March 15, 2025 at 7:00 PM (Paris)
// Your time: Saturday, March 15, 2025 at 2:00 PM

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

Форматирование серверных меток времени по времени пользователя

Серверные логи и записи в базе данных часто используют метки времени в UTC. При отображении их пользователям переводите их в локальное время пользователя.

const serverTimestamp = new Date('2025-03-15T20:00:00Z');
const userTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;

const formatter = new Intl.DateTimeFormat(navigator.language, {
  timeZone: userTimeZone,
  dateStyle: 'short',
  timeStyle: 'medium'
});

console.log(`Activity: ${formatter.format(serverTimestamp)}`);
// Output varies based on user's time zone and locale
// For en-US in New York: "Activity: 3/15/25, 4:00:00 PM"

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

Комбинирование timeZone с другими опциями

Опция timeZone работает со всеми другими Intl.DateTimeFormat опциями. Можно указывать отдельные компоненты даты и времени, использовать готовые стили или управлять календарной системой.

const date = new Date('2025-03-15T20:00:00Z');

const formatter = new Intl.DateTimeFormat('en-US', {
  timeZone: 'Asia/Tokyo',
  weekday: 'long',
  year: 'numeric',
  month: 'long',
  day: 'numeric',
  hour: 'numeric',
  minute: 'numeric',
  second: 'numeric',
  timeZoneName: 'long'
});

console.log(formatter.format(date));
// Output: "Monday, March 16, 2025 at 5:00:00 AM Japan Standard Time"

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

Чего стоит избегать

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

// Incorrect - abbreviations may not work
const formatter = new Intl.DateTimeFormat('en-US', {
  timeZone: 'EST',  // This may throw an error
  dateStyle: 'short',
  timeStyle: 'short'
});

Всегда используйте IANA-идентификаторы, такие как America/New_York, или строки-смещения, например -05:00.

Не предполагайте, что временная зона пользователя совпадает с серверной. Всегда явно форматируйте время в нужной зоне или используйте определённую зону пользователя.

Повторное использование форматтеров в разных часовых поясах

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

const dates = [
  new Date('2025-03-15T20:00:00Z'),
  new Date('2025-03-16T14:00:00Z'),
  new Date('2025-03-17T09:00:00Z')
];

const formatter = new Intl.DateTimeFormat('en-US', {
  timeZone: 'Europe/Berlin',
  dateStyle: 'short',
  timeStyle: 'short'
});

dates.forEach(date => {
  console.log(formatter.format(date));
});

// Output:
// "3/15/25, 9:00 PM"
// "3/16/25, 3:00 PM"
// "3/17/25, 10:00 AM"

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