Семантическая разметка дат и чисел

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

Такой подход называется семантической разметкой форматированного вывода.

Главные методы:

  • formatToParts() — разбивает строку на смысловые части;
  • formatRangeToParts() — предоставляет части диапазона значений;
  • resolvedOptions() — показывает параметры фактического форматирования.

Семантическая разметка особенно важна:

  • при создании интерфейсов с кастомным стилем;
  • при подсветке отдельных частей даты или числа;
  • при адаптации вывода под accessibility;
  • при генерации HTML с отдельными элементами;
  • при интернационализации сложных UI-компонентов.

Метод formatToParts()

Общий принцип работы

Обычный format() возвращает готовую строку:

const formatter = new Intl.NumberFormat('ru-RU', {
  style: 'currency',
  currency: 'RUB'
});

console.log(formatter.format(123456.78));
// 123 456,78 ₽

Метод formatToParts() возвращает структуру:

const parts = formatter.formatToParts(123456.78);

console.log(parts);

Результат:

[
  { type: 'integer', value: '123' },
  { type: 'group', value: ' ' },
  { type: 'integer', value: '456' },
  { type: 'decimal', value: ',' },
  { type: 'fraction', value: '78' },
  { type: 'literal', value: ' ' },
  { type: 'currency', value: '₽' }
]

Каждый объект содержит:

  • type — семантический тип;
  • value — текстовое значение.

Семантические типы в NumberFormat

integer

Целая часть числа.

{
  type: 'integer',
  value: '456'
}

group

Разделитель групп разрядов.

Для русской локали:

{
  type: 'group',
  value: ' '
}

Для американской:

{
  type: 'group',
  value: ','
}

decimal

Разделитель дробной части.

{
  type: 'decimal',
  value: ','
}

fraction

Дробная часть числа.

{
  type: 'fraction',
  value: '78'
}

currency

Символ или код валюты.

{
  type: 'currency',
  value: '€'
}

literal

Статический текст.

Чаще всего пробелы или дополнительные символы.

{
  type: 'literal',
  value: ' '
}

minusSign

Отрицательный знак.

{
  type: 'minusSign',
  value: '-'
}

plusSign

Положительный знак.

{
  type: 'plusSign',
  value: '+'
}

percentSign

Процентный символ.

{
  type: 'percentSign',
  value: '%'
}

compact

Сокращённое обозначение числа.

const formatter = new Intl.NumberFormat('ru', {
  notation: 'compact'
});

console.log(
  formatter.formatToParts(1200000)
);

Результат:

[
  { type: 'integer', value: '1' },
  { type: 'decimal', value: ',' },
  { type: 'fraction', value: '2' },
  { type: 'literal', value: ' ' },
  { type: 'compact', value: 'млн' }
]

Генерация HTML из formatToParts()

Выделение валюты

const formatter = new Intl.NumberFormat('ru-RU', {
  style: 'currency',
  currency: 'RUB'
});

const html = formatter
  .formatToParts(12500)
  .map(part => {
    if (part.type === 'currency') {
      return `<strong>${part.value}</strong>`;
    }

    return part.value;
  })
  .join('');

console.log(html);

Результат:

12 500,00 <strong>₽</strong>

Стилизация дробной части

const formatter = new Intl.NumberFormat('de-DE', {
  minimumFractionDigits: 2
});

const html = formatter
  .formatToParts(1234.56)
  .map(part => {
    if (part.type === 'fraction') {
      return `<small>${part.value}</small>`;
    }

    return part.value;
  })
  .join('');

Локализация интерфейсов

Почему нельзя разбирать строки вручную

Неправильный подход:

const price = formatter.format(1234.56);

const parts = price.split(',');

Проблемы:

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

Корректный подход:

formatter.formatToParts(value)

Форматирование дат через formatToParts()

Базовый пример

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

const parts = formatter.formatToParts(
  new Date('2026-03-15')
);

console.log(parts);

Результат:

[
  { type: 'day', value: '15' },
  { type: 'literal', value: ' ' },
  { type: 'month', value: 'марта' },
  { type: 'literal', value: ' ' },
  { type: 'year', value: '2026' },
  { type: 'literal', value: ' г.' }
]

Типы частей в DateTimeFormat

day

День месяца.

{
  type: 'day',
  value: '15'
}

month

Название или номер месяца.

{
  type: 'month',
  value: 'марта'
}

year

Год.

{
  type: 'year',
  value: '2026'
}

weekday

День недели.

{
  type: 'weekday',
  value: 'понедельник'
}

hour

Часы.

{
  type: 'hour',
  value: '14'
}

minute

Минуты.

{
  type: 'minute',
  value: '30'
}

second

Секунды.

{
  type: 'second',
  value: '45'
}

dayPeriod

Период суток.

{
  type: 'dayPeriod',
  value: 'PM'
}

timeZoneName

Название временной зоны.

{
  type: 'timeZoneName',
  value: 'GMT+3'
}

Создание семантического HTML для дат

Выделение месяца

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

const html = formatter
  .formatToParts(new Date())
  .map(part => {
    if (part.type === 'month') {
      return `<span class="month">${part.value}</span>`;
    }

    return part.value;
  })
  .join('');

Использование тега time

const date = new Date();

const formatter = new Intl.DateTimeFormat('ru-RU', {
  dateStyle: 'long'
});

const html = `
<time datetime="${date.toISOString()}">
  ${formatter.format(date)}
</time>
`;

formatRange()

Форматирование диапазонов

Метод formatRange() предназначен для интервалов.

const formatter = new Intl.DateTimeFormat('ru-RU', {
  dateStyle: 'medium'
});

console.log(
  formatter.formatRange(
    new Date('2026-03-01'),
    new Date('2026-03-10')
  )
);

Результат:

01–10.03.2026

formatRangeToParts()

Семантическое представление диапазона

const formatter = new Intl.DateTimeFormat('en-US', {
  month: 'short',
  day: 'numeric'
});

const parts = formatter.formatRangeToParts(
  new Date('2026-06-10'),
  new Date('2026-06-15')
);

console.log(parts);

Результат:

[
  {
    type: 'month',
    value: 'Jun',
    source: 'shared'
  },
  {
    type: 'literal',
    value: ' ',
    source: 'shared'
  },
  {
    type: 'day',
    value: '10',
    source: 'startRange'
  },
  {
    type: 'literal',
    value: ' – ',
    source: 'shared'
  },
  {
    type: 'day',
    value: '15',
    source: 'endRange'
  }
]

Поле source

formatRangeToParts() добавляет свойство:

source

Возможные значения:

  • shared
  • startRange
  • endRange

Это позволяет понимать:

  • какие части общие;
  • какие относятся к началу диапазона;
  • какие относятся к концу.

Подсветка частей диапазона

const html = parts
  .map(part => {
    if (part.source === 'startRange') {
      return `<span class="start">${part.value}</span>`;
    }

    if (part.source === 'endRange') {
      return `<span class="end">${part.value}</span>`;
    }

    return part.value;
  })
  .join('');

resolvedOptions()

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

const formatter = new Intl.NumberFormat('ru', {
  style: 'currency',
  currency: 'RUB'
});

console.log(
  formatter.resolvedOptions()
);

Результат:

{
  locale: 'ru',
  numberingSystem: 'latn',
  style: 'currency',
  currency: 'RUB',
  minimumFractionDigits: 2,
  maximumFractionDigits: 2
}

Проверка выбранной локали

Автоматическая замена локали

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

const formatter = new Intl.DateTimeFormat(
  ['xx-YY', 'ru-RU']
);

console.log(
  formatter.resolvedOptions().locale
);

Результат:

ru-RU

numberingSystem

Система записи чисел

const formatter = new Intl.NumberFormat('ar-EG');

console.log(
  formatter.resolvedOptions()
    .numberingSystem
);

Возможный результат:

arab

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

const formatter = new Intl.NumberFormat(
  'en-US-u-nu-arab'
);

console.log(
  formatter.format(123456)
);

Результат:

١٢٣٬٤٥٦

Семантическая разметка и accessibility

Озвучивание экранными дикторами

Семантически размеченные части помогают:

  • корректно озвучивать валюты;
  • разделять визуальное и голосовое представление;
  • выделять важные фрагменты;
  • создавать доступные финансовые интерфейсы.

Пример доступного отображения цены

const formatter = new Intl.NumberFormat('ru-RU', {
  style: 'currency',
  currency: 'RUB'
});

const html = formatter
  .formatToParts(1999.99)
  .map(part => {
    switch (part.type) {
      case 'currency':
        return `
          <span aria-label="рублей">
            ${part.value}
          </span>
        `;

      case 'fraction':
        return `
          <small>
            ${part.value}
          </small>
        `;

      default:
        return part.value;
    }
  })
  .join('');

Intl.ListFormat и семантическая структура

Проблема ручного объединения списков

Неправильно:

items.join(', ')

В разных языках правила различаются.


Локализованный список

const formatter = new Intl.ListFormat('ru', {
  style: 'long',
  type: 'conjunction'
});

console.log(
  formatter.format([
    'HTML',
    'CSS',
    'JavaScript'
  ])
);

Результат:

HTML, CSS и JavaScript

formatToParts() в ListFormat

const formatter = new Intl.ListFormat('en', {
  style: 'long',
  type: 'conjunction'
});

console.log(
  formatter.formatToParts([
    'One',
    'Two',
    'Three'
  ])
);

Результат:

[
  { type: 'element', value: 'One' },
  { type: 'literal', value: ', ' },
  { type: 'element', value: 'Two' },
  { type: 'literal', value: ', and ' },
  { type: 'element', value: 'Three' }
]

Intl.RelativeTimeFormat

Семантическое форматирование относительного времени

const formatter =
  new Intl.RelativeTimeFormat('ru');

console.log(
  formatter.formatToParts(-3, 'day')
);

Результат:

[
  { type: 'literal', value: '3' },
  { type: 'literal', value: ' дня назад' }
]

Intl.PluralRules

Семантика множественного числа

const rules = new Intl.PluralRules('ru');

console.log(rules.select(1));
console.log(rules.select(2));
console.log(rules.select(5));

Результат:

one
few
many

Практический пример сложного компонента

Локализованная карточка товара

const priceFormatter =
  new Intl.NumberFormat('ru-RU', {
    style: 'currency',
    currency: 'RUB'
  });

const dateFormatter =
  new Intl.DateTimeFormat('ru-RU', {
    dateStyle: 'long'
  });

const priceHTML = priceFormatter
  .formatToParts(15999.95)
  .map(part => {
    if (part.type === 'currency') {
      return `
        <span class="currency">
          ${part.value}
        </span>
      `;
    }

    if (part.type === 'fraction') {
      return `
        <small>
          ${part.value}
        </small>
      `;
    }

    return part.value;
  })
  .join('');

const releaseHTML = dateFormatter
  .formatToParts(new Date())
  .map(part => {
    if (part.type === 'month') {
      return `
        <strong>
          ${part.value}
        </strong>
      `;
    }

    return part.value;
  })
  .join('');

console.log(priceHTML);
console.log(releaseHTML);

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

Современные методы Intl поддерживаются:

  • Chrome;
  • Firefox;
  • Safari;
  • Edge;
  • Node.js.

Наиболее новые возможности:

  • formatRange()
  • formatRangeToParts()

могут отсутствовать в старых версиях браузеров.


Типичные ошибки

Разбор строки через split()

Ошибка:

price.split(',')

Правильно:

formatter.formatToParts(price)

Игнорирование literal

Ошибка:

parts
  .filter(p => p.type !== 'literal')

Проблема:

  • теряются пробелы;
  • исчезают разделители;
  • ломается локализация.

Жёсткое ожидание порядка частей

Ошибка:

[
  integer,
  decimal,
  fraction,
  currency
]

В разных локалях порядок может отличаться.


Производительность

Повторное использование formatter

Плохо:

numbers.map(n => {
  return new Intl.NumberFormat('ru')
    .format(n);
});

Хорошо:

const formatter =
  new Intl.NumberFormat('ru');

numbers.map(n => formatter.format(n));

Создание объектов Intl является сравнительно дорогой операцией.


Архитектурные преимущества семантической разметки

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

  • избавляет от ручного парсинга;
  • делает интерфейс независимым от локали;
  • улучшает accessibility;
  • упрощает стилизацию;
  • обеспечивает корректную интернационализацию;
  • позволяет создавать универсальные UI-компоненты;
  • делает форматирование декларативным и безопасным.