如何格式化带单位的测量值列表?

使用 JavaScript 的 Intl API,根据本地习惯格式化多个测量值列表,如 5 km、10 km、15 km

简介

在需要显示测量值的应用中,经常需要将多个数值一起展示。例如,健身应用可能会以“5 km、10 km、15 km”显示分段距离;天气应用可能会以“20°C、22°C、25°C、23°C”展示一周的温度;食谱可能会以“2 杯、1 汤匙、3 茶匙”列出配料用量。

这些列表涉及两个国际化难题。首先,每个测量值都需要符合本地习惯的单位格式。其次,整个列表本身也需要使用目标语言的正确标点和分隔符。英文通常用逗号,有时还会用“and”等连词。其他语言则有不同的分隔方式和语法规则。

JavaScript 提供了两个 API 来解决这个问题。Intl.NumberFormat 用于格式化单个带单位的测量值。Intl.ListFormat 用于将多个值组合成语法正确的列表。本教程将介绍如何结合使用这两个 API,生成符合用户所在地区习惯的测量值列表。

格式化测量值列表需要两个步骤

在格式化测量值列表时,不能省略任何一个步骤。如果只格式化列表而不格式化测量值,虽然分隔符符合本地习惯,但单位显示会不正确。如果只格式化测量值而不格式化列表,单位正确但分隔符不符合本地规则。

const distances = [5, 10, 15];

// Wrong: list formatted but not measurements
console.log(distances.join(', '));
// Output: "5, 10, 15" (missing units)

// Wrong: measurements formatted but not list
const formatter = new Intl.NumberFormat('en-US', {
  style: 'unit',
  unit: 'kilometer'
});
console.log(distances.map(d => formatter.format(d)).join(', '));
// Output: "5 km, 10 km, 15 km" (hardcoded comma might be wrong for some locales)

正确的方法是先格式化每个测量值,再将格式化后的字符串数组作为列表进行格式化。

const distances = [5, 10, 15];

const numberFormatter = new Intl.NumberFormat('en-US', {
  style: 'unit',
  unit: 'kilometer'
});

const formattedMeasurements = distances.map(d => numberFormatter.format(d));
// Result: ["5 km", "10 km", "15 km"]

const listFormatter = new Intl.ListFormat('en-US', {
  type: 'unit'
});

console.log(listFormatter.format(formattedMeasurements));
// Output: "5 km, 10 km, 15 km"

这种模式适用于任何类型的测量值和任何语言环境。先为每个测量值加上单位格式,再将格式化后的字符串数组作为列表进行本地化处理。

在测量值列表中使用 type unit

Intl.ListFormat 构造函数接受一个 type 选项,用于控制列表项的组合方式。type: 'unit' 选项会按照技术和科学数据的惯例格式化列表。

const measurements = ['5 km', '10 km', '15 km'];

const unitList = new Intl.ListFormat('en-US', {
  type: 'unit'
});

console.log(unitList.format(measurements));
// Output: "5 km, 10 km, 15 km"

带有 type: 'unit' 的列表会省略“和”或“或”等连接词,仅在各项之间使用简单分隔符。这符合技术场景下测量值的常见书写方式。

type: 'conjunction' 相比,后者会在最后一项前添加“和”。

const measurements = ['5 km', '10 km', '15 km'];

const conjunctionList = new Intl.ListFormat('en-US', {
  type: 'conjunction'
});

console.log(conjunctionList.format(measurements));
// Output: "5 km, 10 km, and 15 km"

连接词形式在叙述性文本中读起来更自然,但在技术场景下则不规范。显示多个测量值时,应使用 type: 'unit',以符合科学和技术写作的标准惯例。

在列表中格式化距离测量值

距离测量值使用 kilometermetermilefoot 等单位标识符。将每个距离与其单位格式化后,再组合成一个列表。

const distances = [5, 10, 15, 20];

const numberFormatter = new Intl.NumberFormat('en-US', {
  style: 'unit',
  unit: 'kilometer'
});

const formattedDistances = distances.map(d => numberFormatter.format(d));

const listFormatter = new Intl.ListFormat('en-US', {
  type: 'unit'
});

console.log(listFormatter.format(formattedDistances));
// Output: "5 km, 10 km, 15 km, 20 km"

同样的模式也适用于英里。

const distances = [3, 6, 9];

const numberFormatter = new Intl.NumberFormat('en-US', {
  style: 'unit',
  unit: 'mile'
});

const formattedDistances = distances.map(d => numberFormatter.format(d));

const listFormatter = new Intl.ListFormat('en-US', {
  type: 'unit'
});

console.log(listFormatter.format(formattedDistances));
// Output: "3 mi, 6 mi, 9 mi"

可以通过设置数字格式化选项来显示带小数位的距离。

const distances = [5.2, 10.7, 15.3];

const numberFormatter = new Intl.NumberFormat('en-US', {
  style: 'unit',
  unit: 'kilometer',
  maximumFractionDigits: 1
});

const formattedDistances = distances.map(d => numberFormatter.format(d));

const listFormatter = new Intl.ListFormat('en-US', {
  type: 'unit'
});

console.log(listFormatter.format(formattedDistances));
// Output: "5.2 km, 10.7 km, 15.3 km"

数字格式化器会在列表格式化器组合数值前,自动处理四舍五入和小数位。

在列表中格式化重量测量值

重量测量值同样遵循该模式,使用 kilogrampoundouncegram 等单位标识符。

const weights = [50, 75, 100];

const numberFormatter = new Intl.NumberFormat('en-US', {
  style: 'unit',
  unit: 'kilogram'
});

const formattedWeights = weights.map(w => numberFormatter.format(w));

const listFormatter = new Intl.ListFormat('en-US', {
  type: 'unit'
});

console.log(listFormatter.format(formattedWeights));
// Output: "50 kg, 75 kg, 100 kg"

也可以选择以磅为单位显示重量。

const weights = [110, 165, 220];

const numberFormatter = new Intl.NumberFormat('en-US', {
  style: 'unit',
  unit: 'pound'
});

const formattedWeights = weights.map(w => numberFormatter.format(w));

const listFormatter = new Intl.ListFormat('en-US', {
  type: 'unit'
});

console.log(listFormatter.format(formattedWeights));
// Output: "110 lb, 165 lb, 220 lb"

数字格式化器会自动使用每个单位的正确缩写。

在列表中格式化温度测量值

温度测量使用单位标识符,如 celsiusfahrenheit

const temperatures = [20, 22, 25, 23, 21];

const numberFormatter = new Intl.NumberFormat('en-US', {
  style: 'unit',
  unit: 'celsius'
});

const formattedTemperatures = temperatures.map(t => numberFormatter.format(t));

const listFormatter = new Intl.ListFormat('en-US', {
  type: 'unit'
});

console.log(listFormatter.format(formattedTemperatures));
// Output: "20°C, 22°C, 25°C, 23°C, 21°C"

温度格式化器会自动在输出中包含度数符号。

华氏温度的处理方式相同。

const temperatures = [68, 72, 77, 73, 70];

const numberFormatter = new Intl.NumberFormat('en-US', {
  style: 'unit',
  unit: 'fahrenheit'
});

const formattedTemperatures = temperatures.map(t => numberFormatter.format(t));

const listFormatter = new Intl.ListFormat('en-US', {
  type: 'unit'
});

console.log(listFormatter.format(formattedTemperatures));
// Output: "68°F, 72°F, 77°F, 73°F, 70°F"

不同测量类型的格式模式保持一致,仅单位标识符不同。

在列表中格式化体积测量值

体积测量使用单位标识符,如 litergallonmilliliterfluid-ounce

const volumes = [1, 2, 3];

const numberFormatter = new Intl.NumberFormat('en-US', {
  style: 'unit',
  unit: 'liter'
});

const formattedVolumes = volumes.map(v => numberFormatter.format(v));

const listFormatter = new Intl.ListFormat('en-US', {
  type: 'unit'
});

console.log(listFormatter.format(formattedVolumes));
// Output: "1 L, 2 L, 3 L"

体积测量支持小数值。

const volumes = [0.5, 1.5, 2.5];

const numberFormatter = new Intl.NumberFormat('en-US', {
  style: 'unit',
  unit: 'liter',
  maximumFractionDigits: 1
});

const formattedVolumes = volumes.map(v => numberFormatter.format(v));

const listFormatter = new Intl.ListFormat('en-US', {
  type: 'unit'
});

console.log(listFormatter.format(formattedVolumes));
// Output: "0.5 L, 1.5 L, 2.5 L"

数值格式化器会在列表格式化器处理数值前,先处理小数精度。

在列表中格式化速度测量值

速度测量使用复合单位,如 kilometer-per-hourmile-per-hour

const speeds = [50, 75, 100];

const numberFormatter = new Intl.NumberFormat('en-US', {
  style: 'unit',
  unit: 'kilometer-per-hour'
});

const formattedSpeeds = speeds.map(s => numberFormatter.format(s));

const listFormatter = new Intl.ListFormat('en-US', {
  type: 'unit'
});

console.log(listFormatter.format(formattedSpeeds));
// Output: "50 km/h, 75 km/h, 100 km/h"

英里每小时的处理方式相同。

const speeds = [30, 45, 60];

const numberFormatter = new Intl.NumberFormat('en-US', {
  style: 'unit',
  unit: 'mile-per-hour'
});

const formattedSpeeds = speeds.map(s => numberFormatter.format(s));

const listFormatter = new Intl.ListFormat('en-US', {
  type: 'unit'
});

console.log(listFormatter.format(formattedSpeeds));
// Output: "30 mph, 45 mph, 60 mph"

复合单位会自动使用正确的缩写和分隔符进行格式化。

语言环境决定列表分隔符格式

locale 参数控制列表项的分隔和标点方式。不同语言对列表格式有不同的约定。

const distances = [5, 10, 15];

const numberFormatter = new Intl.NumberFormat('en-US', {
  style: 'unit',
  unit: 'kilometer'
});

const formattedDistances = distances.map(d => numberFormatter.format(d));

const enList = new Intl.ListFormat('en-US', {
  type: 'unit'
});

const frList = new Intl.ListFormat('fr-FR', {
  type: 'unit'
});

const deList = new Intl.ListFormat('de-DE', {
  type: 'unit'
});

console.log(enList.format(formattedDistances));
// Output: "5 km, 10 km, 15 km"

console.log(frList.format(formattedDistances));
// Output: "5 km, 10 km, 15 km"

console.log(deList.format(formattedDistances));
// Output: "5 km, 10 km, 15 km"

虽然千米的缩写在这些语言环境中大致相同,但空格和分隔符的用法可能不同。Intl.ListFormat API 会自动处理这些与 locale 相关的格式规则。

有些语言在列表中使用不同的分隔符或标点模式。API 可确保您的列表在每种语言环境下都遵循正确的约定,无需您了解具体规则。

数字语言环境应与列表语言环境一致

在格式化测量值列表时,数字格式化器和列表格式化器应使用相同的 locale,以确保输出格式一致。

const distances = [1000, 2000, 3000];

const locale = 'de-DE';

const numberFormatter = new Intl.NumberFormat(locale, {
  style: 'unit',
  unit: 'meter'
});

const formattedDistances = distances.map(d => numberFormatter.format(d));

const listFormatter = new Intl.ListFormat(locale, {
  type: 'unit'
});

console.log(listFormatter.format(formattedDistances));
// Output: "1.000 m, 2.000 m, 3.000 m"

德语格式使用句点作为千位分隔符。数字格式化器和列表格式化器都采用德语习惯,因为它们共享同一个 locale。

为数字和列表格式化使用不同的 locale 会导致输出不一致。

const distances = [1000, 2000, 3000];

const numberFormatter = new Intl.NumberFormat('de-DE', {
  style: 'unit',
  unit: 'meter'
});

const formattedDistances = distances.map(d => numberFormatter.format(d));

const listFormatter = new Intl.ListFormat('en-US', {
  type: 'unit'
});

console.log(listFormatter.format(formattedDistances));
// Output: "1.000 m, 2.000 m, 3.000 m"

这样会产生混合格式,数字采用德语习惯,而列表则使用英语习惯。务必为两个格式化器使用相同的 locale。

控制列表显示样式

style 选项用于控制列表格式化的详细程度。该选项接受三个值:"long""short""narrow"

const measurements = ['5 km', '10 km', '15 km'];

const longList = new Intl.ListFormat('en-US', {
  type: 'unit',
  style: 'long'
});

const shortList = new Intl.ListFormat('en-US', {
  type: 'unit',
  style: 'short'
});

const narrowList = new Intl.ListFormat('en-US', {
  type: 'unit',
  style: 'narrow'
});

console.log(longList.format(measurements));
// Output: "5 km, 10 km, 15 km"

console.log(shortList.format(measurements));
// Output: "5 km, 10 km, 15 km"

console.log(narrowList.format(measurements));
// Output: "5 km 10 km 15 km"

longshort 样式在英文单位列表中产生类似的输出。narrow 样式则使用最少的空格,并省略项目之间的分隔符。

不同的 locale 在样式之间的差异更大。locale 决定了每个样式级别的具体格式。

与长单位名称结合使用

通过在数字格式化器中设置 unitDisplay: 'long',可以用完整的单位名称而不是缩写来格式化度量值。

const distances = [5, 10, 15];

const numberFormatter = new Intl.NumberFormat('en-US', {
  style: 'unit',
  unit: 'kilometer',
  unitDisplay: 'long'
});

const formattedDistances = distances.map(d => numberFormatter.format(d));

const listFormatter = new Intl.ListFormat('en-US', {
  type: 'unit'
});

console.log(listFormatter.format(formattedDistances));
// Output: "5 kilometers, 10 kilometers, 15 kilometers"

数字格式化器会自动处理单数和复数形式。列表格式化器会将格式化后的字符串组合,无论它们是缩写还是全称。

const distances = [1, 5];

const numberFormatter = new Intl.NumberFormat('en-US', {
  style: 'unit',
  unit: 'kilometer',
  unitDisplay: 'long'
});

const formattedDistances = distances.map(d => numberFormatter.format(d));

const listFormatter = new Intl.ListFormat('en-US', {
  type: 'unit'
});

console.log(listFormatter.format(formattedDistances));
// Output: "1 kilometer, 5 kilometers"

数字格式化器会对 1 使用“kilometer”,对 5 使用“kilometers”。列表格式化器会用合适的分隔符将它们组合。

复用格式化器以提升性能

创建 Intl.NumberFormatIntl.ListFormat 实例时需要加载 locale 数据并处理选项。当你需要格式化多个度量列表时,建议只创建一次格式化器并复用。

const numberFormatter = new Intl.NumberFormat('en-US', {
  style: 'unit',
  unit: 'kilometer'
});

const listFormatter = new Intl.ListFormat('en-US', {
  type: 'unit'
});

const distanceLists = [
  [5, 10, 15],
  [20, 25, 30],
  [35, 40, 45]
];

distanceLists.forEach(distances => {
  const formattedDistances = distances.map(d => numberFormatter.format(d));
  console.log(listFormatter.format(formattedDistances));
});
// Output:
// "5 km, 10 km, 15 km"
// "20 km, 25 km, 30 km"
// "35 km, 40 km, 45 km"

此模式会为每个格式化器只创建一次实例,并多次复用。当需要格式化大量列表时,性能差异会变得显著。

创建可复用的格式化函数

你可以将这种两步格式化模式封装到一个可复用的函数中。

function formatMeasurementList(values, locale, unit) {
  const numberFormatter = new Intl.NumberFormat(locale, {
    style: 'unit',
    unit: unit
  });

  const formattedValues = values.map(v => numberFormatter.format(v));

  const listFormatter = new Intl.ListFormat(locale, {
    type: 'unit'
  });

  return listFormatter.format(formattedValues);
}

console.log(formatMeasurementList([5, 10, 15], 'en-US', 'kilometer'));
// Output: "5 km, 10 km, 15 km"

console.log(formatMeasurementList([50, 75, 100], 'en-US', 'kilogram'));
// Output: "50 kg, 75 kg, 100 kg"

console.log(formatMeasurementList([20, 22, 25], 'en-US', 'celsius'));
// Output: "20°C, 22°C, 25°C"

该函数可处理任意测量类型和 locale。你还可以扩展它以支持更多格式化选项。

function formatMeasurementList(values, locale, unit, options = {}) {
  const numberFormatter = new Intl.NumberFormat(locale, {
    style: 'unit',
    unit: unit,
    ...options
  });

  const formattedValues = values.map(v => numberFormatter.format(v));

  const listFormatter = new Intl.ListFormat(locale, {
    type: 'unit'
  });

  return listFormatter.format(formattedValues);
}

console.log(formatMeasurementList(
  [5.123, 10.789, 15.456],
  'en-US',
  'kilometer',
  { maximumFractionDigits: 1 }
));
// Output: "5.1 km, 10.8 km, 15.5 km"

console.log(formatMeasurementList(
  [1, 5, 10],
  'en-US',
  'kilometer',
  { unitDisplay: 'long' }
));
// Output: "1 kilometer, 5 kilometers, 10 kilometers"

该函数会将额外选项传递给数字格式化器,从而可以控制小数位数、单位显示方式及其他格式化设置。

按用户 locale 格式化列表

你无需硬编码特定 locale,可以使用用户的浏览器语言偏好。navigator.language 属性会返回用户的首选 locale。

const userLocale = navigator.language;

const distances = [5, 10, 15];

const numberFormatter = new Intl.NumberFormat(userLocale, {
  style: 'unit',
  unit: 'kilometer'
});

const formattedDistances = distances.map(d => numberFormatter.format(d));

const listFormatter = new Intl.ListFormat(userLocale, {
  type: 'unit'
});

console.log(listFormatter.format(formattedDistances));
// Output varies by user's locale

这种方式会根据每位用户的格式化习惯显示测量列表。不同用户会看到同样的数据,但格式符合其 locale 约定。

在应用中显示测量列表

你可以在任何需要向用户展示多个测量值的场景中使用这种模式。例如,健身应用显示分段时间、天气应用显示温度预报、食谱应用显示配料用量、科学应用显示实验数据等。

const splitTimes = [5, 10, 15, 20];

const numberFormatter = new Intl.NumberFormat(navigator.language, {
  style: 'unit',
  unit: 'kilometer',
  maximumFractionDigits: 1
});

const formattedTimes = splitTimes.map(t => numberFormatter.format(t));

const listFormatter = new Intl.ListFormat(navigator.language, {
  type: 'unit'
});

const result = listFormatter.format(formattedTimes);

document.getElementById('split-times').textContent = result;
// Displays: "5 km, 10 km, 15 km, 20 km" (or locale equivalent)

格式化后的字符串与其他字符串值一样,可以插入到文本内容、属性或任何向用户展示信息的场景中。