Как форматировать списки с "или" в JavaScript
Используйте Intl.ListFormat с типом disjunction для правильного форматирования альтернатив на любом языке
Введение
Приложения часто предоставляют пользователям выбор или альтернативы. Компонент загрузки файлов принимает файлы форматов "PNG, JPEG или SVG". Форма оплаты позволяет использовать "кредитную карту, дебетовую карту или PayPal" в качестве способов оплаты. Сообщение об ошибке предлагает исправить "имя пользователя, пароль или адрес электронной почты", чтобы устранить сбои аутентификации.
Эти списки используют "или" для указания альтернатив. Ручное форматирование таких списков с помощью конкатенации строк не работает в других языках, так как в разных языках существуют разные правила пунктуации, разные слова для "или" и разные правила расстановки запятых. API Intl.ListFormat с типом disjunction правильно форматирует такие альтернативные списки для любого языка.
Что такое дизъюнктивные списки
Дизъюнктивный список представляет собой альтернативы, где обычно применяется один из вариантов. Слово "дизъюнкция" означает разделение или альтернативы. В английском языке дизъюнктивные списки используют "or" в качестве союза:
const paymentMethods = ["credit card", "debit card", "PayPal"];
// Желаемый результат: "credit card, debit card, or PayPal"
Это отличается от конъюнктивных списков, которые используют "and" для указания, что все элементы применяются вместе. Дизъюнктивные списки передают выбор, конъюнктивные списки передают комбинацию.
Обычные контексты для дизъюнктивных списков включают варианты оплаты, ограничения форматов файлов, предложения по устранению неполадок, альтернативы фильтров поиска и любой интерфейс, где пользователи выбирают один вариант из нескольких возможных.
Почему ручное форматирование не работает
Англоговорящие пишут дизъюнктивные списки как "A, B, or C" с запятыми между элементами и "or" перед последним элементом. Этот шаблон не работает в других языках:
// Жестко заданный английский шаблон
const items = ["apple", "orange", "banana"];
const text = items.slice(0, -1).join(", ") + ", or " + items[items.length - 1];
// "apple, orange, or banana"
Этот код выдает некорректный результат на испанском, французском, немецком и большинстве других языков. У каждого языка свои правила форматирования дизъюнктивных списков.
В испанском используется "o" без запятой перед ним:
Ожидается: "manzana, naranja o plátano"
Английский шаблон выдает: "manzana, naranja, or plátano"
Во французском используется "ou" без запятой перед ним:
Ожидается: "pomme, orange ou banane"
Английский шаблон выдает: "pomme, orange, or banane"
В немецком используется "oder" без запятой перед ним:
Ожидается: "Apfel, Orange oder Banane"
Английский шаблон выдает: "Apfel, Orange, or Banane"
В японском используется частица "か" (ka) с другой пунктуацией:
Ожидается: "りんご、オレンジ、またはバナナ"
Английский шаблон выдает: "りんご、オレンジ、 or バナナ"
Эти различия выходят за рамки простой замены слов. Расстановка пунктуации, правила пробелов и грамматические частицы различаются в каждом языке. Ручная конкатенация строк не может справиться с этой сложностью.
Использование Intl.ListFormat с типом disjunction
API Intl.ListFormat форматирует списки в соответствии с языковыми правилами. Установите опцию type в значение "disjunction", чтобы форматировать альтернативные списки:
const formatter = new Intl.ListFormat("en", { type: "disjunction" });
const paymentMethods = ["credit card", "debit card", "PayPal"];
console.log(formatter.format(paymentMethods));
// "credit card, debit card, or PayPal"
Форматировщик обрабатывает массивы любой длины:
const formatter = new Intl.ListFormat("en", { type: "disjunction" });
console.log(formatter.format([]));
// ""
console.log(formatter.format(["credit card"]));
// "credit card"
console.log(formatter.format(["credit card", "PayPal"]));
// "credit card or PayPal"
console.log(formatter.format(["credit card", "debit card", "PayPal"]));
// "credit card, debit card, or PayPal"
API автоматически применяет правильную пунктуацию и союзы для каждого случая.
Понимание стилей disjunction
Опция style управляет степенью подробности форматирования. Существует три стиля: long, short и narrow. По умолчанию используется стиль long.
const items = ["email", "phone", "SMS"];
const long = new Intl.ListFormat("en", {
type: "disjunction",
style: "long"
});
console.log(long.format(items));
// "email, phone, or SMS"
const short = new Intl.ListFormat("en", {
type: "disjunction",
style: "short"
});
console.log(short.format(items));
// "email, phone, or SMS"
const narrow = new Intl.ListFormat("en", {
type: "disjunction",
style: "narrow"
});
console.log(narrow.format(items));
// "email, phone, or SMS"
На английском языке все три стиля дают одинаковый результат для дизъюнктивных списков. В других языках различия более заметны. Например, в немецком языке используется "oder" в длинном стиле, а в узком стиле могут быть сокращения. Различия становятся более очевидными в языках с несколькими уровнями формальности или более длинными союзами.
Узкий стиль обычно убирает пробелы или использует более короткие союзы, чтобы сэкономить место в ограниченных макетах. Используйте длинный стиль для стандартного текста, короткий стиль для умеренно компактных отображений и узкий стиль для ограниченных пространств, таких как мобильные интерфейсы или компактные таблицы.
Как выглядят разделительные списки в разных языках
Каждый язык форматирует разделительные списки в соответствии со своими правилами. Intl.ListFormat автоматически обрабатывает эти различия.
Английский использует запятые с "or":
const en = new Intl.ListFormat("en", { type: "disjunction" });
console.log(en.format(["PNG", "JPEG", "SVG"]));
// "PNG, JPEG, or SVG"
Испанский использует запятые с "o" и без запятой перед последним союзом:
const es = new Intl.ListFormat("es", { type: "disjunction" });
console.log(es.format(["PNG", "JPEG", "SVG"]));
// "PNG, JPEG o SVG"
Французский использует запятые с "ou" и без запятой перед последним союзом:
const fr = new Intl.ListFormat("fr", { type: "disjunction" });
console.log(fr.format(["PNG", "JPEG", "SVG"]));
// "PNG, JPEG ou SVG"
Немецкий использует запятые с "oder" и без запятой перед последним союзом:
const de = new Intl.ListFormat("de", { type: "disjunction" });
console.log(de.format(["PNG", "JPEG", "SVG"]));
// "PNG, JPEG oder SVG"
Японский использует другую пунктуацию и частицы:
const ja = new Intl.ListFormat("ja", { type: "disjunction" });
console.log(ja.format(["PNG", "JPEG", "SVG"]));
// "PNG、JPEG、またはSVG"
Китайский использует китайские знаки препинания:
const zh = new Intl.ListFormat("zh", { type: "disjunction" });
console.log(zh.format(["PNG", "JPEG", "SVG"]));
// "PNG、JPEG或SVG"
Эти примеры показывают, как API адаптируется к грамматическим и пунктуационным правилам каждого языка. Один и тот же код работает на всех языках, если указать соответствующую локаль.
Форматирование вариантов оплаты
Формы оплаты предлагают несколько вариантов выбора метода оплаты. Форматируйте их с помощью разделительных списков:
const formatter = new Intl.ListFormat("en", { type: "disjunction" });
function getPaymentMessage(methods) {
if (methods.length === 0) {
return "Нет доступных методов оплаты";
}
return `Оплатить с помощью ${formatter.format(methods)}.`;
}
const methods = ["кредитная карта", "дебетовая карта", "PayPal", "Apple Pay"];
console.log(getPaymentMessage(methods));
// "Оплатить с помощью кредитная карта, дебетовая карта, PayPal или Apple Pay."
Для международных приложений передавайте локаль пользователя:
const userLocale = navigator.language; // например, "fr-FR"
const formatter = new Intl.ListFormat(userLocale, { type: "disjunction" });
function getPaymentMessage(methods) {
if (methods.length === 0) {
return "Нет доступных методов оплаты";
}
return `Оплатить с помощью ${formatter.format(methods)}.`;
}
Этот подход работает в процессах оформления заказа, селекторах методов оплаты и любом интерфейсе, где пользователи выбирают способ оплаты.
Ограничения на загрузку файлов
Компоненты загрузки файлов указывают, какие типы файлов принимает система:
const formatter = new Intl.ListFormat("ru", { type: "disjunction" });
function getAcceptedFormatsMessage(formats) {
if (formats.length === 0) {
return "Форматы файлов не принимаются";
}
if (formats.length === 1) {
return `Принимаемый формат: ${formats[0]}`;
}
return `Принимаемые форматы: ${formatter.format(formats)}`;
}
const imageFormats = ["PNG", "JPEG", "SVG", "WebP"];
console.log(getAcceptedFormatsMessage(imageFormats));
// "Принимаемые форматы: PNG, JPEG, SVG или WebP"
const documentFormats = ["PDF", "DOCX"];
console.log(getAcceptedFormatsMessage(documentFormats));
// "Принимаемые форматы: PDF или DOCX"
Этот шаблон подходит для загрузки изображений, отправки документов и любого ввода файлов с ограничениями по формату.
Предложения по устранению неполадок форматирования
Сообщения об ошибках часто предлагают несколько способов решения проблемы. Представляйте эти предложения в виде дизъюнктивных списков:
const formatter = new Intl.ListFormat("ru", { type: "disjunction" });
function getAuthenticationError(missingFields) {
if (missingFields.length === 0) {
return "Аутентификация не удалась";
}
return `Пожалуйста, проверьте ${formatter.format(missingFields)} и попробуйте снова.`;
}
console.log(getAuthenticationError(["имя пользователя", "пароль"]));
// "Пожалуйста, проверьте имя пользователя или пароль и попробуйте снова."
console.log(getAuthenticationError(["электронную почту", "имя пользователя", "пароль"]));
// "Пожалуйста, проверьте электронную почту, имя пользователя или пароль и попробуйте снова."
Дизъюнктивный список уточняет, что пользователям нужно исправить любой из упомянутых полей, а не обязательно все сразу.
Альтернативы фильтров поиска
Интерфейсы поиска показывают активные фильтры. Когда фильтры представляют альтернативы, используйте дизъюнктивные списки:
const formatter = new Intl.ListFormat("ru", { type: "disjunction" });
function getFilterSummary(filters) {
if (filters.length === 0) {
return "Фильтры не применены";
}
if (filters.length === 1) {
return `Показаны результаты для: ${filters[0]}`;
}
return `Показаны результаты для: ${formatter.format(filters)}`;
}
const categories = ["Электроника", "Книги", "Одежда"];
console.log(getFilterSummary(categories));
// "Показаны результаты для: Электроника, Книги или Одежда"
Это подходит для фильтров категорий, выбора тегов и любого интерфейса фильтров, где выбранные значения представляют альтернативы, а не комбинации.
Повторное использование форматтеров для повышения производительности
Создание экземпляров Intl.ListFormat имеет накладные расходы. Создавайте форматтеры один раз и переиспользуйте их:
// Создайте один раз на уровне модуля
const disjunctionFormatter = new Intl.ListFormat("en", { type: "disjunction" });
// Переиспользуйте в нескольких функциях
function formatPaymentMethods(methods) {
return disjunctionFormatter.format(methods);
}
function formatFileTypes(types) {
return disjunctionFormatter.format(types);
}
function formatErrorSuggestions(suggestions) {
return disjunctionFormatter.format(suggestions);
}
Для приложений, поддерживающих несколько локалей, храните форматтеры в кэше:
const formatters = new Map();
function getDisjunctionFormatter(locale) {
if (!formatters.has(locale)) {
formatters.set(
locale,
new Intl.ListFormat(locale, { type: "disjunction" })
);
}
return formatters.get(locale);
}
const formatter = getDisjunctionFormatter("en");
console.log(formatter.format(["A", "B", "C"]));
// "A, B, or C"
Этот подход снижает затраты на инициализацию, поддерживая несколько локалей в приложении.
Использование formatToParts для пользовательского рендеринга
Метод formatToParts() возвращает массив объектов, представляющих каждую часть форматированного списка. Это позволяет применять пользовательское оформление:
const formatter = new Intl.ListFormat("en", { type: "disjunction" });
const parts = formatter.formatToParts(["PNG", "JPEG", "SVG"]);
console.log(parts);
// [
// { type: "element", value: "PNG" },
// { type: "literal", value: ", " },
// { type: "element", value: "JPEG" },
// { type: "literal", value: ", or " },
// { type: "element", value: "SVG" }
// ]
Каждая часть имеет type и value. type может быть либо "element" для элементов списка, либо "literal" для пунктуации и союзов.
Используйте это, чтобы применять разные стили к элементам и литералам:
const formatter = new Intl.ListFormat("en", { type: "disjunction" });
const formats = ["PNG", "JPEG", "SVG"];
const html = formatter.formatToParts(formats)
.map(part => {
if (part.type === "element") {
return `<code>${part.value}</code>`;
}
return part.value;
})
.join("");
console.log(html);
// "<code>PNG</code>, <code>JPEG</code>, or <code>SVG</code>"
Этот подход сохраняет правильную пунктуацию и союзы для локали, одновременно применяя пользовательское оформление к самим элементам.
Поддержка браузеров и совместимость
Intl.ListFormat работает во всех современных браузерах с апреля 2021 года. Поддержка включает Chrome 72+, Firefox 78+, Safari 14.1+ и Edge 79+.
Проверьте поддержку перед использованием API:
if (typeof Intl.ListFormat !== "undefined") {
const formatter = new Intl.ListFormat("en", { type: "disjunction" });
return formatter.format(items);
} else {
// Альтернатива для старых браузеров
return items.join(", ");
}
Для более широкой совместимости используйте полифил, например, @formatjs/intl-listformat. Устанавливайте его только там, где это необходимо:
if (typeof Intl.ListFormat === "undefined") {
await import("@formatjs/intl-listformat/polyfill");
}
const formatter = new Intl.ListFormat("en", { type: "disjunction" });
С учетом текущей поддержки браузеров большинство приложений могут использовать Intl.ListFormat напрямую без полифилов.
Распространенные ошибки, которых следует избегать
Использование типа conjunction вместо disjunction приводит к неправильному значению:
// Неправильно: предполагается, что требуются все методы
const wrong = new Intl.ListFormat("en", { type: "conjunction" });
console.log(`Pay with ${wrong.format(["credit card", "debit card"])}`);
// "Pay with credit card and debit card"
// Правильно: предполагается выбор одного метода
const correct = new Intl.ListFormat("en", { type: "disjunction" });
console.log(`Pay with ${correct.format(["credit card", "debit card"])}`);
// "Pay with credit card or debit card"
Создание новых форматтеров многократно расходует ресурсы:
// Неэффективно
function formatOptions(options) {
return new Intl.ListFormat("en", { type: "disjunction" }).format(options);
}
// Эффективно
const formatter = new Intl.ListFormat("en", { type: "disjunction" });
function formatOptions(options) {
return formatter.format(options);
}
Жесткое кодирование "or" в строках препятствует локализации:
// Не работает в других языках
const text = items.join(", ") + ", or other options";
// Работает во всех языках
const formatter = new Intl.ListFormat(userLocale, { type: "disjunction" });
const allItems = [...items, "other options"];
const text = formatter.format(allItems);
Необработка пустых массивов может привести к неожиданным результатам:
// Защищенный вариант
function formatPaymentMethods(methods) {
if (methods.length === 0) {
return "Нет доступных способов оплаты";
}
return formatter.format(methods);
}
Хотя format([]) возвращает пустую строку, явная обработка пустого состояния улучшает пользовательский опыт.
Когда использовать дизъюнктивные списки
Используйте дизъюнктивные списки при представлении альтернатив или вариантов, где обычно применяется один из них. Это включает выбор способа оплаты, ограничения форматов файлов, предложения по исправлению ошибок аутентификации, параметры фильтрации поиска и выбор типа учетной записи.
Не используйте дизъюнктивные списки, если все элементы должны применяться вместе. Вместо этого используйте конъюнктивные списки. Например, "Имя, электронная почта и пароль обязательны" использует конъюнкцию, потому что все поля должны быть заполнены, а не только одно.
Не используйте дизъюнктивные списки для нейтральных перечислений без намека на выбор. Измерения и технические спецификации обычно используют списки единиц вместо дизъюнкции или конъюнкции.
API заменяет ручные шаблоны конкатенации строк для альтернатив. Каждый раз, когда вы пишете код, который объединяет элементы с помощью "или" для текста, ориентированного на пользователя, подумайте, обеспечивает ли Intl.ListFormat с типом disjunction лучшую поддержку локалей.