Пользовательские шаблоны форматирования

Механизмы 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>
  `;
}

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


Работа с типами частей и семантикой

Каждый форматтер возвращает строго типизированные части. Основные типы:

  • integer
  • fraction
  • decimal
  • group
  • currency
  • literal
  • year, month, day
  • hour, 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 сохраняется контроль, но появляется ответственность за корректную сборку:

  • необходимость экранирования HTML (при веб-рендеринге)
  • сохранение порядка частей
  • обработка неизвестных типов
  • возможные изменения спецификации Intl

Особенно важно не полагаться на фиксированные типы, а рассматривать их как расширяемую систему токенов.


Практические паттерны построения шаблонов

Наиболее устойчивые подходы:

1. Проход без изменения структуры

parts.map(p => p.value).join("");

2. Точечное оборачивание

if (p.type === "fraction") { ... }

3. Таблица обработчиков

const handlers = {
  currency: v => `<span>${v}</span>`
};

4. Двухэтапная композиция

  • первый этап: локализация через Intl
  • второй этап: визуальная разметка

Обобщённая модель пользовательских шаблонов

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

raw value → Intl formatter → parts → template engine → UI string

Такая модель обеспечивает отделение:

  • языковых правил
  • бизнес-логики отображения
  • визуального представления