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

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

Введение

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

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

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

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

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

Рассмотрим панель управления, которая отображает денежные суммы с символом валюты другого цвета. С помощью 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);
// Вывод: "$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);
// Вывод: "<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);
// Вывод: "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);
// Вывод: "<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);
// Вывод: "<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);
// Результат: "<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);
// Результат: "<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: " часов, " },
  { type: "integer", value: "30" },
  { type: "literal", value: " минут, " },
  { type: "integer", value: "15" },
  { type: "literal", value: " секунд" }
]

Тип 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);
// Вывод: "<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} долларов США">${formatted}</span>`;
}

console.log(formatAccessible(1234.56));
// Вывод: "<span aria-label="1234.56 долларов США">$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

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