如何在 JavaScript 中格式化类似 A、B、C 的列表?

使用 Intl.ListFormat 按照本地化规则和分隔符格式化数组。

简介

当你向用户展示一个项目列表时,需要用逗号和类似“和”这样的连接词将它们连接起来。不同语言在列表格式上有不同的习惯。英文使用逗号和 "and",西班牙语用 "y",法语用 "et",而中文则有完全不同的标点用法。

Intl.ListFormat API 可以将数组格式化为符合本地语言习惯的字符串,自动处理分隔符和连接词的差异,适配不同文化的列表格式。

手动格式化列表的问题

你可以用 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"

格式化器会根据指定的 locale 使用正确的分隔符和连接词。你可以将 locale 作为构造函数的第一个参数传入。

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"

narrow 样式通常会省略连词。具体输出取决于本地化规则。

不同语言环境的列表格式

每种语言环境都有自己的列表格式规则。格式化器会自动应用这些规则。

英语使用逗号,并在“和”前加牛津逗号。

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"

中文使用 "和" 作为 "and",并用顿号 、 作为分隔符。

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

输出是一个包含 typevalue 属性的对象数组。

[
  { 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>"

这种方式让你能够精确控制格式,同时保持符合本地习惯的分隔符和连词。

浏览器支持

Intl.ListFormat API 在所有现代浏览器中都可用。从 2021 年 4 月起,主流浏览器(包括 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");
}

对于旧版浏览器,你需要提供降级方案或使用 polyfill。降级方案可以使用简单的 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 的浏览器中也能正常运行。