Объекты Intl в JavaScript предназначены не только для
форматирования строк в нужной локали. Одной из важнейших возможностей
API является получение структурированных частей форматированного
значения. Вместо готовой строки можно получить массив токенов,
описывающих смысл каждого фрагмента: число, разделитель, месяц, валюта,
знак, дробная часть и так далее.
Такой подход называется семантической разметкой форматированного вывода.
Главные методы:
formatToParts() — разбивает строку на смысловые
части;formatRangeToParts() — предоставляет части диапазона
значений;resolvedOptions() — показывает параметры фактического
форматирования.Семантическая разметка особенно важна:
Обычный 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 — текстовое значение.Целая часть числа.
{
type: 'integer',
value: '456'
}
Разделитель групп разрядов.
Для русской локали:
{
type: 'group',
value: ' '
}
Для американской:
{
type: 'group',
value: ','
}
Разделитель дробной части.
{
type: 'decimal',
value: ','
}
Дробная часть числа.
{
type: 'fraction',
value: '78'
}
Символ или код валюты.
{
type: 'currency',
value: '€'
}
Статический текст.
Чаще всего пробелы или дополнительные символы.
{
type: 'literal',
value: ' '
}
Отрицательный знак.
{
type: 'minusSign',
value: '-'
}
Положительный знак.
{
type: 'plusSign',
value: '+'
}
Процентный символ.
{
type: 'percentSign',
value: '%'
}
Сокращённое обозначение числа.
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: 'млн' }
]
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)
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: ' г.' }
]
День месяца.
{
type: 'day',
value: '15'
}
Название или номер месяца.
{
type: 'month',
value: 'марта'
}
Год.
{
type: 'year',
value: '2026'
}
День недели.
{
type: 'weekday',
value: 'понедельник'
}
Часы.
{
type: 'hour',
value: '14'
}
Минуты.
{
type: 'minute',
value: '30'
}
Секунды.
{
type: 'second',
value: '45'
}
Период суток.
{
type: 'dayPeriod',
value: 'PM'
}
Название временной зоны.
{
type: 'timeZoneName',
value: 'GMT+3'
}
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('');
const date = new Date();
const formatter = new Intl.DateTimeFormat('ru-RU', {
dateStyle: 'long'
});
const html = `
<time datetime="${date.toISOString()}">
${formatter.format(date)}
</time>
`;
Метод 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
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'
}
]
formatRangeToParts() добавляет свойство:
source
Возможные значения:
sharedstartRangeendRangeЭто позволяет понимать:
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('');
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
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)
);
Результат:
١٢٣٬٤٥٦
Семантически размеченные части помогают:
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('');
Неправильно:
items.join(', ')
В разных языках правила различаются.
const formatter = new Intl.ListFormat('ru', {
style: 'long',
type: 'conjunction'
});
console.log(
formatter.format([
'HTML',
'CSS',
'JavaScript'
])
);
Результат:
HTML, CSS и JavaScript
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' }
]
const formatter =
new Intl.RelativeTimeFormat('ru');
console.log(
formatter.formatToParts(-3, 'day')
);
Результат:
[
{ type: 'literal', value: '3' },
{ type: 'literal', value: ' дня назад' }
]
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 поддерживаются:
Наиболее новые возможности:
formatRange()formatRangeToParts()могут отсутствовать в старых версиях браузеров.
Ошибка:
price.split(',')
Правильно:
formatter.formatToParts(price)
Ошибка:
parts
.filter(p => p.type !== 'literal')
Проблема:
Ошибка:
[
integer,
decimal,
fraction,
currency
]
В разных локалях порядок может отличаться.
Плохо:
numbers.map(n => {
return new Intl.NumberFormat('ru')
.format(n);
});
Хорошо:
const formatter =
new Intl.NumberFormat('ru');
numbers.map(n => formatter.format(n));
Создание объектов Intl является сравнительно дорогой
операцией.
Семантический вывод через Intl: