Как отображать локализованные подписи для полей даты и времени?

Используйте Intl.DisplayNames для получения подписей полей и Intl.DateTimeFormat для получения названий месяцев и дней недели на любом языке.

Введение

Когда вы создаёте формы для ввода даты и времени, нужны подписи, которые объясняют каждое поле. В календаре нужны подписи вроде «Месяц», «Год» и «День». В выборе времени — «Час» и «Минута». Эти подписи должны отображаться на языке пользователя.

Жёстко прописывать эти подписи на английском не подходит для международных приложений. Французский пользователь ожидает увидеть «Mois» и «Année», а испанский — «Mes» и «Año». Вам нужна система, которая будет автоматически предоставлять эти подписи на любом языке.

В JavaScript есть два дополняющих друг друга API. Intl.DisplayNames позволяет получать подписи полей, такие как «Месяц» и «Год». А Intl.DateTimeFormat возвращает сами значения для этих полей, например, названия месяцев и дней недели.

Как устроены подписи для полей даты и времени

В интерфейсах с датой и временем нужны два типа подписей. Подписи полей объясняют, какие данные нужно ввести в каждое поле. Значения полей — это сами данные, которые появляются в выпадающих списках и селекторах.

К подписям полей относятся слова вроде «Год», «Месяц», «День», «Час», «Минута» и «Секунда». Они подписывают сами поля формы.

Значения полей — это названия месяцев, например, «Январь» и «Февраль», дни недели, такие как «Понедельник» и «Вторник», и обозначения периодов, например, «AM» и «PM». Они заполняют выпадающие списки и варианты выбора.

Полноценная форма даты требует оба типа подписей.

<label>Month</label>
<select>
  <option>January</option>
  <option>February</option>
  <option>March</option>
  <!-- more months -->
</select>

<label>Year</label>
<input type="number" />

И «Месяц», и «Январь» требуют локализации, но для этого нужны разные подходы.

Получение названий полей с помощью Intl.DisplayNames

Конструктор Intl.DisplayNames с type: "dateTimeField" возвращает локализованные названия для компонентов даты и времени.

const labels = new Intl.DisplayNames('en-US', { type: 'dateTimeField' });

console.log(labels.of('year'));
// "year"

console.log(labels.of('month'));
// "month"

console.log(labels.of('day'));
// "day"

console.log(labels.of('hour'));
// "hour"

console.log(labels.of('minute'));
// "minute"

console.log(labels.of('second'));
// "second"

Метод of() принимает код поля и возвращает его локализованное название. Название соответствует правилам выбранной локали.

Вы можете получить названия на любом языке, изменив локаль.

// Spanish labels
const esLabels = new Intl.DisplayNames('es-ES', { type: 'dateTimeField' });
console.log(esLabels.of('year'));
// "año"
console.log(esLabels.of('month'));
// "mes"
console.log(esLabels.of('day'));
// "día"
console.log(esLabels.of('hour'));
// "hora"
console.log(esLabels.of('minute'));
// "minuto"

// French labels
const frLabels = new Intl.DisplayNames('fr-FR', { type: 'dateTimeField' });
console.log(frLabels.of('year'));
// "année"
console.log(frLabels.of('month'));
// "mois"
console.log(frLabels.of('day'));
// "jour"
console.log(frLabels.of('hour'));
// "heure"
console.log(frLabels.of('minute'));
// "minute"

// Japanese labels
const jaLabels = new Intl.DisplayNames('ja-JP', { type: 'dateTimeField' });
console.log(jaLabels.of('year'));
// "年"
console.log(jaLabels.of('month'));
// "月"
console.log(jaLabels.of('day'));
// "日"
console.log(jaLabels.of('hour'));
// "時"
console.log(jaLabels.of('minute'));
// "分"

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

Доступные коды полей даты и времени

API Intl.DisplayNames поддерживает следующие коды полей.

const labels = new Intl.DisplayNames('en-US', { type: 'dateTimeField' });

console.log(labels.of('era'));
// "era"

console.log(labels.of('year'));
// "year"

console.log(labels.of('quarter'));
// "quarter"

console.log(labels.of('month'));
// "month"

console.log(labels.of('weekOfYear'));
// "week"

console.log(labels.of('weekday'));
// "day of the week"

console.log(labels.of('day'));
// "day"

console.log(labels.of('dayPeriod'));
// "AM/PM"

console.log(labels.of('hour'));
// "hour"

console.log(labels.of('minute'));
// "minute"

console.log(labels.of('second'));
// "second"

console.log(labels.of('timeZoneName'));
// "time zone"

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

Получение локализованных названий месяцев

API Intl.DateTimeFormat предоставляет названия месяцев для заполнения выпадающих списков и списков выбора. Создайте форматтер с опцией month, установленной в "long", затем форматируйте даты, соответствующие каждому месяцу.

function getMonthNames(locale) {
  const formatter = new Intl.DateTimeFormat(locale, {
    month: 'long',
    timeZone: 'UTC'
  });

  const months = [];
  for (let month = 0; month < 12; month++) {
    const date = new Date(Date.UTC(2000, month, 1));
    months.push(formatter.format(date));
  }

  return months;
}

console.log(getMonthNames('en-US'));
// ["January", "February", "March", "April", "May", "June",
//  "July", "August", "September", "October", "November", "December"]

Функция создаёт даты для каждого месяца года и форматирует их, чтобы получить название месяца. Установка timeZone: 'UTC' обеспечивает одинаковый результат во всех часовых поясах.

Вы можете получить названия месяцев на любом языке.

console.log(getMonthNames('es-ES'));
// ["enero", "febrero", "marzo", "abril", "mayo", "junio",
//  "julio", "agosto", "septiembre", "octubre", "noviembre", "diciembre"]

console.log(getMonthNames('fr-FR'));
// ["janvier", "février", "mars", "avril", "mai", "juin",
//  "juillet", "août", "septembre", "octobre", "novembre", "décembre"]

console.log(getMonthNames('ja-JP'));
// ["1月", "2月", "3月", "4月", "5月", "6月",
//  "7月", "8月", "9月", "10月", "11月", "12月"]

Каждая локаль форматирует названия месяцев по своим правилам.

Управление длиной названия месяца

Опция month принимает разные значения, которые определяют длину названия месяца.

Значение "long" возвращает полные названия месяцев.

const longFormatter = new Intl.DateTimeFormat('en-US', {
  month: 'long',
  timeZone: 'UTC'
});

const date = new Date(Date.UTC(2000, 0, 1));
console.log(longFormatter.format(date));
// "January"

Значение "short" возвращает сокращённые названия месяцев.

const shortFormatter = new Intl.DateTimeFormat('en-US', {
  month: 'short',
  timeZone: 'UTC'
});

console.log(shortFormatter.format(date));
// "Jan"

Значение "narrow" возвращает самые короткие названия месяцев, обычно одну букву.

const narrowFormatter = new Intl.DateTimeFormat('en-US', {
  month: 'narrow',
  timeZone: 'UTC'
});

console.log(narrowFormatter.format(date));
// "J"

Используйте "narrow" аккуратно, так как несколько месяцев могут начинаться на одну и ту же букву. Например, в английском языке January, June и July все начинаются с "J".

Получение локализованных названий дней недели

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

function getWeekdayNames(locale, format = 'long') {
  const formatter = new Intl.DateTimeFormat(locale, {
    weekday: format,
    timeZone: 'UTC'
  });

  const weekdays = [];
  // Start from a Sunday (January 2, 2000 was a Sunday)
  for (let day = 0; day < 7; day++) {
    const date = new Date(Date.UTC(2000, 0, 2 + day));
    weekdays.push(formatter.format(date));
  }

  return weekdays;
}

console.log(getWeekdayNames('en-US'));
// ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]

console.log(getWeekdayNames('en-US', 'short'));
// ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]

console.log(getWeekdayNames('en-US', 'narrow'));
// ["S", "M", "T", "W", "T", "F", "S"]

Вы можете получить названия дней недели на любом языке.

console.log(getWeekdayNames('es-ES'));
// ["domingo", "lunes", "martes", "miércoles", "jueves", "viernes", "sábado"]

console.log(getWeekdayNames('fr-FR'));
// ["dimanche", "lundi", "mardi", "mercredi", "jeudi", "vendredi", "samedi"]

console.log(getWeekdayNames('ja-JP'));
// ["日曜日", "月曜日", "火曜日", "水曜日", "木曜日", "金曜日", "土曜日"]

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

Получение локализованных обозначений времени суток

Обозначения времени суток — это индикаторы AM и PM, используемые в 12-часовом формате времени. Используйте formatToParts(), чтобы получить эти обозначения.

function getPeriodLabels(locale) {
  const formatter = new Intl.DateTimeFormat(locale, {
    hour: 'numeric',
    hour12: true,
    timeZone: 'UTC'
  });

  const amDate = new Date(Date.UTC(2000, 0, 1, 0, 0, 0));
  const pmDate = new Date(Date.UTC(2000, 0, 1, 12, 0, 0));

  const amParts = formatter.formatToParts(amDate);
  const pmParts = formatter.formatToParts(pmDate);

  const am = amParts.find(part => part.type === 'dayPeriod').value;
  const pm = pmParts.find(part => part.type === 'dayPeriod').value;

  return { am, pm };
}

console.log(getPeriodLabels('en-US'));
// { am: "AM", pm: "PM" }

console.log(getPeriodLabels('es-ES'));
// { am: "a. m.", pm: "p. m." }

console.log(getPeriodLabels('fr-FR'));
// { am: "AM", pm: "PM" }

console.log(getPeriodLabels('ja-JP'));
// { am: "午前", pm: "午後" }

Метод formatToParts() возвращает массив объектов, представляющих каждую часть отформатированного времени. Объект с type: "dayPeriod" содержит обозначение AM или PM.

В некоторых локалях по умолчанию используется 24-часовой формат времени и отсутствуют обозначения времени суток. Принудительно включить 12-часовой формат можно с помощью опции hour12: true.

Создание полной локализованной формы даты

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

function createDateForm(locale) {
  const fieldLabels = new Intl.DisplayNames(locale, { type: 'dateTimeField' });
  const monthNames = getMonthNames(locale);
  const currentYear = new Date().getFullYear();

  return {
    monthLabel: fieldLabels.of('month'),
    months: monthNames.map((name, index) => ({
      value: index + 1,
      label: name
    })),
    dayLabel: fieldLabels.of('day'),
    yearLabel: fieldLabels.of('year'),
    yearPlaceholder: currentYear
  };
}

// Helper function from previous example
function getMonthNames(locale) {
  const formatter = new Intl.DateTimeFormat(locale, {
    month: 'long',
    timeZone: 'UTC'
  });

  const months = [];
  for (let month = 0; month < 12; month++) {
    const date = new Date(Date.UTC(2000, month, 1));
    months.push(formatter.format(date));
  }

  return months;
}

const enForm = createDateForm('en-US');
console.log(enForm.monthLabel);
// "month"
console.log(enForm.months[0]);
// { value: 1, label: "January" }
console.log(enForm.dayLabel);
// "day"
console.log(enForm.yearLabel);
// "year"

const esForm = createDateForm('es-ES');
console.log(esForm.monthLabel);
// "mes"
console.log(esForm.months[0]);
// { value: 1, label: "enero" }
console.log(esForm.dayLabel);
// "día"
console.log(esForm.yearLabel);
// "año"

Эта структура содержит всё необходимое для отображения локализованной формы даты в HTML.

function renderDateForm(locale) {
  const form = createDateForm(locale);

  return `
    <div class="date-form">
      <div class="form-field">
        <label>${form.monthLabel}</label>
        <select name="month">
          ${form.months.map(month =>
            `<option value="${month.value}">${month.label}</option>`
          ).join('')}
        </select>
      </div>

      <div class="form-field">
        <label>${form.dayLabel}</label>
        <input type="number" name="day" min="1" max="31" />
      </div>

      <div class="form-field">
        <label>${form.yearLabel}</label>
        <input type="number" name="year" placeholder="${form.yearPlaceholder}" />
      </div>
    </div>
  `;
}

console.log(renderDateForm('en-US'));
// Renders form with English labels and month names

console.log(renderDateForm('fr-FR'));
// Renders form with French labels and month names

Форма автоматически адаптируется под любую выбранную локаль.

Создание локализованной формы времени

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

function createTimeForm(locale) {
  const fieldLabels = new Intl.DisplayNames(locale, { type: 'dateTimeField' });
  const periods = getPeriodLabels(locale);

  const hours = [];
  for (let hour = 1; hour <= 12; hour++) {
    hours.push({ value: hour, label: hour.toString() });
  }

  const minutes = [];
  for (let minute = 0; minute < 60; minute += 5) {
    minutes.push({
      value: minute,
      label: minute.toString().padStart(2, '0')
    });
  }

  return {
    hourLabel: fieldLabels.of('hour'),
    hours: hours,
    minuteLabel: fieldLabels.of('minute'),
    minutes: minutes,
    periodLabel: fieldLabels.of('dayPeriod'),
    periods: [
      { value: 'am', label: periods.am },
      { value: 'pm', label: periods.pm }
    ]
  };
}

// Helper function from previous example
function getPeriodLabels(locale) {
  const formatter = new Intl.DateTimeFormat(locale, {
    hour: 'numeric',
    hour12: true,
    timeZone: 'UTC'
  });

  const amDate = new Date(Date.UTC(2000, 0, 1, 0, 0, 0));
  const pmDate = new Date(Date.UTC(2000, 0, 1, 12, 0, 0));

  const amParts = formatter.formatToParts(amDate);
  const pmParts = formatter.formatToParts(pmDate);

  const am = amParts.find(part => part.type === 'dayPeriod').value;
  const pm = pmParts.find(part => part.type === 'dayPeriod').value;

  return { am, pm };
}

const enTime = createTimeForm('en-US');
console.log(enTime.hourLabel);
// "hour"
console.log(enTime.minuteLabel);
// "minute"
console.log(enTime.periodLabel);
// "AM/PM"
console.log(enTime.periods);
// [{ value: "am", label: "AM" }, { value: "pm", label: "PM" }]

const jaTime = createTimeForm('ja-JP');
console.log(jaTime.hourLabel);
// "時"
console.log(jaTime.minuteLabel);
// "分"
console.log(jaTime.periodLabel);
// "午前/午後"
console.log(jaTime.periods);
// [{ value: "am", label: "午前" }, { value: "pm", label: "午後" }]

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

Когда использовать подписи к полям даты и времени

Метки для полей даты и времени используются в разных типах интерфейсов.

Пользовательские выборщики даты и времени

Используйте локализованные метки при создании выборщиков даты, календарей или селекторов времени.

const locale = navigator.language;
const labels = new Intl.DisplayNames(locale, { type: 'dateTimeField' });

const datePicker = {
  yearLabel: labels.of('year'),
  monthLabel: labels.of('month'),
  dayLabel: labels.of('day')
};

Метки полей формы

Добавляйте метки к стандартным полям формы для данных даты и времени.

const labels = new Intl.DisplayNames('en-US', { type: 'dateTimeField' });

document.querySelector('#birthdate-month-label').textContent =
  labels.of('month');
document.querySelector('#birthdate-year-label').textContent =
  labels.of('year');

Метки для доступности

Добавляйте локализованные ARIA-метки для экранных дикторов и вспомогательных технологий.

const locale = navigator.language;
const labels = new Intl.DisplayNames(locale, { type: 'dateTimeField' });

const input = document.querySelector('#date-input');
input.setAttribute('aria-label', labels.of('year'));

Заголовки таблиц данных

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

const labels = new Intl.DisplayNames('en-US', { type: 'dateTimeField' });

const table = `
  <table>
    <thead>
      <tr>
        <th>${labels.of('year')}</th>
        <th>${labels.of('month')}</th>
        <th>${labels.of('day')}</th>
      </tr>
    </thead>
  </table>
`;

Поддержка браузеров

API Intl.DisplayNames с type: "dateTimeField" поддерживается в основных браузерах с марта 2022 года.

Chrome и Edge поддерживают это с версии 99. Firefox — с версии 99. Safari — с версии 15.4.

Вы можете проверить доступность этой функции перед использованием.

function supportsDateTimeFieldLabels() {
  try {
    const labels = new Intl.DisplayNames('en', { type: 'dateTimeField' });
    labels.of('year');
    return true;
  } catch (error) {
    return false;
  }
}

if (supportsDateTimeFieldLabels()) {
  const labels = new Intl.DisplayNames('en-US', { type: 'dateTimeField' });
  console.log(labels.of('month'));
} else {
  console.log('month'); // Fallback to English
}

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

const fallbackLabels = {
  en: {
    year: 'year',
    month: 'month',
    day: 'day',
    hour: 'hour',
    minute: 'minute',
    second: 'second'
  },
  es: {
    year: 'año',
    month: 'mes',
    day: 'día',
    hour: 'hora',
    minute: 'minuto',
    second: 'segundo'
  },
  fr: {
    year: 'année',
    month: 'mois',
    day: 'jour',
    hour: 'heure',
    minute: 'minute',
    second: 'seconde'
  }
};

function getFieldLabel(field, locale) {
  if (supportsDateTimeFieldLabels()) {
    const labels = new Intl.DisplayNames(locale, { type: 'dateTimeField' });
    return labels.of(field);
  }

  const language = locale.split('-')[0];
  return fallbackLabels[language]?.[field] || fallbackLabels.en[field];
}

console.log(getFieldLabel('month', 'es-ES'));
// "mes" (from API if supported, from fallback otherwise)

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

API Intl.DateTimeFormat для получения названий месяцев и дней недели поддерживается шире — начиная с Internet Explorer 11 и во всех современных браузерах. В большинстве случаев можно использовать его без проверки поддержки.