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

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

Введение

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

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

API Intl.DateTimeFormat в JavaScript предоставляет опцию 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());
// Вывод: 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));
// Вывод: "3/15/25, 3:00 PM"

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

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

Понимание названий часовых поясов IANA

timeZone принимает идентификаторы часовых поясов из базы данных часовых поясов IANA. Эти идентификаторы используют формат 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)}`);
});

// Результат:
// 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));
// Вывод: "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);
// Вывод: "America/New_York" (или фактический часовой пояс пользователя)

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

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

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

Вы можете отформатировать один и тот же объект 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)}`);
});

// Вывод:
// New York: 15 марта 2025 г., 16:00
// London: 15 марта 2025 г., 20:00
// Tokyo: 16 марта 2025 г., 5:00

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

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

При форматировании дат без времени часовой пояс может влиять на то, какая календарная дата отображается. Дата в полночь по 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(`Лос-Анджелес: ${formatter1.format(date)}`);
console.log(`Токио: ${formatter2.format(date)}`);

// Результат:
// Лос-Анджелес: 15 марта 2025 г.
// Токио: 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));
// Результат: "16.03.25, 5:00:00 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(`Зима: ${formatter.format(winterDate)}`);
console.log(`Лето: ${formatter.format(summerDate)}`);

// Вывод:
// Зима: 15 января 2025 г., 15:00:00 EST
// Лето: 15 июля 2025 г., 16:00:00 EDT

В январе Нью-Йорк использует восточное стандартное время (Eastern Standard Time) со смещением UTC-5, показывая 15:00. В июле Нью-Йорк использует восточное летнее время (Eastern Daylight Time) со смещением 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(`Время события: ${eventFormatter.format(eventTime)} (Париж)`);
console.log(`Ваше время: ${userFormatter.format(eventTime)}`);

// Вывод (для пользователя в Нью-Йорке):
// Время события: суббота, 15 марта 2025 г., 19:00 (Париж)
// Ваше время: суббота, 15 марта 2025 г., 14:00

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

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

Серверные логи и записи в базе данных часто используют временные метки в формате 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(`Действие: ${formatter.format(serverTimestamp)}`);
// Вывод зависит от часового пояса и локали пользователя
// Для en-US в Нью-Йорке: "Действие: 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));
// Вывод: "Monday, March 16, 2025 at 5:00:00 AM Japan Standard Time"

Опция timeZoneName управляет тем, как название часового пояса отображается в выводе. Позднее эти опции будут рассмотрены более подробно.

Чего следует избегать

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

// Неправильно - сокращения могут не работать
const formatter = new Intl.DateTimeFormat('en-US', {
  timeZone: 'EST',  // Это может вызвать ошибку
  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));
});

// Результат:
// "3/15/25, 9:00 PM"
// "3/16/25, 3:00 PM"
// "3/17/25, 10:00 AM"

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