数字、货币和单位格式化

问题

如何使用JavaScript为不同语言/区域动态格式化数字、货币和单位?

数字的格式,包括货币和单位等格式,在不同文化、地区和语言之间差异很大,看似无伤大雅的错误也可能导致误解或出错。

答案

把格式硬编码到页面里很容易出问题,而且难以长期维护。Web平台提供了稳健且基于标准的方案来应对这个复杂的问题。本文将介绍如何利用JavaScript自带的Intl对象,让你的网页能够轻松适配不同国际环境下的数字、货币和单位格式。

核心问题:为什么格式化这么复杂?

在介绍解决方案之前,先看看为什么国际化数字、货币和单位格式化并不简单:

小数分隔符

许多地区,如美国和英国,使用句点作为小数分隔符(例如1,234.56),而欧洲和南美的大部分地区使用逗号(例如1.234,56)。

在少数情况下,货币符号甚至可以充当小数分隔符,例如佛得角埃斯库多

数字分组

数字的分组分隔符有所不同,可能是逗号句点,也可能是空格

分组方式也不同。比较常见的是每3位一组(例如1,234,567),但有些地区(如印度)在百位以上采用每2位一组(例如12,34,567)。某些格式也会使用每4位一组。

数字系统

虽然欧洲数字(0-9)在全球都很常见,但许多文化更偏好使用本地的数字系统。例如,12,345在阿拉伯语中可能写作١٢٬٣٤٥,在泰语中可能写作๑๒,๓๔๕

货币符号的显示方式

符号、代码还是名称:货币可以用多种方式显示:

  • 简短的符号:例如 "€"、"£"、"¥"、"$"
  • 中等长度/消歧义符号:用多个字符明确具体币种的符号(例如CA$US$MX$
  • 依上下文变化的符号:同一种货币在不同语境中使用不同符号(例如日元可显示为¥
  • ISO 4217代码:三个字母的标准化代码(例如EURGBPJPY
  • 全名:完整的货币名称(例如美元日元

位置和间距:符号可以出现在数字前面(例如$100.00),也可以出现在后面(例如1,000 ₫)。我们从这两个例子也可以看出,有些货币符号与数字之间不留空格,而有些则会留空格。

歧义:同一个符号可能代表多种货币(例如$可以表示美元、加元、墨西哥比索等)。

Intl.NumberFormat对象

JavaScript中的Intl.NumberFormat对象会自动处理特定语言/区域的小数分隔符、数字分组分隔符、货币符号以及其他数字书写习惯。

使用Intl.NumberFormat格式化普通数字

Intl.NumberFormat允许你按照指定的语言区域来格式化数字。

const number = 1234567.89;

// 面向美国英语用户
const usFormatter = new Intl.NumberFormat('en-US');
console.log(`英语(美国): ${usFormatter.format(number)}`); // 输出:英语(美国): 1,234,567.89

// 面向德语用户
const deFormatter = new Intl.NumberFormat('de-DE');
console.log(`德语: ${deFormatter.format(number)}`); // 输出:德语: 1.234.567,89

// 面向印度英语用户(注意分组方式)
const enINFormatter = new Intl.NumberFormat('en-IN');
console.log(`英语(印度): ${enINFormatter.format(number)}`); // 输出:英语(印度): 12,34,567.89

new Intl.NumberFormat(locales, options)中的options对象是关键所在。

minimumFractionDigitsmaximumFractionDigits用于控制小数位数。useGrouping是一个布尔值,用来开启或关闭数字分组。

const pi = 3.14159265;

// 强制指定精度
const preciseFormatter = new Intl.NumberFormat('en-US', {
    minimumFractionDigits: 5,
    maximumFractionDigits: 5,
});
console.log(`${preciseFormatter.format(pi)}`); // 输出:3.14159

// 不分组
const noGroupingFormatter = new Intl.NumberFormat('en-US', {
    useGrouping: false,
});
console.log(`不分组: ${noGroupingFormatter.format(1234567)}`); // 输出:不分组: 1234567

用于货币的Intl.NumberFormat

你可以指定style: 'currency',并提供currency代码(使用ISO 4217标准)。

const price = 500.75;

// 美元
const usdFormatter = new Intl.NumberFormat('en-US', {
    style: 'currency',
    currency: 'USD',
});
console.log(`美元: ${usdFormatter.format(price)}`);        // 输出:美元: $500.75

// 德国使用的欧元
const eurDeFormatter = new Intl.NumberFormat('de-DE', {
    style: 'currency',
    currency: 'EUR',
});
console.log(`欧元(德国): ${eurDeFormatter.format(price)}`); // 输出:欧元(德国): 500,75 €

// 日元(默认不显示小数位):
const jpyFormatter = new Intl.NumberFormat('ja-JP', {
    style: 'currency',
    currency: 'JPY',
});
console.log(`日元: ${jpyFormatter.format(price)}`); // 输出:日元: ¥501(会自动四舍五入且不显示小数)

// 越南盾(默认不显示小数位):
const vndFormatter = new Intl.NumberFormat('vi-VN', {
    style: 'currency',
    currency: 'VND',
});
console.log(`越南盾: ${vndFormatter.format(12000)}`); // 输出:越南盾: 12.000 ₫

Intl.NumberFormat会自动为给定货币处理正确的小数位数。如有需要,你也可以用minimumFractionDigitsmaximumFractionDigits来覆盖默认值。

使用Intl.NumberFormat格式化单位

Intl.NumberFormat也可以为带单位的数字设置格式:

const distance = 1000;
const storage = 5;

// 米
const meterFormatter = new Intl.NumberFormat('en-US', {
    style: 'unit',
    unit: 'meter', // 标准单位标识符
    unitDisplay: 'long', // 'long'、'short' 或 'narrow'
});
console.log(`美式英语中的米(长格式): ${meterFormatter.format(distance)}`); // 输出:美式英语中的米(长格式): 1,000 meters

const meterShortFormatter = new Intl.NumberFormat('en-US', {
    style: 'unit',
    unit: 'meter',
    unitDisplay: 'short',
});
console.log(`美式英语中的米(短格式): ${meterShortFormatter.format(distance)}`); // 输出:美式英语中的米(短格式): 1,000 m

// 法语区域设置中的千字节:
const kbFrFormatter = new Intl.NumberFormat('fr-FR', {
    style: 'unit',
    unit: 'kilobyte',
    unitDisplay: 'long',
});
console.log(`法语中的千字节: ${kbFrFormatter.format(storage)}`); // 输出:法语中的千字节: 5 kilooctets

标准单位标识符列表(例如meterkilogramliterkilobytepercenthour)可参见ECMAScript国际化API规范

选择合适的语言区域

通常,你会希望根据网页的语言来格式化数字、货币和单位。这可以通过HTML元素上的lang属性来确定(你需要设置这个属性):

// 从HTML lang属性获取页面语言
const pageLocale = document.documentElement.lang || 'en-US'; // 回退为'en-US'
const formatter = new Intl.NumberFormat(pageLocale, { style: 'currency', currency: 'USD' });
console.log(`${formatter.format(627.92)}`);

有时,你可能希望用某个特定语言区域覆盖页面默认语言,例如在制作国际化教程或展示多语言内容时:

// 不管页面语言是什么,都强制使用特定语言区域
const tutorialFormatter = new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' });
console.log(`德语示例: ${tutorialFormatter.format(199.99)}`); // 输出:199,99 €