Intl API 详解:NumberFormat / DateTimeFormat
High Contrast
Dark Mode
Light Mode
Sepia
Forest
2 min read400 words

Intl API 详解:NumberFormat / DateTimeFormat

核心问题:浏览器原生 Intl API 能做什么?如何在不引入第三方库的情况下处理多种语言的数字和日期格式?


真实场景

你的全球电商平台需要在不同地区显示商品价格:美国用户看 $1,234.56,德国用户看 1.234,56 €,日本用户看 ¥1,235。使用 JavaScript 原生 Intl API 可以零依赖地处理这些格式差异。


Intl API 全景

Intl 是 ECMAScript 国际化 API 的命名空间,包含以下核心类:

功能 浏览器支持
Intl.NumberFormat 数字、货币、百分比格式化 全面支持
Intl.DateTimeFormat 日期和时间格式化 全面支持
Intl.RelativeTimeFormat 相对时间("3天前") 全面支持
Intl.Collator 语言敏感字符串排序 全面支持
Intl.PluralRules 复数规则判断 全面支持
Intl.ListFormat 列表格式化("A、B 和 C") 全面支持
Intl.Segmenter 文本分割(按字/词/句) 较新,Chrome 87+, Safari 16+
Intl.DisplayNames 语言/地区/货币的本地化名称 全面支持
Intl.DurationFormat 时间段格式化 实验性

Intl.NumberFormat

基础数字格式化

// 构造函数缓存(重要!避免重复创建带来的性能损耗)
const numberFormatters = new Map();
function getNumberFormatter(locale, options = {}) {
const key = `${locale}:${JSON.stringify(options)}`;
if (!numberFormatters.has(key)) {
numberFormatters.set(key, new Intl.NumberFormat(locale, options));
}
return numberFormatters.get(key);
}
// 使用
const num = 1234567.89;
getNumberFormatter('zh-CN').format(num);  // "1,234,567.89"
getNumberFormatter('de-DE').format(num);  // "1.234.567,89"
getNumberFormatter('fr-FR').format(num);  // "1 234 567,89"
getNumberFormatter('hi-IN').format(num);  // "12,34,567.89"(印度分组)
getNumberFormatter('ar-SA').format(num);  // "١٬٢٣٤٬٥٦٧٫٨٩"

货币格式化

const currency = (amount, currency, locale) =>
new Intl.NumberFormat(locale, {
style: 'currency',
currency,
// currencyDisplay: 'symbol' | 'code' | 'name' | 'narrowSymbol'
currencyDisplay: 'symbol',
// 最小/最大小数位数
minimumFractionDigits: 2,
maximumFractionDigits: 2,
}).format(amount);
currency(1234.56, 'USD', 'en-US');  // "$1,234.56"
currency(1234.56, 'EUR', 'de-DE');  // "1.234,56 €"
currency(1234.56, 'EUR', 'fr-FR');  // "1 234,56 €"
currency(1234.56, 'CNY', 'zh-CN');  // "¥1,234.56"
currency(1234,    'JPY', 'ja-JP');  // "¥1,234"(日元无小数)
currency(1234.56, 'SAR', 'ar-SA');  // "١٬٢٣٤٫٥٦ ر.س."
// 货币代码显示
currency(1234.56, 'USD', 'zh-CN');
// 使用 currencyDisplay: 'code' → "USD 1,234.56"
// 使用 currencyDisplay: 'name' → "1,234.56美元"

百分比和单位

// 百分比
new Intl.NumberFormat('zh-CN', { style: 'percent' }).format(0.856);
// "85.6%"
new Intl.NumberFormat('ar-SA', { style: 'percent' }).format(0.856);
// "٨٥٫٦٪"
// 单位(需要浏览器支持)
new Intl.NumberFormat('zh-CN', {
style: 'unit',
unit: 'kilometer',
unitDisplay: 'short',
}).format(1234.5);  // "1,234.5公里"
new Intl.NumberFormat('en-US', {
style: 'unit',
unit: 'mile',
unitDisplay: 'long',
}).format(1234.5);  // "1,234.5 miles"
// 支持的单位(部分):
// acre, bit, byte, celsius, centimeter, day, degree, fahrenheit,
// foot, gigabyte, gram, hectare, hour, inch, kilobyte, kilometer,
// kilogram, liter, megabyte, meter, mile, minute, month, ounce,
// percent, pound, second, terabyte, week, yard, year

高级数字格式选项

// 紧凑格式(K/M/B)
new Intl.NumberFormat('zh-CN', { notation: 'compact' }).format(1234567);
// "123万"
new Intl.NumberFormat('en-US', { notation: 'compact' }).format(1234567);
// "1.2M"
// 科学计数法
new Intl.NumberFormat('en-US', { notation: 'scientific' }).format(1234567);
// "1.235E6"
// 工程计数法
new Intl.NumberFormat('en-US', { notation: 'engineering' }).format(1234567);
// "1.235E6"
// 有效数字位数控制
new Intl.NumberFormat('en-US', {
maximumSignificantDigits: 3,
}).format(1234567);  // "1,230,000"
// 分组分隔符控制
new Intl.NumberFormat('en-US', { useGrouping: false }).format(1234567);
// "1234567"(无千分位)
// 符号展示
new Intl.NumberFormat('en-US', { signDisplay: 'always' }).format(42);
// "+42"(正数也显示 +)
new Intl.NumberFormat('en-US', { signDisplay: 'always' }).format(-42);
// "-42"

Unicode 数字系统

// 切换数字系统(nu 扩展)
new Intl.NumberFormat('ar-SA-u-nu-latn').format(12345);
// "12,345"(拉丁数字,而非阿拉伯数字)
new Intl.NumberFormat('zh-CN-u-nu-hanidec').format(12345);
// "一二,三四五"(汉字数字)
// 常见数字系统:
// latn(0-9),arab(٠-٩),hanidec(零一二...),
// fullwide(0-9),tibt(西藏数字)

Intl.DateTimeFormat

基础日期格式化

const date = new Date('2026-03-22T15:30:00Z');
// 默认格式
new Intl.DateTimeFormat('zh-CN').format(date);  // "2026/3/22"
new Intl.DateTimeFormat('en-US').format(date);  // "3/22/2026"
new Intl.DateTimeFormat('en-GB').format(date);  // "22/03/2026"
new Intl.DateTimeFormat('ja-JP').format(date);  // "2026/3/22"
new Intl.DateTimeFormat('de-DE').format(date);  // "22.3.2026"
new Intl.DateTimeFormat('ar-SA').format(date);  // "٢٢‏/٣‏/٢٠٢٦"
// 详细格式选项
new Intl.DateTimeFormat('zh-CN', {
year: 'numeric',   // '2-digit' | 'numeric'
month: 'long',     // '2-digit' | 'numeric' | 'narrow' | 'short' | 'long'
day: 'numeric',
weekday: 'long',   // 'narrow' | 'short' | 'long'
}).format(date);
// "2026年3月22日星期日"
new Intl.DateTimeFormat('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
weekday: 'long',
}).format(date);
// "Sunday, March 22, 2026"

时间格式化

const datetime = new Date('2026-03-22T15:30:45Z');
// 时间组件
new Intl.DateTimeFormat('zh-CN', {
hour: 'numeric',
minute: '2-digit',
second: '2-digit',
hour12: false,
timeZone: 'Asia/Shanghai',
}).format(datetime);
// "23:30:45"(UTC+8)
new Intl.DateTimeFormat('en-US', {
hour: 'numeric',
minute: '2-digit',
hour12: true,
timeZone: 'America/New_York',
}).format(datetime);
// "11:30 AM"(EST/EDT)
// 时区名称
new Intl.DateTimeFormat('zh-CN', {
hour: 'numeric',
minute: '2-digit',
timeZoneName: 'long', // 'short' | 'long' | 'shortOffset' | 'longOffset'
timeZone: 'Asia/Shanghai',
}).format(datetime);
// "23:30 中国标准时间"

预设风格(dateStyle / timeStyle)

// 使用预设风格(更简洁)
// 'full' | 'long' | 'medium' | 'short'
const opts = { dateStyle: 'long', timeStyle: 'short', timeZone: 'Asia/Shanghai' };
new Intl.DateTimeFormat('zh-CN', opts).format(datetime);  // "2026年3月22日 23:30"
new Intl.DateTimeFormat('en-US', opts).format(datetime);  // "March 22, 2026 at 11:30 PM"
new Intl.DateTimeFormat('ja-JP', opts).format(datetime);  // "2026年3月22日 23:30"
new Intl.DateTimeFormat('ar-SA', opts).format(datetime);  // "٢٢ مارس ٢٠٢٦ في ١١:٣٠ م"

formatToParts:细粒度控制

// 获取各个部分,自定义组合
const parts = new Intl.DateTimeFormat('zh-CN', {
year: 'numeric', month: 'long', day: 'numeric', weekday: 'long',
}).formatToParts(new Date('2026-03-22'));
// 输出类似:
// [
//   { type: 'year', value: '2026' },
//   { type: 'literal', value: '年' },
//   { type: 'month', value: '3' },
//   { type: 'literal', value: '月' },
//   { type: 'day', value: '22' },
//   { type: 'literal', value: '日' },
//   { type: 'weekday', value: '星期日' },
// ]
// 用于自定义样式(如高亮年份)
const formatted = parts.map(({ type, value }) =>
type === 'year' ? `<strong>${value}</strong>` : value
).join('');

Intl.RelativeTimeFormat

const rtf = new Intl.RelativeTimeFormat('zh-CN', { numeric: 'auto' });
rtf.format(-1, 'day');    // "昨天"
rtf.format(0, 'day');     // "今天"
rtf.format(1, 'day');     // "明天"
rtf.format(-3, 'hour');   // "3小时前"
rtf.format(7, 'day');     // "7天后"
rtf.format(-2, 'week');   // "2周前"
rtf.format(-1, 'month');  // "上个月"
rtf.format(-1, 'year');   // "去年"
// 英语
const rtfEn = new Intl.RelativeTimeFormat('en-US', { numeric: 'auto' });
rtfEn.format(-1, 'day');   // "yesterday"
rtfEn.format(3, 'hour');   // "in 3 hours"
// 自动计算相对时间(工具函数)
function formatRelativeTime(date: Date, locale: string): string {
const rtf = new Intl.RelativeTimeFormat(locale, { numeric: 'auto' });
const now = new Date();
const diffMs = date.getTime() - now.getTime();
const diffSeconds = Math.round(diffMs / 1000);
const diffMinutes = Math.round(diffSeconds / 60);
const diffHours = Math.round(diffMinutes / 60);
const diffDays = Math.round(diffHours / 24);
if (Math.abs(diffSeconds) < 60) return rtf.format(diffSeconds, 'second');
if (Math.abs(diffMinutes) < 60) return rtf.format(diffMinutes, 'minute');
if (Math.abs(diffHours) < 24) return rtf.format(diffHours, 'hour');
if (Math.abs(diffDays) < 30) return rtf.format(diffDays, 'day');
// 超过30天显示完整日期
return new Intl.DateTimeFormat(locale, { dateStyle: 'medium' }).format(date);
}

Intl.ListFormat

const listEn = new Intl.ListFormat('en-US', { style: 'long', type: 'conjunction' });
listEn.format(['Alice', 'Bob', 'Charlie']);  // "Alice, Bob, and Charlie"
const listZh = new Intl.ListFormat('zh-CN', { style: 'long', type: 'conjunction' });
listZh.format(['苹果', '香蕉', '橙子']);  // "苹果、香蕉和橙子"
// type 选项:
// 'conjunction': "A, B, and C"
// 'disjunction': "A, B, or C"
// 'unit': "A, B, C"(无连词)

性能最佳实践

// ✅ 推荐:缓存 Intl 对象(创建代价较高)
class I18nFormatter {
private numberFormatters = new Map<string, Intl.NumberFormat>();
private dateFormatters = new Map<string, Intl.DateTimeFormat>();
number(locale: string, options?: Intl.NumberFormatOptions): Intl.NumberFormat {
const key = `${locale}:${JSON.stringify(options)}`;
if (!this.numberFormatters.has(key)) {
this.numberFormatters.set(key, new Intl.NumberFormat(locale, options));
}
return this.numberFormatters.get(key)!;
}
currency(amount: number, currency: string, locale: string): string {
return this.number(locale, { style: 'currency', currency }).format(amount);
}
date(date: Date, locale: string, options?: Intl.DateTimeFormatOptions): string {
const key = `${locale}:${JSON.stringify(options)}`;
if (!this.dateFormatters.has(key)) {
this.dateFormatters.set(key, new Intl.DateTimeFormat(locale, options));
}
return this.dateFormatters.get(key)!.format(date);
}
}
// ❌ 避免:每次调用都创建新对象
function formatPrice(amount: number, locale: string) {
return new Intl.NumberFormat(locale, { style: 'currency', currency: 'USD' }).format(amount);
// 每次调用都创建新的 NumberFormat 对象,性能差
}

下一节Intl.NumberFormat 可以格式化数字,但不能处理复数("1 item" vs "2 items")。下面学习 CLDR 复数规则和 ICU 消息格式。

CLDR 复数规则与 ICU 消息格式