Intl.ListFormat API

将数组格式化为符合本地习惯的可读列表

简介

在向用户展示多个项目时,开发者通常会用逗号连接数组元素,并在最后一个元素前加上“and”:

const users = ["Alice", "Bob", "Charlie"];
const message = users.slice(0, -1).join(", ") + ", and " + users[users.length - 1];
// "Alice, Bob, and Charlie"

这种做法会将英文的标点规则硬编码,导致在其他语言中出现问题。例如,日语使用不同的助词,德语有不同的空格规则,中文则采用不同的分隔符。Intl.ListFormat API 通过根据各自语言的习惯格式化列表,解决了这一问题。

Intl.ListFormat 的作用

Intl.ListFormat 可以将数组转换为符合各语言语法和标点规则的可读列表。它支持所有语言中常见的三种列表类型:

  • 连接列表 使用“and”连接项目(如“A, B, and C”)
  • 选择列表 使用“or”表示选项(如“A, B, or C”)
  • 单位列表 用于格式化计量单位,无连接词(如“5 ft, 2 in”)

该 API 能根据不同语言的标点、用词和空格规则,正确格式化这些列表类型。

基本用法

创建格式化器时指定 locale 和 options,然后用 format() 处理数组:

const formatter = new Intl.ListFormat("en", {
  type: "conjunction",
  style: "long"
});

const items = ["bread", "milk", "eggs"];
console.log(formatter.format(items));
// "bread, milk, and eggs"

格式化器可以处理任意长度的数组,包括各种边界情况:

formatter.format([]);              // ""
formatter.format(["bread"]);       // "bread"
formatter.format(["bread", "milk"]); // "bread and milk"

列表类型控制连接词

type 选项决定格式化列表时使用哪种连接词。

连接列表

当所有项目都适用时,使用 type: "conjunction",这是默认类型:

const formatter = new Intl.ListFormat("en", { type: "conjunction" });

console.log(formatter.format(["HTML", "CSS", "JavaScript"]));
// "HTML, CSS, and JavaScript"

常见用法包括显示已选项目、列出功能、展示多个同时适用的值。

选择列表

当列表用于展示选项或备选时,使用 type: "disjunction"

const formatter = new Intl.ListFormat("en", { type: "disjunction" });

console.log(formatter.format(["credit card", "debit card", "PayPal"]));
// "credit card, debit card, or PayPal"

此内容用于选项列表、包含多种解决方案的错误信息,以及用户需要从多个项目中选择一个的场景。

单位列表

对于需要以无连词形式展示的度量和技术数值,请使用 type: "unit"

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

console.log(formatter.format(["5 feet", "2 inches"]));
// "5 feet, 2 inches"

单位列表适用于度量、技术规格和复合数值。

列表样式控制详细程度

style 选项可调整格式化的详细程度。共有三种样式:longshortnarrow

const items = ["Monday", "Wednesday", "Friday"];

const long = new Intl.ListFormat("en", { style: "long" });
console.log(long.format(items));
// "Monday, Wednesday, and Friday"

const short = new Intl.ListFormat("en", { style: "short" });
console.log(short.format(items));
// "Monday, Wednesday, and Friday"

const narrow = new Intl.ListFormat("en", { style: "narrow" });
console.log(narrow.format(items));
// "Monday, Wednesday, Friday"

在英文中,longshort 对大多数列表的输出相同。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" }
// ]

每个部分包含 typevaluetype 要么是 "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 实例有一定开销。建议只创建一次并复用:

// Create once
const listFormatter = new Intl.ListFormat("en", { type: "conjunction" });

// Reuse many times
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("at least 8 characters");
  }
  if (!/[A-Z]/.test(password)) {
    errors.push("an uppercase letter");
  }
  if (!/[0-9]/.test(password)) {
    errors.push("a number");
  }

  if (errors.length > 0) {
    return `Password must contain ${formatter.format(errors)}.`;
  }

  return null;
}

console.log(validatePassword("weak"));
// "Password must contain at least 8 characters, an uppercase letter, or a number."

析取列表能明确告知用户需要修复其中任意一个问题,并且格式会根据各地习惯自动适配。

显示已选项

当用户选择多个项目时,可用连词列表格式化选项:

const formatter = new Intl.ListFormat("en", { type: "conjunction" });

function getSelectionMessage(selectedFiles) {
  if (selectedFiles.length === 0) {
    return "No files selected";
  }

  if (selectedFiles.length === 1) {
    return `${selectedFiles[0]} selected`;
  }

  return `${formatter.format(selectedFiles)} selected`;
}

console.log(getSelectionMessage(["report.pdf", "data.csv", "notes.txt"]));
// "report.pdf, data.csv, and notes.txt selected"

这种模式适用于文件选择、筛选条件、类别选择等多选场景。

处理长列表

对于项目较多的列表,建议在格式化前进行截断:

const formatter = new Intl.ListFormat("en", { 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)}, and ${remaining} others`;
}

console.log(formatUserList(["Alice", "Bob", "Charlie", "David", "Eve"]));
// "Alice, Bob, and 3 others"

这样既保证了可读性,又能提示总数。具体阈值可根据界面需求调整。

浏览器支持与兼容方案

自 2021 年 4 月起,Intl.ListFormat 已在所有主流浏览器中支持,包括 Chrome 72+、Firefox 78+、Safari 14.1+ 和 Edge 79+。

可通过特性检测判断是否支持:

if (typeof Intl.ListFormat !== "undefined") {
  const formatter = new Intl.ListFormat("en");
  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");
}

鉴于当前浏览器的支持情况,大多数应用可以直接使用 Intl.ListFormat,无需 polyfill。

常见错误及避免方法

反复创建新的格式化器会浪费资源:

// Inefficient
function display(items) {
  return new Intl.ListFormat("en").format(items);
}

// Efficient
const formatter = new Intl.ListFormat("en");
function display(items) {
  return formatter.format(items);
}

array.join() 用于面向用户的文本会带来本地化问题:

// Breaks in other languages
const text = items.join(", ");

// Works across languages
const formatter = new Intl.ListFormat(userLocale);
const text = formatter.format(items);

假设英语的连词规则在所有语言中都适用,会导致其他语言环境下输出不正确。务必将用户的 locale 传递给构造函数。

未处理空数组可能导致意外输出:

// Defensive
function formatItems(items) {
  if (items.length === 0) {
    return "No items";
  }
  return formatter.format(items);
}

虽然 format([]) 会返回空字符串,但显式处理空状态能提升用户体验。

何时使用 Intl.ListFormat

每当需要在文本中展示多个项目时,都应使用 Intl.ListFormat。这包括导航面包屑、已选筛选项、校验错误、用户列表、分类标签和功能列表等。

不要在结构化数据展示(如表格或选项菜单)中使用它。这些组件有各自独立于文本列表规则的格式化需求。

该 API 替代了手动字符串拼接和连接的模式。每当你为面向用户的文本编写 join(", ") 时,都应考虑 Intl.ListFormat 是否能提供更好的本地化支持。