Стилизация списков

Работа с перечнями значений в интерфейсах требует не только простой конкатенации строк, но и учета правил конкретного языка: разделители, союзы, порядок элементов и пунктуация могут существенно отличаться. В экосистеме FormatJS для этого используется API Intl.ListFormat, обеспечивающий локализованное форматирование списков.

Базовая концепция форматирования списков

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

  • локаль пользователя;
  • семантику списка (соединение, разделение, перечисление единиц);
  • стиль отображения (длинный, короткий, узкий формат).

Основная идея заключается в том, что список рассматривается как языковая конструкция, а не просто массив строк.

Создание форматтера списка

Объект Intl.ListFormat создается с указанием локали и параметров форматирования:

const formatter = new Intl.ListFormat('ru', {
  style: 'long',
  type: 'conjunction'
});

Параметры:

  • locale — строка локали (ru, en, de, fr-CA и т.д.);
  • style — визуальная плотность представления;
  • type — логика соединения элементов.

Типы списков: conjunction, disjunction, unit

conjunction

Используется для перечисления с союзом «и»:

const formatter = new Intl.ListFormat('ru', {
  style: 'long',
  type: 'conjunction'
});

formatter.format(['яблоки', 'груши', 'сливы']);
// "яблоки, груши и сливы"

Особенности:

  • последний элемент соединяется союзом;
  • в разных языках союз может меняться;
  • запятые также локализуются.

disjunction

Используется для альтернативы («или»):

const formatter = new Intl.ListFormat('ru', {
  style: 'long',
  type: 'disjunction'
});

formatter.format(['HTML', 'PDF', 'DOCX']);
// "HTML, PDF или DOCX"

Применяется в UI, где требуется выбор одного варианта из нескольких.

unit

Используется для единиц измерения или технических списков:

const formatter = new Intl.ListFormat('en', {
  style: 'long',
  type: 'unit'
});

formatter.format(['5 кг', '200 г', '1 л']);

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

Стили отображения: long, short, narrow

long

Полноформатное отображение с традиционными союзами:

new Intl.ListFormat('ru', { style: 'long' });

Результат:

  • «яблоки, груши и сливы»

Используется по умолчанию для большинства интерфейсов.

short

Более компактное отображение, часто с сокращениями или упрощенными правилами:

new Intl.ListFormat('ru', { style: 'short' });

Результат может быть менее формальным, например:

  • «яблоки, груши, сливы»

narrow

Максимально компактный вариант без союзов:

new Intl.ListFormat('ru', { style: 'narrow' });

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

Поведение в разных локалях

Одно из ключевых преимуществ Intl.ListFormat заключается в полной локализационной адаптации.

Пример для английского языка:

const en = new Intl.ListFormat('en', { type: 'conjunction' });
en.format(['apples', 'bananas', 'oranges']);
// "apples, bananas, and oranges"

Пример для немецкого:

const de = new Intl.ListFormat('de', { type: 'conjunction' });
de.format(['Äpfel', 'Birnen', 'Pflaumen']);
// "Äpfel, Birnen und Pflaumen"

Различия:

  • порядок союзов;
  • наличие или отсутствие оксфордской запятой;
  • различия в пунктуации.

Использование в FormatJS и React Intl

В экосистеме FormatJS Intl.ListFormat часто используется напрямую или через интеграцию с react-intl.

Прямое использование:

import '@formatjs/intl-listformat/polyfill';
import '@formatjs/intl-listformat/locale-data/ru';

const formatter = new Intl.ListFormat('ru', {
  style: 'long',
  type: 'conjunction'
});

formatter.format(['один', 'два', 'три']);

В React Intl логика может быть обернута в утилитарные функции:

import { useIntl } from 'react-intl';

function useFormattedList() {
  const intl = useIntl();

  return (items) =>
    new Intl.ListFormat(intl.locale, {
      style: 'long',
      type: 'conjunction'
    }).format(items);
}

Такой подход обеспечивает согласованность локали во всём приложении.

Оптимизация и переиспользование форматтеров

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

const cache = new Map();

function getListFormatter(locale, options) {
  const key = `${locale}-${options.type}-${options.style}`;

  if (!cache.has(key)) {
    cache.set(key, new Intl.ListFormat(locale, options));
  }

  return cache.get(key);
}

Это особенно важно в React-приложениях, где повторные рендеры могут приводить к пересозданию объектов.

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

В реальных интерфейсах списки часто формируются динамически:

const errors = [];

if (!email) errors.push('Email');
if (!password) errors.push('Пароль');
if (!name) errors.push('Имя');

const formatter = new Intl.ListFormat('ru', {
  style: 'long',
  type: 'conjunction'
});

const message = `Не заполнены поля: ${formatter.format(errors)}`;

Результат:

  • «Не заполнены поля: Email, Пароль и Имя»

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

Локализация единиц измерения через type: unit

Режим unit применяется не только к физическим единицам, но и к любым структурным наборам данных:

const formatter = new Intl.ListFormat('en', {
  style: 'long',
  type: 'unit'
});

formatter.format(['10 ms', '20 ms', '30 ms']);
// "10 ms, 20 ms, 30 ms"

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

Влияние ICU и поддержки браузеров

Intl.ListFormat основан на ICU (International Components for Unicode), поэтому:

  • поведение строго соответствует стандартам локализации;
  • различия между языками централизованы в данных ICU;
  • не требуется ручное управление грамматикой.

В старых браузерах может отсутствовать нативная поддержка, поэтому FormatJS предоставляет полифил:

import '@formatjs/intl-listformat/polyfill';

Также подключаются данные локалей:

import '@formatjs/intl-listformat/locale-data/ru';
import '@formatjs/intl-listformat/locale-data/en';

Особенности форматирования вложенных данных

Хотя Intl.ListFormat работает со строками, на практике часто требуется предварительная трансформация объектов:

const users = [
  { name: 'Алексей' },
  { name: 'Мария' },
  { name: 'Иван' }
];

const formatter = new Intl.ListFormat('ru', {
  style: 'long',
  type: 'conjunction'
});

const result = formatter.format(users.map(u => u.name));

Такой подход сохраняет разделение ответственности: данные преобразуются отдельно от локализации.

Сочетание с другими API FormatJS

Intl.ListFormat часто используется вместе с:

  • Intl.NumberFormat для числовых значений в списках;
  • Intl.DateTimeFormat для дат;
  • ICU MessageFormat для сложных текстовых конструкций.

Пример комбинированного использования:

const numberFormatter = new Intl.NumberFormat('ru');
const listFormatter = new Intl.ListFormat('ru', { type: 'conjunction' });

const values = [1000, 2500, 500];

const formatted = listFormatter.format(
  values.map(v => numberFormatter.format(v))
);

Результат:

  • «1 000, 2 500 и 500»

Ограничения и нюансы поведения

Несмотря на универсальность, существуют особенности:

  • форматирование всегда зависит от локали;
  • невозможно задать полностью кастомные разделители;
  • поведение определяется спецификацией ICU;
  • вложенные структуры не поддерживаются напрямую.

При необходимости сложной логики форматирования требуется предварительная обработка данных до передачи в Intl.ListFormat.