Как форматировать массивы с учётом локали в JavaScript?

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

Введение

Когда вы преобразуете массив в читаемую строку, элементы нужно разделять запятыми или другими знаками препинания. В разных языках используются разные разделители. В английском — запятая и пробел, в японском — перечислительная запятая 、, а в арабском — свои знаки препинания и порядок слов.

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

Почему разделители в массивах зависят от локали

Можно подумать, что во всех языках для разделения элементов списка используется запятая. Это не так.

В английском элементы разделяются запятой и пробелом.

// English: "red, green, blue"

В японском используется перечислительная запятая 、 без пробелов.

// Japanese: "赤、緑、青"

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

// Chinese: "红、绿、蓝"

В арабском используется другая запятая ، и текст читается справа налево.

// Arabic: "أحمر، أخضر، أزرق"

Эти различия обрабатываются автоматически в Intl.ListFormat. Вам не нужно знать правила пунктуации для каждого языка.

Проблема с join()

Метод Array.prototype.join() преобразует массивы в строки, используя указанный вами разделитель.

const colors = ["red", "green", "blue"];
console.log(colors.join(", "));
// "red, green, blue"

Это жёстко задаёт английскую пунктуацию. Разделитель «запятая и пробел» не подходит для других языков.

const colors = ["赤", "緑", "青"];
console.log(colors.join(", "));
// "赤, 緑, 青" (wrong - should use 、 instead of ,)

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

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

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

const formatter = new Intl.ListFormat("en");
const colors = ["red", "green", "blue"];
console.log(formatter.format(colors));
// "red, green, and blue"

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

const enFormatter = new Intl.ListFormat("en");
const jaFormatter = new Intl.ListFormat("ja");
const arFormatter = new Intl.ListFormat("ar");

const colors = ["red", "green", "blue"];

console.log(enFormatter.format(colors));
// "red, green, and blue"

console.log(jaFormatter.format(["赤", "緑", "青"]));
// "赤、緑、青"

console.log(arFormatter.format(["أحمر", "أخضر", "أزرق"]));
// "أحمر، أخضر، أزرق"

Браузер предоставляет правила пунктуации. Вам не нужно поддерживать отдельный код для каждой локали.

Как меняются разделители в разных языках

Форматтер использует разные разделители в зависимости от локали. Эти примеры показывают, как один и тот же массив форматируется по-разному.

В английском используется запятая, пробел и слово «and».

const formatter = new Intl.ListFormat("en");
console.log(formatter.format(["apple", "orange", "banana"]));
// "apple, orange, and banana"

В испанском используется запятая, пробел и слово «y».

const formatter = new Intl.ListFormat("es");
console.log(formatter.format(["manzana", "naranja", "plátano"]));
// "manzana, naranja y plátano"

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

const formatter = new Intl.ListFormat("fr");
console.log(formatter.format(["pomme", "orange", "banane"]));
// "pomme, orange et banane"

В немецком используется запятая, пробел и слово «und».

const formatter = new Intl.ListFormat("de");
console.log(formatter.format(["Apfel", "Orange", "Banane"]));
// "Apfel, Orange und Banane"

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

const formatter = new Intl.ListFormat("ja");
console.log(formatter.format(["りんご", "オレンジ", "バナナ"]));
// "りんご、オレンジ、バナナ"

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

const formatter = new Intl.ListFormat("zh");
console.log(formatter.format(["苹果", "橙子", "香蕉"]));
// "苹果、橙子和香蕉"

В корейском используется запятая и частица 및.

const formatter = new Intl.ListFormat("ko");
console.log(formatter.format(["사과", "오렌지", "바나나"]));
// "사과, 오렌지 및 바나나"

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

Использование локали пользователя

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

const userLocale = navigator.language;
const formatter = new Intl.ListFormat(userLocale);
const items = ["first", "second", "third"];
console.log(formatter.format(items));

Это гарантирует, что список использует разделители, соответствующие ожиданиям пользователя. Пользователь с французскими настройками браузера увидит французскую пунктуацию. Пользователь с японскими настройками — японскую пунктуацию.

Форматирование массивов без союзов

По умолчанию Intl.ListFormat добавляет союз, например «и», перед последним элементом. Это можно отключить, используя тип unit.

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

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

const enFormatter = new Intl.ListFormat("en", { type: "unit" });
const jaFormatter = new Intl.ListFormat("ja", { type: "unit" });

console.log(enFormatter.format(["Item A", "Item B", "Item C"]));
// "Item A, Item B, Item C"

console.log(jaFormatter.format(["項目A", "項目B", "項目C"]));
// "項目A、項目B、項目C"

Даже без союзов пунктуация-разделитель всё равно соответствует правилам локали.

Создание переиспользуемых форматтеров

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

const formatter = new Intl.ListFormat("en");

console.log(formatter.format(["red", "green"]));
// "red and green"

console.log(formatter.format(["a", "b", "c", "d"]));
// "a, b, c, and d"

console.log(formatter.format(["one"]));
// "one"

Один и тот же форматтер работает с массивами любой длины. Он применяет правильные разделители и союзы в зависимости от количества элементов в массиве.

Обработка пустых массивов

Если вы форматируете пустой массив, форматтер возвращает пустую строку.

const formatter = new Intl.ListFormat("en");
console.log(formatter.format([]));
// ""

Если вам нужно другое поведение, проверьте массив на пустоту до форматирования.

function formatList(items, locale) {
  if (items.length === 0) {
    return "No items";
  }
  const formatter = new Intl.ListFormat(locale);
  return formatter.format(items);
}

console.log(formatList([], "en"));
// "No items"

console.log(formatList(["apple"], "en"));
// "apple"

Это даёт вам контроль над тем, как пустые массивы будут отображаться пользователям.

Поддержка браузеров

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");
}

Для старых браузеров можно использовать метод 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)

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