Механизмы Intl.NumberFormat и
Intl.DateTimeFormat предоставляют не только готовую строку
через метод format, но и структурированное представление
результата через formatToParts. Именно этот инструмент
является основой для построения пользовательских шаблонов
форматирования, где требуется полный контроль над тем, как
локализованные компоненты собираются в итоговую строку.
Метод возвращает массив токенов:
const nf = new Intl.NumberFormat("ru-RU", {
style: "currency",
currency: "RUB"
});
nf.formatToParts(1234.56);
Результат:
[
{ type: "integer", value: "1" },
{ type: "group", value: " " },
{ type: "integer", value: "234" },
{ type: "decimal", value: "," },
{ type: "fraction", value: "56" },
{ type: "literal", value: " " },
{ type: "currency", value: "₽" }
]
Каждый элемент несёт семантический смысл, а не просто текст. Это позволяет переопределять порядок, добавлять обёртки и внедрять произвольную разметку.
Построение пользовательского шаблона начинается с редукции массива частей:
function formatCurrencyParts(value) {
const nf = new Intl.NumberFormat("ru-RU", {
style: "currency",
currency: "RUB"
});
return nf.formatToParts(value)
.map(part => {
if (part.type === "currency") {
return `<span class="currency">${part.value}</span>`;
}
if (part.type === "decimal") {
return `<span class="decimal">${part.value}</span>`;
}
return part.value;
})
.join("");
}
Такой подход позволяет отделять визуальные элементы от локализованной логики. Важное свойство: локаль определяет структуру, а шаблон — представление.
Числовые форматы часто требуют нестандартного отображения: валютные блоки, компактные обозначения, дробные акценты.
const nf = new Intl.NumberFormat("en-US", {
notation: "compact",
compactDisplay: "short"
});
nf.formatToParts(1200000);
Результат может включать:
[
{ type: "integer", value: "1" },
{ type: "decimal", value: "." },
{ type: "fraction", value: "2" },
{ type: "literal", value: "M" }
]
При сборке шаблона возможно выделение числовой и суффиксной части:
function formatCompact(value) {
return nf.formatToParts(value)
.reduce((acc, part) => {
if (part.type === "literal") {
return acc + `<sup>${part.value}</sup>`;
}
return acc + part.value;
}, "");
}
Такой подход используется в интерфейсах финансовых и аналитических систем, где важно визуально отделить масштаб числа.
Intl.DateTimeFormat предоставляет аналогичный
механизм:
const dtf = new Intl.DateTimeFormat("ru-RU", {
year: "numeric",
month: "long",
day: "numeric",
hour: "2-digit",
minute: "2-digit"
});
dtf.formatToParts(new Date());
Результат содержит структурные элементы:
[
{ type: "day", value: "26" },
{ type: "literal", value: " " },
{ type: "month", value: "мая" },
{ type: "literal", value: " " },
{ type: "year", value: "2026" },
{ type: "literal", value: ", " },
{ type: "hour", value: "14" },
{ type: "literal", value: ":" },
{ type: "minute", value: "05" }
]
Сборка шаблона может учитывать контекст отображения:
function formatDateCustom(date) {
return dtf.formatToParts(date)
.map(part => {
switch (part.type) {
case "month":
return `<strong>${part.value}</strong>`;
case "day":
return `<span class="day">${part.value}</span>`;
default:
return part.value;
}
})
.join("");
}
Пользовательские шаблоны часто требуют объединения нескольких форматтеров: числа, даты, проценты и списки.
const price = new Intl.NumberFormat("ru-RU", {
style: "currency",
currency: "RUB"
});
const date = new Intl.DateTimeFormat("ru-RU", {
year: "numeric",
month: "short"
});
Композиция через части:
function renderInvoice(amount, createdAt) {
const formattedPrice = price.formatToParts(amount)
.map(p => p.value)
.join("");
const formattedDate = date.formatToParts(createdAt)
.map(p => p.value)
.join("");
return `
<div class="invoice">
<div class="amount">${formattedPrice}</div>
<div class="date">${formattedDate}</div>
</div>
`;
}
Такой подход сохраняет локализационную корректность, избегая ручного форматирования значений.
Каждый форматтер возвращает строго типизированные части. Основные типы:
integerfractiondecimalgroupcurrencyliteralyear, month, dayhour, minute, secondЭта структура позволяет строить универсальные шаблоны без знания конкретного языка или региона.
Пример универсального обработчика:
function wrapParts(parts, map) {
return parts.map(p => {
if (map[p.type]) {
return map[p.type](p.value);
}
return p.value;
}).join("");
}
Использование:
wrapParts(nf.formatToParts(9999), {
integer: v => `<b>${v}</b>`,
currency: v => `<i>${v}</i>`
});
В разных локалях порядок частей может меняться. Например, валюта может располагаться до или после числа, а разделители групп могут отличаться.
new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" });
new Intl.NumberFormat("ru-RU", { style: "currency", currency: "RUB" });
Использование formatToParts устраняет зависимость от
порядка:
Пользовательские шаблоны часто включают дополнительные элементы интерфейса: иконки, единицы измерения, ссылки.
function formatSpeed(value) {
const nf = new Intl.NumberFormat("en-US", {
maximumFractionDigits: 1
});
return nf.formatToParts(value)
.map(part => {
if (part.type === "integer") {
return `<span>${part.value}</span>`;
}
return part.value;
})
.join("") + " km/h";
}
Такой подход сохраняет локализованное число, но расширяет его доменной семантикой.
При построении шаблонов через formatToParts сохраняется
контроль, но появляется ответственность за корректную сборку:
Особенно важно не полагаться на фиксированные типы, а рассматривать их как расширяемую систему токенов.
Наиболее устойчивые подходы:
1. Проход без изменения структуры
parts.map(p => p.value).join("");
2. Точечное оборачивание
if (p.type === "fraction") { ... }
3. Таблица обработчиков
const handlers = {
currency: v => `<span>${v}</span>`
};
4. Двухэтапная композиция
Пользовательский шаблон в контексте Intl API фактически не заменяет форматирование, а накладывается поверх него. Структурные части выступают промежуточным представлением между локализацией и отображением.
raw value → Intl formatter → parts → template engine → UI string
Такая модель обеспечивает отделение: