如何在 JavaScript 中格式化带“或”的列表
使用 Intl.ListFormat 的 disjunction 类型,在任何语言中都能正确格式化备选项列表
引言
应用程序经常向用户展示多种选择或备选项。例如,文件上传组件支持上传“PNG、JPEG 或 SVG”文件;支付表单允许使用“信用卡、借记卡或 PayPal”作为支付方式;错误信息会建议修正“用户名、密码或邮箱地址”以解决认证失败。
这些列表使用“或”来表示备选项。如果手动用字符串拼接来格式化,在其他语言中会出现问题,因为不同语言有不同的标点规则、不同的“或”表达方式,以及不同的逗号使用习惯。使用 Intl.ListFormat API 的 disjunction 类型,可以在任何语言中正确格式化这些备选项列表。
什么是析取列表
析取列表用于展示多个备选项,通常只会选择其中一个。英文中的“disjunction”意为分离或备选。在英语中,析取列表用“or”作为连接词:
const paymentMethods = ["credit card", "debit card", "PayPal"];
// Desired output: "credit card, debit card, or PayPal"
这与用“and”表示所有项都适用的合取列表不同。析取列表表达选择,合取列表表达组合。
常见的析取列表场景包括支付方式、文件格式限制、故障排查建议、搜索筛选条件等任何需要用户从多个选项中选择一个的界面。
为什么手动格式化会失败
英语中,析取列表通常写作“A、B 或 C”,各项之间用逗号分隔,最后一项前用“or”。但这种格式在其他语言中并不适用:
// Hardcoded English pattern
const items = ["apple", "orange", "banana"];
const text = items.slice(0, -1).join(", ") + ", or " + items[items.length - 1];
// "apple, orange, or banana"
这段代码在西班牙语、法语、德语以及大多数其他语言中都会产生错误的输出。每种语言对析取列表的格式都有不同的规则。
西班牙语使用 "o",前面不加逗号:
Expected: "manzana, naranja o plátano"
English pattern produces: "manzana, naranja, or plátano"
法语使用 "ou",前面不加逗号:
Expected: "pomme, orange ou banane"
English pattern produces: "pomme, orange, or banane"
德语使用 "oder",前面不加逗号:
Expected: "Apfel, Orange oder Banane"
English pattern produces: "Apfel, Orange, or Banane"
日语使用助词 "か"(ka),标点用法不同:
Expected: "りんご、オレンジ、またはバナナ"
English pattern produces: "りんご、オレンジ、 or バナナ"
这些差异不仅仅是简单的词语替换。标点位置、空格规则和语法助词在不同语言中都有所不同。手动字符串拼接无法处理这种复杂性。
使用 Intl.ListFormat 的 disjunction 类型
Intl.ListFormat API 会根据语言规则格式化列表。将 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"
在英语中,三种样式下的析取列表输出相同。其他语言则有更多变化。例如德语在 long 样式下使用 "oder",而在 narrow 样式下可能会缩写。对于有多种正式程度或较长连词的语言,这些差异会更加明显。
narrow 样式通常会去除空格或使用更短的连词,以适应空间受限的布局。long 样式适用于标准文本,short 样式适用于中等紧凑的显示,narrow 样式适用于如移动端界面或紧凑表格等空间极为有限的场景。
不同语言中析取列表的表现
每种语言在格式化析取列表时都有其独特的规范。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 如何适应每种语言的语法和标点规范。只要传入合适的 locale,相同的代码可在所有语言中使用。
支付选项格式化
支付表单会展示多种支付方式。可用析取列表进行格式化:
const formatter = new Intl.ListFormat("en", { type: "disjunction" });
function getPaymentMessage(methods) {
if (methods.length === 0) {
return "No payment methods available";
}
return `Pay with ${formatter.format(methods)}.`;
}
const methods = ["credit card", "debit card", "PayPal", "Apple Pay"];
console.log(getPaymentMessage(methods));
// "Pay with credit card, debit card, PayPal, or Apple Pay."
对于国际化应用,请传入用户的 locale:
const userLocale = navigator.language; // e.g., "fr-FR"
const formatter = new Intl.ListFormat(userLocale, { type: "disjunction" });
function getPaymentMessage(methods) {
if (methods.length === 0) {
return "No payment methods available";
}
return `Pay with ${formatter.format(methods)}.`;
}
这种方式适用于结账流程、支付方式选择器以及任何让用户选择支付方式的界面。
文件上传限制格式化
文件上传组件会指定系统支持的文件类型:
const formatter = new Intl.ListFormat("en", { type: "disjunction" });
function getAcceptedFormatsMessage(formats) {
if (formats.length === 0) {
return "No file formats accepted";
}
if (formats.length === 1) {
return `Accepted format: ${formats[0]}`;
}
return `Accepted formats: ${formatter.format(formats)}`;
}
const imageFormats = ["PNG", "JPEG", "SVG", "WebP"];
console.log(getAcceptedFormatsMessage(imageFormats));
// "Accepted formats: PNG, JPEG, SVG, or WebP"
const documentFormats = ["PDF", "DOCX"];
console.log(getAcceptedFormatsMessage(documentFormats));
// "Accepted formats: PDF or DOCX"
此模式适用于图片上传、文档提交及任何带格式限制的文件输入。
故障排查建议格式化
错误消息通常会提供多种解决问题的方法。请将这些建议以“或”列表的形式呈现:
const formatter = new Intl.ListFormat("en", { type: "disjunction" });
function getAuthenticationError(missingFields) {
if (missingFields.length === 0) {
return "Authentication failed";
}
return `Please check your ${formatter.format(missingFields)} and try again.`;
}
console.log(getAuthenticationError(["username", "password"]));
// "Please check your username or password and try again."
console.log(getAuthenticationError(["email", "username", "password"]));
// "Please check your email, username, or password and try again."
使用“或”列表可以明确告知用户只需修复其中任意一个字段,而非全部字段。
搜索筛选条件的格式化方式
搜索界面会显示已激活的筛选条件。当筛选条件存在备选项时,请使用“或”列表:
const formatter = new Intl.ListFormat("en", { type: "disjunction" });
function getFilterSummary(filters) {
if (filters.length === 0) {
return "No filters applied";
}
if (filters.length === 1) {
return `Showing results for: ${filters[0]}`;
}
return `Showing results for: ${formatter.format(filters)}`;
}
const categories = ["Electronics", "Books", "Clothing"];
console.log(getFilterSummary(categories));
// "Showing results for: Electronics, Books, or Clothing"
此方法适用于分类筛选、标签选择以及任何选中值代表备选项而非组合的筛选界面。
复用格式化器以提升性能
创建 Intl.ListFormat 实例会有一定开销。建议只创建一次并复用:
// Create once at module level
const disjunctionFormatter = new Intl.ListFormat("en", { type: "disjunction" });
// Reuse in multiple functions
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>"
这种方式可以在自定义展示列表项的同时,保持本地化正确的标点和连接词。
浏览器支持与兼容性
自 2021 年 4 月起,所有现代浏览器均支持 Intl.ListFormat,包括 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 {
// Fallback for older browsers
return items.join(", ");
}
为获得更广泛的兼容性,可使用如 @formatjs/intl-listformat 这样的 polyfill,仅在必要时安装:
if (typeof Intl.ListFormat === "undefined") {
await import("@formatjs/intl-listformat/polyfill");
}
const formatter = new Intl.ListFormat("en", { type: "disjunction" });
鉴于当前浏览器的支持,大多数应用可以直接使用 Intl.ListFormat,无需 polyfill。
常见错误及避免方法
使用连词类型而非析取类型会导致含义错误:
// Wrong: suggests all methods required
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"
// Correct: suggests choosing one method
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"
重复创建新的格式化器会浪费资源:
// Inefficient
function formatOptions(options) {
return new Intl.ListFormat("en", { type: "disjunction" }).format(options);
}
// Efficient
const formatter = new Intl.ListFormat("en", { type: "disjunction" });
function formatOptions(options) {
return formatter.format(options);
}
在字符串中硬编码 "or" 会阻碍本地化:
// Breaks in other languages
const text = items.join(", ") + ", or other options";
// Works across languages
const formatter = new Intl.ListFormat(userLocale, { type: "disjunction" });
const allItems = [...items, "other options"];
const text = formatter.format(allItems);
未处理空数组可能导致意外输出:
// Defensive
function formatPaymentMethods(methods) {
if (methods.length === 0) {
return "No payment methods available";
}
return formatter.format(methods);
}
虽然 format([]) 会返回空字符串,但显式处理空状态能提升用户体验。
何时使用析取列表
当需要展示备选项或选择项,且通常只适用一个选项时,应使用析取列表。这包括支付方式选择、文件格式限制、认证错误建议、搜索筛选选项和账户类型选择等场景。
当所有项目都必须同时适用时,不要使用析取列表,应使用连词列表。例如,“姓名、邮箱和密码为必填项”使用连词,因为所有字段都必须填写,而不是只选一个。
对于没有选择含义的中性枚举,不要使用析取列表。测量值和技术规格通常使用单位列表,而不是析取或连词列表。
该 API 替代了手动拼接字符串的方式来处理备选项。每当你需要在面向用户的文本中用 "or" 拼接项目时,请考虑使用析取类型的 Intl.ListFormat,以获得更好的本地化支持。