API Intl.ListFormat
Форматирование массивов в читаемые списки с учетом локали
Введение
При отображении нескольких элементов пользователям разработчики часто объединяют массивы с помощью запятых и добавляют "и" перед последним элементом:
const users = ["Alice", "Bob", "Charlie"];
const message = users.slice(0, -1).join(", ") + ", и " + users[users.length - 1];
// "Alice, Bob, и Charlie"
Этот подход жестко задает правила пунктуации английского языка и не работает в других языках. В японском используются другие частицы, в немецком — другие правила расстановки пробелов, а в китайском — другие разделители. API Intl.ListFormat решает эту проблему, форматируя списки в соответствии с правилами каждого языка.
Что делает Intl.ListFormat
Intl.ListFormat преобразует массивы в читаемые списки, которые соответствуют грамматическим и пунктуационным правилам любого языка. Он обрабатывает три типа списков, которые встречаются во всех языках:
- Списки соединений используют "и" для соединения элементов ("A, B и C")
- Списки разделений используют "или" для представления альтернатив ("A, B или C")
- Списки единиц форматируют измерения без соединений ("5 футов, 2 дюйма")
API знает, как каждый язык форматирует эти типы списков, от пунктуации до выбора слов и расстановки пробелов.
Основное использование
Создайте форматтер с указанием локали и параметров, затем вызовите format() с массивом:
const formatter = new Intl.ListFormat("ru", {
type: "conjunction",
style: "long"
});
const items = ["хлеб", "молоко", "яйца"];
console.log(formatter.format(items));
// "хлеб, молоко и яйца"
Форматтер обрабатывает массивы любой длины, включая крайние случаи:
formatter.format([]); // ""
formatter.format(["хлеб"]); // "хлеб"
formatter.format(["хлеб", "молоко"]); // "хлеб и молоко"
Типы списков управляют соединениями
Опция type определяет, какое соединение будет использоваться в отформатированном списке.
Списки с союзами
Используйте type: "conjunction" для списков, где все элементы применяются вместе. Это тип по умолчанию:
const formatter = new Intl.ListFormat("en", { type: "conjunction" });
console.log(formatter.format(["HTML", "CSS", "JavaScript"]));
// "HTML, CSS и JavaScript"
Общие случаи использования включают отображение выбранных элементов, перечисление функций и показ нескольких значений, которые применяются одновременно.
Списки с разделительными союзами
Используйте type: "disjunction" для списков, представляющих альтернативы или варианты выбора:
const formatter = new Intl.ListFormat("en", { type: "disjunction" });
console.log(formatter.format(["credit card", "debit card", "PayPal"]));
// "credit card, debit card или PayPal"
Это используется в списках опций, сообщениях об ошибках с несколькими решениями и в любом контексте, где пользователи выбирают один элемент.
Списки единиц измерения
Используйте type: "unit" для измерений и технических значений, которые должны отображаться без союзов:
const formatter = new Intl.ListFormat("en", { type: "unit" });
console.log(formatter.format(["5 feet", "2 inches"]));
// "5 feet, 2 inches"
Списки единиц измерения подходят для измерений, технических спецификаций и составных значений.
Стили списков контролируют степень подробности
Опция style регулирует, насколько подробно будет отображаться форматирование. Существует три стиля: long, short и narrow.
const items = ["Monday", "Wednesday", "Friday"];
const long = new Intl.ListFormat("en", { style: "long" });
console.log(long.format(items));
// "Monday, Wednesday и Friday"
const short = new Intl.ListFormat("en", { style: "short" });
console.log(short.format(items));
// "Monday, Wednesday и Friday"
const narrow = new Intl.ListFormat("en", { style: "narrow" });
console.log(narrow.format(items));
// "Monday, Wednesday, Friday"
В английском языке long и short дают одинаковый результат для большинства списков. Стиль narrow опускает союз. В других языках различия между стилями, особенно для списков с разделительными союзами, более заметны.
Как разные языки форматируют списки
У каждого языка есть свои правила форматирования списков. Intl.ListFormat автоматически учитывает эти различия.
Английский использует запятые, пробелы и союзы:
const en = new Intl.ListFormat("en");
console.log(en.format(["Tokyo", "Paris", "London"]));
// "Tokyo, Paris, and London"
Немецкий использует ту же структуру с запятыми, но другие союзы:
const de = new Intl.ListFormat("de");
console.log(de.format(["Tokyo", "Paris", "London"]));
// "Tokyo, Paris und London"
Японский использует другие разделители и частицы:
const ja = new Intl.ListFormat("ja");
console.log(ja.format(["東京", "パリ", "ロンドン"]));
// "東京、パリ、ロンドン"
Китайский использует совершенно другую пунктуацию:
const zh = new Intl.ListFormat("zh");
console.log(zh.format(["东京", "巴黎", "伦敦"]));
// "东京、巴黎和伦敦"
Эти различия касаются не только пунктуации, но и правил расстановки пробелов, использования союзов и грамматических частиц. Жёсткое кодирование одного подхода нарушает работу для других языков.
Использование formatToParts для кастомного рендеринга
Метод formatToParts() возвращает массив объектов вместо строки. Каждый объект представляет собой одну часть форматированного списка:
const formatter = new Intl.ListFormat("en");
const parts = formatter.formatToParts(["red", "green", "blue"]);
console.log(parts);
// [
// { type: "element", value: "red" },
// { type: "literal", value: ", " },
// { type: "element", value: "green" },
// { type: "literal", value: ", and " },
// { type: "element", value: "blue" }
// ]
Каждая часть имеет type и value. type может быть либо "element" для элементов списка, либо "literal" для пунктуации и союзов.
Эта структура позволяет кастомизировать рендеринг, где элементы и литералы требуют разного стиля:
const formatter = new Intl.ListFormat("en");
const items = ["Alice", "Bob", "Charlie"];
const html = formatter.formatToParts(items)
.map(part => {
if (part.type === "element") {
return `<strong>${part.value}</strong>`;
}
return part.value;
})
.join("");
console.log(html);
// "<strong>Alice</strong>, <strong>Bob</strong>, and <strong>Charlie</strong>"
Этот подход сохраняет правильную пунктуацию для локали, одновременно позволяя применять кастомное оформление к элементам списка.
Повторное использование форматтеров для повышения производительности
Создание экземпляров Intl.ListFormat имеет накладные расходы. Создавайте форматтеры один раз и используйте их повторно:
// Создайте один раз
const listFormatter = new Intl.ListFormat("en", { type: "conjunction" });
// Используйте многократно
function displayUsers(users) {
return listFormatter.format(users.map(u => u.name));
}
function displayTags(tags) {
return listFormatter.format(tags);
}
Для приложений с несколькими локалями храните форматтеры в карте:
const formatters = new Map();
function getListFormatter(locale, options) {
const key = `${locale}-${options.type}-${options.style}`;
if (!formatters.has(key)) {
formatters.set(key, new Intl.ListFormat(locale, options));
}
return formatters.get(key);
}
const formatter = getListFormatter("en", { type: "conjunction", style: "long" });
console.log(formatter.format(["a", "b", "c"]));
Этот подход снижает затраты на повторную инициализацию, поддерживая несколько локалей и конфигураций.
Форматирование сообщений об ошибках
Проверка формы часто генерирует несколько ошибок. Форматируйте их с помощью списков дизъюнкции, чтобы представить варианты:
const formatter = new Intl.ListFormat("en", { type: "disjunction" });
function validatePassword(password) {
const errors = [];
if (password.length < 8) {
errors.push("не менее 8 символов");
}
if (!/[A-Z]/.test(password)) {
errors.push("заглавную букву");
}
if (!/[0-9]/.test(password)) {
errors.push("цифру");
}
if (errors.length > 0) {
return `Пароль должен содержать ${formatter.format(errors)}.`;
}
return null;
}
console.log(validatePassword("weak"));
// "Пароль должен содержать не менее 8 символов, заглавную букву или цифру."
Список дизъюнкции уточняет, что пользователям нужно исправить любую из этих проблем, а форматирование адаптируется к конвенциям каждой локали.
Отображение выбранных элементов
Когда пользователи выбирают несколько элементов, форматируйте выбор с помощью списков соединений:
const formatter = new Intl.ListFormat("en", { type: "conjunction" });
function getSelectionMessage(selectedFiles) {
if (selectedFiles.length === 0) {
return "Файлы не выбраны";
}
if (selectedFiles.length === 1) {
return `${selectedFiles[0]} выбран`;
}
return `${formatter.format(selectedFiles)} выбраны`;
}
console.log(getSelectionMessage(["report.pdf", "data.csv", "notes.txt"]));
// "report.pdf, data.csv и notes.txt выбраны"
Этот подход подходит для выбора файлов, фильтров, категорий и любого интерфейса с множественным выбором.
Обработка длинных списков
Для списков с большим количеством элементов рассмотрите возможность их сокращения перед форматированием:
const formatter = new Intl.ListFormat("ru", { type: "conjunction" });
function formatUserList(users) {
if (users.length <= 3) {
return formatter.format(users);
}
const visible = users.slice(0, 2);
const remaining = users.length - 2;
return `${formatter.format(visible)}, и еще ${remaining}`;
}
console.log(formatUserList(["Алиса", "Боб", "Чарли", "Дэвид", "Ева"]));
// "Алиса, Боб, и еще 3"
Это сохраняет читаемость, указывая общее количество. Точный порог зависит от ограничений вашего интерфейса.
Поддержка браузеров и резервные варианты
Intl.ListFormat работает во всех современных браузерах с апреля 2021 года. Поддержка включает Chrome 72+, Firefox 78+, Safari 14.1+ и Edge 79+.
Проверьте поддержку с помощью обнаружения функций:
if (typeof Intl.ListFormat !== "undefined") {
const formatter = new Intl.ListFormat("ru");
return formatter.format(items);
} else {
// Резервный вариант для старых браузеров
return items.join(", ");
}
Для более широкой совместимости используйте полифил, например, @formatjs/intl-listformat. Устанавливайте его только для сред, где это необходимо:
if (typeof Intl.ListFormat === "undefined") {
await import("@formatjs/intl-listformat/polyfill");
}
С учетом текущей поддержки браузеров большинство приложений могут использовать Intl.ListFormat напрямую без полифилов.
Распространенные ошибки, которых следует избегать
Создание новых форматтеров повторно расходует ресурсы:
// Неэффективно
function display(items) {
return new Intl.ListFormat("ru").format(items);
}
// Эффективно
const formatter = new Intl.ListFormat("ru");
function display(items) {
return formatter.format(items);
}
Использование array.join() для текста, ориентированного на пользователя, создает проблемы с локализацией:
// Неправильно для других языков
const text = items.join(", ");
// Работает для всех языков
const formatter = new Intl.ListFormat(userLocale);
const text = formatter.format(items);
Предположение, что правила соединения на английском языке применимы повсеместно, приводит к некорректному выводу в других локалях. Всегда передавайте локаль пользователя в конструктор.
Необработка пустых массивов может вызвать неожиданный вывод:
// Защищенный вариант
function formatItems(items) {
if (items.length === 0) {
return "Нет элементов";
}
return formatter.format(items);
}
Хотя format([]) возвращает пустую строку, явная обработка пустого состояния улучшает пользовательский опыт.
Когда использовать Intl.ListFormat
Используйте Intl.ListFormat всякий раз, когда нужно отображать несколько элементов в тексте. Это включает в себя навигационные цепочки, выбранные фильтры, ошибки валидации, списки пользователей, теги категорий и списки функций.
Не используйте его для отображения структурированных данных, таких как таблицы или меню опций. Эти компоненты имеют свои собственные требования к форматированию, которые не связаны с правилами списков в тексте.
API заменяет ручное объединение строк и шаблоны соединения. Всякий раз, когда вы собираетесь использовать join(", ") для текста, ориентированного на пользователя, подумайте, обеспечивает ли Intl.ListFormat лучшую поддержку локалей.