Как разбить форматированный вывод на части для стилизации

Используйте formatToParts(), чтобы получить доступ к отдельным компонентам форматированного вывода для кастомной стилизации

Введение

Метод format() у JavaScript-форматтеров возвращает готовые строки вроде "$1,234.56" или "15 января 2025 г.". Это удобно для простого отображения, но вы не сможете по-разному стилизовать отдельные части. Например, нельзя сделать символ валюты жирным, выделить цветом название месяца или добавить кастомную разметку к определённым компонентам.

В JavaScript есть метод formatToParts(), который решает эту проблему. Вместо одной строки он возвращает массив объектов, каждый из которых представляет отдельную часть форматированного вывода. У каждой части есть тип, например currency, month или element, и значение с текстом. Вы можете обработать эти части, чтобы применить свою стилизацию, собрать сложные макеты или интегрировать форматированный контент в продвинутые интерфейсы.

Метод formatToParts() доступен во многих Intl-форматтерах, включая NumberFormat, DateTimeFormat, ListFormat, RelativeTimeFormat и DurationFormat. Это делает такой подход единым для всей интернационализации форматирования в JavaScript.

Почему форматированные строки сложно стилизовать

Когда вы получаете форматированную строку вроде "$1,234.56", вы не можете легко определить, где заканчивается символ валюты и начинается число. В разных локалях символы располагаются по-разному. В некоторых используются другие разделители. Надёжно распарсить такие строки можно только с помощью сложной логики, которая дублирует правила форматирования, уже реализованные в Intl API.

Представьте дашборд, где суммы денег отображаются с символом валюты другого цвета. С помощью format() вам пришлось бы:

  1. Определить, какие символы являются символом валюты
  2. Учесть пробелы между символом и числом
  3. Обработать разные позиции символа в разных локалях
  4. Аккуратно парсить строку, чтобы не испортить число

Такой подход ненадёжен и подвержен ошибкам. Любое изменение правил форматирования локали ломает вашу логику парсинга.

Такая же проблема возникает с датами, списками и другим отформатированным выводом. Нельзя надёжно распарсить отформатированные строки, чтобы выделить компоненты, не реализовав заново все правила форматирования для каждой локали.

Метод formatToParts() решает эту проблему, предоставляя компоненты отдельно. Вы получаете структурированные данные, которые точно указывают, какая часть за что отвечает, независимо от локали.

Как работает formatToParts

Метод formatToParts() работает так же, как format(), за исключением возвращаемого значения. Вы создаёте форматтер с теми же опциями, а затем вызываете formatToParts() вместо format().

Метод возвращает массив объектов. Каждый объект содержит два свойства:

  • type: определяет, что означает часть, например currency, month или literal
  • value: содержит строку для этой части

Части идут в том же порядке, что и в отформатированном выводе. Это можно проверить, если соединить все значения — получится тот же результат, что и при вызове format().

Этот паттерн одинаков для всех форматтеров, поддерживающих formatToParts(). Конкретные типы частей зависят от форматтера, но структура всегда одна и та же.

Разделение форматированных чисел на части

Форматтер NumberFormat предоставляет formatToParts() для разбиения форматированных чисел. Это работает для обычных чисел, валют, процентов и других числовых стилей.

const formatter = new Intl.NumberFormat("en-US", {
  style: "currency",
  currency: "USD"
});

const parts = formatter.formatToParts(1234.56);
console.log(parts);

Это выводит массив объектов:

[
  { type: "currency", value: "$" },
  { type: "integer", value: "1" },
  { type: "group", value: "," },
  { type: "integer", value: "234" },
  { type: "decimal", value: "." },
  { type: "fraction", value: "56" }
]

Каждый объект определяет, что означает часть, и содержит её значение. Тип currency обозначает символ валюты. Тип integer обозначает целые цифры. Тип group обозначает разделитель тысяч. Тип decimal обозначает десятичную точку. Тип fraction обозначает цифры после запятой.

Вы можете проверить, что части совпадают с отформатированным выводом:

const formatted = parts.map(part => part.value).join("");
console.log(formatted);
// Output: "$1,234.56"

Объединённые части дают тот же результат, что и вызов format().

Стилизация символов валюты в форматированных числах

Основной кейс для formatToParts() — применять разные стили к разным компонентам. Можно обработать массив частей и обернуть определённые типы в HTML-элементы.

Сделать символ валюты жирным:

const formatter = new Intl.NumberFormat("en-US", {
  style: "currency",
  currency: "USD"
});

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

console.log(html);
// Output: "<strong>$</strong>1,234.56"

Этот подход работает с любым языком разметки. Можно генерировать HTML, JSX или любой другой формат, обрабатывая массив частей.

Стилизация дробной части по-другому:

const formatter = new Intl.NumberFormat("en-US", {
  minimumFractionDigits: 2
});

const parts = formatter.formatToParts(1234.5);
const html = parts
  .map(part => {
    if (part.type === "decimal" || part.type === "fraction") {
      return `<span class="text-gray-500">${part.value}</span>`;
    }
    return part.value;
  })
  .join("");

console.log(html);
// Output: "1,234<span class="text-gray-500">.50</span>"

Этот приём часто встречается в ценниках, где дробная часть отображается меньше или светлее.

Разделение форматированных дат на части

Форматтер DateTimeFormat предоставляет formatToParts() для разбиения форматированных дат и времени.

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

const date = new Date(2025, 0, 15);
const parts = formatter.formatToParts(date);
console.log(parts);

Это выводит массив объектов:

[
  { type: "month", value: "January" },
  { type: "literal", value: " " },
  { type: "day", value: "15" },
  { type: "literal", value: ", " },
  { type: "year", value: "2025" }
]

Тип month обозначает название или номер месяца. Тип day обозначает день месяца. Тип year обозначает год. Тип literal обозначает пробелы, знаки препинания или другой текст, добавленный форматтером.

Стилизация названий месяцев в отформатированных датах

Можно применять собственные стили к компонентам даты по тому же принципу, что и к числам.

Выделение названия месяца жирным:

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

const date = new Date(2025, 0, 15);
const parts = formatter.formatToParts(date);
const html = parts
  .map(part => {
    if (part.type === "month") {
      return `<strong>${part.value}</strong>`;
    }
    return part.value;
  })
  .join("");

console.log(html);
// Output: "<strong>January</strong> 15, 2025"

Стилизация нескольких компонентов даты:

const formatter = new Intl.DateTimeFormat("en-US", {
  weekday: "long",
  year: "numeric",
  month: "long",
  day: "numeric"
});

const date = new Date(2025, 0, 15);
const parts = formatter.formatToParts(date);

const html = parts
  .map(part => {
    switch (part.type) {
      case "weekday":
        return `<span class="font-bold">${part.value}</span>`;
      case "month":
        return `<span class="text-blue-600">${part.value}</span>`;
      case "year":
        return `<span class="text-gray-500">${part.value}</span>`;
      default:
        return part.value;
    }
  })
  .join("");

console.log(html);
// Output: "<span class="font-bold">Wednesday</span>, <span class="text-blue-600">January</span> 15, <span class="text-gray-500">2025</span>"

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

Разделение отформатированных списков на части

Форматтер ListFormat предоставляет formatToParts() для разбора отформатированных списков.

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

const items = ["apples", "oranges", "bananas"];
const parts = formatter.formatToParts(items);
console.log(parts);

Это выводит массив объектов:

[
  { type: "element", value: "apples" },
  { type: "literal", value: ", " },
  { type: "element", value: "oranges" },
  { type: "literal", value: ", and " },
  { type: "element", value: "bananas" }
]

Тип element представляет каждый элемент списка. Тип literal — это разделители и союзы, добавляемые форматтером.

Индивидуальная стилизация элементов списка

Можно применять собственные стили к элементам списка по тому же принципу.

Выделение элементов списка жирным:

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

const items = ["apples", "oranges", "bananas"];
const parts = formatter.formatToParts(items);
const html = parts
  .map(part => {
    if (part.type === "element") {
      return `<strong>${part.value}</strong>`;
    }
    return part.value;
  })
  .join("");

console.log(html);
// Output: "<strong>apples</strong>, <strong>oranges</strong>, and <strong>bananas</strong>"

Стилизация отдельных элементов списка:

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

const items = ["apples", "oranges", "bananas"];
const parts = formatter.formatToParts(items);

let itemIndex = 0;
const html = parts
  .map(part => {
    if (part.type === "element") {
      const currentIndex = itemIndex++;
      if (currentIndex === 0) {
        return `<span class="text-green-600">${part.value}</span>`;
      }
      return part.value;
    }
    return part.value;
  })
  .join("");

console.log(html);
// Output: "<span class="text-green-600">apples</span>, oranges, and bananas"

Этот подход позволяет выделять отдельные элементы, сохраняя корректное форматирование для выбранной локали.

Разделение отформатированных относительных времён на части

Форматтер RelativeTimeFormat предоставляет formatToParts() для разбора относительных временных выражений.

const formatter = new Intl.RelativeTimeFormat("en-US", {
  numeric: "auto"
});

const parts = formatter.formatToParts(-1, "day");
console.log(parts);

Это выводит массив объектов:

[
  { type: "literal", value: "yesterday" }
]

Для числовых относительных дат:

const formatter = new Intl.RelativeTimeFormat("en-US", {
  numeric: "always"
});

const parts = formatter.formatToParts(-3, "day");
console.log(parts);
// [
//   { type: "integer", value: "3" },
//   { type: "literal", value: " days ago" }
// ]

Тип integer обозначает числовое значение. Тип literal обозначает единицу относительного времени и направление.

Разделение форматированных длительностей на части

Форматтер DurationFormat предоставляет formatToParts() для разбиения форматированных длительностей.

const formatter = new Intl.DurationFormat("en-US", {
  style: "long"
});

const parts = formatter.formatToParts({
  hours: 2,
  minutes: 30,
  seconds: 15
});
console.log(parts);

Это выводит массив объектов, похожий на:

[
  { type: "integer", value: "2" },
  { type: "literal", value: " hours, " },
  { type: "integer", value: "30" },
  { type: "literal", value: " minutes, " },
  { type: "integer", value: "15" },
  { type: "literal", value: " seconds" }
]

Тип integer обозначает числовые значения. Тип literal обозначает имена единиц и разделители.

Сборка HTML из форматированных частей

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

function formatWithStyles(parts, styleMap) {
  return parts
    .map(part => {
      const style = styleMap[part.type];
      if (style) {
        return `<span class="${style}">${part.value}</span>`;
      }
      return part.value;
    })
    .join("");
}

const numberFormatter = new Intl.NumberFormat("en-US", {
  style: "currency",
  currency: "USD"
});

const parts = numberFormatter.formatToParts(1234.56);
const html = formatWithStyles(parts, {
  currency: "font-bold text-gray-700",
  integer: "text-2xl",
  fraction: "text-sm text-gray-500"
});

console.log(html);
// Output: "<span class="font-bold text-gray-700">$</span><span class="text-2xl">1</span>,<span class="text-2xl">234</span>.<span class="text-sm text-gray-500">56</span>"

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

Понимание порядка частей для конкретной локали

Массив частей автоматически учитывает правила форматирования для локали. В разных языках компоненты располагаются в разном порядке и используют разные форматы, но formatToParts() обрабатывает эти различия.

const usdFormatter = new Intl.NumberFormat("en-US", {
  style: "currency",
  currency: "USD"
});

console.log(usdFormatter.formatToParts(1234.56));
// [
//   { type: "currency", value: "$" },
//   { type: "integer", value: "1" },
//   { type: "group", value: "," },
//   { type: "integer", value: "234" },
//   { type: "decimal", value: "." },
//   { type: "fraction", value: "56" }
// ]

const eurFormatter = new Intl.NumberFormat("de-DE", {
  style: "currency",
  currency: "EUR"
});

console.log(eurFormatter.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: "€" }
// ]

В немецком формате валюта ставится после числа с пробелом. Разделитель групп — точка, а десятичный разделитель — запятая. Ваш код для стилизации обрабатывает массив частей одинаково для любой локали, а форматирование подстраивается автоматически.

Создание доступных форматированных отображений

Можно использовать formatToParts(), чтобы добавить атрибуты доступности к форматированному выводу. Это помогает скринридерам правильно озвучивать значения.

const formatter = new Intl.NumberFormat("en-US", {
  style: "currency",
  currency: "USD"
});

function formatAccessible(number) {
  const parts = formatter.formatToParts(number);
  const formatted = parts.map(part => part.value).join("");

  return `<span aria-label="${number} US dollars">${formatted}</span>`;
}

console.log(formatAccessible(1234.56));
// Output: "<span aria-label="1234.56 US dollars">$1,234.56</span>"

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

Комбинирование formatToParts с компонентами фреймворков

Современные фреймворки, такие как React, могут использовать formatToParts() для эффективной сборки компонентов.

function CurrencyDisplay({ value, locale, currency }) {
  const formatter = new Intl.NumberFormat(locale, {
    style: "currency",
    currency: currency
  });

  const parts = formatter.formatToParts(value);

  return (
    <span className="currency-display">
      {parts.map((part, index) => {
        if (part.type === "currency") {
          return <strong key={index}>{part.value}</strong>;
        }
        if (part.type === "fraction" || part.type === "decimal") {
          return <span key={index} className="text-sm text-gray-500">{part.value}</span>;
        }
        return <span key={index}>{part.value}</span>;
      })}
    </span>
  );
}

Этот компонент применяет разное оформление к разным частям, при этом корректно форматирует данные для любого языка и валюты.

Когда использовать formatToParts

Используйте format(), когда вам нужна простая отформатированная строка без дополнительной настройки. Это самый частый вариант для большинства случаев отображения.

Используйте formatToParts(), если вам нужно:

  • Применять разное оформление к отдельным частям форматированного результата
  • Создавать HTML или JSX с отформатированным контентом
  • Добавлять атрибуты или метаданные к отдельным компонентам
  • Встраивать форматированный результат в сложные макеты
  • Обрабатывать форматированный результат программно
  • Создавать кастомные визуальные решения с детальным контролем

Метод formatToParts() немного более ресурсоёмкий, чем format(), потому что возвращает массив объектов, а не одну строку. Эта разница несущественна для обычных приложений, но если вы форматируете тысячи значений в секунду, format() работает быстрее.

В большинстве случаев выбирайте подходящий вариант исходя из ваших потребностей в оформлении, а не из соображений производительности. Если не требуется кастомизация вывода, используйте format(). Если нужен особый стиль или разметка, используйте formatToParts().

Общие типы частей для разных форматтеров

Разные форматтеры создают разные типы частей, но некоторые встречаются в нескольких форматтерах:

  • literal: Пробелы, знаки препинания или другой текст, добавляемый при форматировании. Встречается в датах, числах, списках и длительностях.
  • integer: Целые цифры. Встречается в числах, относительном времени и длительностях.
  • decimal: Десятичный разделитель. Встречается в числах.
  • fraction: Десятичные цифры. Встречается в числах.

Типы, специфичные для форматтера, включают:

  • Числа: currency, group, percentSign, minusSign, plusSign, unit, compact, exponentInteger
  • Даты: weekday, era, year, month, day, hour, minute, second, dayPeriod, timeZoneName
  • Списки: element
  • Относительное время: числовые значения отображаются как integer, текст — как literal

Понимание этих типов помогает писать стили, которые корректно обрабатывают любой вывод форматтера.