Как форматировать списки типа A, B и C в JavaScript?

Используйте Intl.ListFormat для форматирования массивов с учётом локальных союзов и разделителей.

Введение

Когда вы показываете пользователям список элементов, их нужно соединять запятыми и союзом, например, «и». В разных языках используются разные правила для форматирования списков. В английском — запятые и «and», в испанском — «y», во французском — «et», а в китайском — вообще другая пунктуация.

API Intl.ListFormat форматирует массивы в строки с правильными разделителями и союзами для выбранной локали. Это автоматически учитывает культурные различия в оформлении списков.

Проблемы ручного форматирования списков

Вы можете объединить элементы массива через запятую с помощью метода join().

const fruits = ["apples", "oranges", "bananas"];
const list = fruits.join(", ");
console.log(list);
// "apples, oranges, bananas"

У этого подхода две проблемы. Во-первых, он не добавляет союз перед последним элементом. Во-вторых, используется английская пунктуация, которая не подходит для других языков.

Можно вручную добавить «и» перед последним элементом.

const fruits = ["apples", "oranges", "bananas"];
const lastFruit = fruits[fruits.length - 1];
const otherFruits = fruits.slice(0, -1);
const list = otherFruits.join(", ") + ", and " + lastFruit;
console.log(list);
// "apples, oranges, and bananas"

Этот код работает только для английского. Испанские пользователи увидят «apples, oranges, and bananas» вместо «apples, oranges y bananas». Французские — увидят «and» вместо «et». Правила пунктуации и союзов различаются в зависимости от языка.

Использование Intl.ListFormat для форматирования списков

Конструктор Intl.ListFormat создаёт форматтер, который преобразует массивы в строки списков с учётом локали.

const formatter = new Intl.ListFormat("en");
const fruits = ["apples", "oranges", "bananas"];
console.log(formatter.format(fruits));
// "apples, oranges, and bananas"

Форматтер использует правильные разделители и союзы для указанной локали. Локаль передаётся первым аргументом в конструктор.

const enFormatter = new Intl.ListFormat("en");
const esFormatter = new Intl.ListFormat("es");
const frFormatter = new Intl.ListFormat("fr");

const fruits = ["apples", "oranges", "bananas"];

console.log(enFormatter.format(fruits));
// "apples, oranges, and bananas"

console.log(esFormatter.format(fruits));
// "apples, oranges y bananas"

console.log(frFormatter.format(fruits));
// "apples, oranges et bananas"

Форматтер автоматически применяет правила пунктуации и союзов для каждой локали.

Форматирование списков с помощью «и»

Стандартное поведение Intl.ListFormat использует союз «и» или его эквивалент на других языках. Это называется конъюнктивным форматированием.

const formatter = new Intl.ListFormat("en", { type: "conjunction" });
const items = ["bread", "milk", "eggs"];
console.log(formatter.format(items));
// "bread, milk, and eggs"

Опция type определяет, какой соединитель будет использоваться между элементами. Значение "conjunction" формирует списки с использованием «и». Это значение по умолчанию, поэтому его можно не указывать.

Понимание опций типа списка

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

Тип "conjunction" использует «и» или его эквивалент.

const formatter = new Intl.ListFormat("en", { type: "conjunction" });
console.log(formatter.format(["red", "green", "blue"]));
// "red, green, and blue"

Тип "disjunction" использует «или» или его эквивалент.

const formatter = new Intl.ListFormat("en", { type: "disjunction" });
console.log(formatter.format(["red", "green", "blue"]));
// "red, green, or blue"

Тип "unit" форматирует списки измерений или количеств без союза.

const formatter = new Intl.ListFormat("en", { type: "unit" });
console.log(formatter.format(["5 pounds", "12 ounces"]));
// "5 pounds, 12 ounces"

Тип unit использует минимальную пунктуацию, подходящую для технических или измерительных данных.

Понимание опций стиля

Опция style определяет длину и формальность форматированного вывода. Она принимает три значения.

Стиль "long" использует полные слова и стандартную пунктуацию. Это значение по умолчанию.

const formatter = new Intl.ListFormat("en", { style: "long" });
console.log(formatter.format(["Alice", "Bob", "Carol"]));
// "Alice, Bob, and Carol"

Стиль "short" использует сокращённые формы, если они доступны.

const formatter = new Intl.ListFormat("en", { style: "short" });
console.log(formatter.format(["Alice", "Bob", "Carol"]));
// "Alice, Bob, & Carol"

Стиль "narrow" использует максимально компактную форму.

const formatter = new Intl.ListFormat("en", { style: "narrow" });
console.log(formatter.format(["Alice", "Bob", "Carol"]));
// "Alice, Bob, Carol"

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

Как разные локали форматируют списки

В каждой локали свои правила форматирования списков. Форматтер применяет их автоматически.

В английском используются запятые, включая оксфордскую запятую перед «и».

const formatter = new Intl.ListFormat("en");
console.log(formatter.format(["coffee", "tea", "juice"]));
// "coffee, tea, and juice"

В испанском используются запятые и союз «y».

const formatter = new Intl.ListFormat("es");
console.log(formatter.format(["café", "té", "jugo"]));
// "café, té y jugo"

Во французском используются запятые и союз «et».

const formatter = new Intl.ListFormat("fr");
console.log(formatter.format(["café", "thé", "jus"]));
// "café, thé et jus"

В китайском для «и» используется символ 和, а в качестве разделителя — перечислительная запятая 、.

const formatter = new Intl.ListFormat("zh");
console.log(formatter.format(["咖啡", "茶", "可乐"]));
// "咖啡、茶和可乐"

В немецком используются запятые и союз «und».

const formatter = new Intl.ListFormat("de");
console.log(formatter.format(["Kaffee", "Tee", "Saft"]));
// "Kaffee, Tee und Saft"

Форматтер учитывает эти различия, и вам не нужно знать правила для каждого языка.

Получение отдельных частей с помощью formatToParts

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

const formatter = new Intl.ListFormat("en");
const parts = formatter.formatToParts(["red", "green", "blue"]);
console.log(parts);

Результат — это массив объектов со свойствами type и value.

[
  { type: "element", value: "red" },
  { type: "literal", value: ", " },
  { type: "element", value: "green" },
  { type: "literal", value: ", and " },
  { type: "element", value: "blue" }
]

Каждый элемент списка имеет тип "element". Разделители и союзы имеют тип "literal". Это можно использовать для кастомного оформления.

const formatter = new Intl.ListFormat("en");
const parts = formatter.formatToParts(["red", "green", "blue"]);

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

console.log(html);
// "<strong>red</strong>, <strong>green</strong>, and <strong>blue</strong>"

Такой подход даёт полный контроль над форматированием и сохраняет корректные для языка разделители и союзы.

Поддержка браузерами

API Intl.ListFormat доступен во всех современных браузерах. С апреля 2021 года его поддерживают основные браузеры: Chrome, Firefox, Safari и Edge.

Перед использованием можно проверить, доступен ли этот API.

if (typeof Intl.ListFormat !== "undefined") {
  const formatter = new Intl.ListFormat("en");
  console.log(formatter.format(["a", "b", "c"]));
} else {
  console.log("Intl.ListFormat is not supported");
}

Для старых браузеров потребуется fallback или polyfill. В качестве fallback можно использовать простой метод join().

function formatList(items, locale) {
  if (typeof Intl.ListFormat !== "undefined") {
    const formatter = new Intl.ListFormat(locale);
    return formatter.format(items);
  }
  return items.join(", ");
}

console.log(formatList(["red", "green", "blue"], "en"));
// "red, green, and blue" (or "red, green, blue" in older browsers)

Это гарантирует работу кода даже в браузерах без поддержки Intl.ListFormat.