TL;DR: 日期、时间、数字和货币的格式因 locale 不同而差异巨大——MM/DD 与 DD/MM、小数点是逗号还是句号、12 小时制还是 24 小时制。使用 Intl API 无需任何依赖即可实现本地化。案例来源:tanstackship.com。
引言
根据当地习惯格式化日期、数字和货币,是面向全球市场的 SaaS 产品的必备能力。美国用户期望"March 5, 2026",德国用户期望"5. März 2026",日本用户期望"2026年3月5日"。如何检测应向用户呈现哪种 locale,请参阅语言检测策略指南。
各地区格式差异
| Locale | 日期格式 | 时间格式 | 小数分隔符 | 货币符号位置 |
|---|---|---|---|---|
| en-US | MM/DD/YYYY | 12h (3:00 PM) | 1,234.56 | $1,234.56 |
| en-GB | DD/MM/YYYY | 24h (15:00) | 1,234.56 | £1,234.56 |
| de-DE | DD.MM.YYYY | 24h (15:00) | 1.234,56 | 1.234,56 € |
| zh-CN | YYYY/MM/DD | 24h (15:00) | 1,234.56 | ¥1,234.56 |
| ja-JP | YYYY年MM月DD日 | 24h (15:00) | 1,234.56 | ¥1,234.56 |
| fr-FR | DD/MM/YYYY | 24h (15:00) | 1 234,56 | 1 234,56 € |
| ar-SA | DD/MM/YYYY | 12h (٣:٠٠ م) | 1,234.56 | ١٬٢٣٤٫٥٦ ﷼ |
使用 Intl API
// 日期格式化
const date = new Date('2026-06-01')
// 美国:2026 年 6 月 1 日
new Intl.DateTimeFormat('en-US', {
dateStyle: 'long'
}).format(date) // "June 1, 2026"
// 德国:2026 年 6 月 1 日
new Intl.DateTimeFormat('de-DE', {
dateStyle: 'long'
}).format(date) // "1. Juni 2026"
// 日本:2026 年 6 月 1 日
new Intl.DateTimeFormat('ja-JP', {
dateStyle: 'long'
}).format(date) // "2026年6月1日"
// 相对时间 (Intl.RelativeTimeFormat)
const rtf = new Intl.RelativeTimeFormat('es', { numeric: 'auto' })
rtf.format(-1, 'day') // "ayer"
rtf.format(-7, 'day') // "hace 7 días"
数字和货币格式化
// 数字格式化
const price = 1234567.89
new Intl.NumberFormat('en-US').format(price) // "1,234,567.89"
new Intl.NumberFormat('de-DE').format(price) // "1.234.567,89"
new Intl.NumberFormat('ar-SA').format(price) // "١٬٢٣٤٫٥٦٧٫٨٩"
// 货币格式化
new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD'
}).format(price) // "$1,234,567.89"
new Intl.NumberFormat('de-DE', {
style: 'currency',
currency: 'EUR'
}).format(price) // "1.234.567,89 €"
new Intl.NumberFormat('ja-JP', {
style: 'currency',
currency: 'JPY'
}).format(price) // "¥1,234,568" (JPY 无小数位)
// 仪表板紧凑记数法
new Intl.NumberFormat('en-US', {
notation: 'compact',
compactDisplay: 'short'
}).format(1500000) // "1.5M"
用于本地化格式化的 React Hooks
import { useMemo } from 'react'
function useLocaleFormatter(locale: string) {
return useMemo(() => ({
date: (date: Date | string, options?: Intl.DateTimeFormatOptions) =>
new Intl.DateTimeFormat(locale, options).format(new Date(date)),
number: (num: number, options?: Intl.NumberFormatOptions) =>
new Intl.NumberFormat(locale, options).format(num),
currency: (amount: number, currency: string) =>
new Intl.NumberFormat(locale, {
style: 'currency',
currency,
}).format(amount),
relativeTime: (value: number, unit: Intl.RelativeTimeFormatUnit) =>
new Intl.RelativeTimeFormat(locale, { numeric: 'auto' }).format(value, unit),
}), [locale])
}
// 用法
function InvoiceRow({ invoice, locale }: Props) {
const f = useLocaleFormatter(locale)
return (
<tr>
<td>{f.date(invoice.createdAt, { dateStyle: 'medium' })}</td>
<td>{f.currency(invoice.amount, invoice.currency)}</td>
<td>{f.number(invoice.items)}</td>
<td>{f.relativeTime(-invoice.daysOverdue, 'day')}</td>
</tr>
)
}
数据库存储建议
| 数据类型 | 存储格式 | 显示 |
|---|---|---|
| 日期 | ISO 8601 (UTC) | 渲染时格式化 |
| 时间 | ISO 8601 + 时区偏移量 | 转换至用户时区 |
| 价格 | 整数(最小货币单位) | 本地化货币格式化 |
| 百分比 | 浮点数 (0-1) | 本地化百分比格式化 |
| 单位 | 公制/英制分别存储 | 渲染时转换 |
总结
内置的 Intl API 无需额外依赖库即可覆盖 95% 的本地化格式化需求。日期以 ISO 8601 UTC 格式存储,金额以最小货币单位(如分)存储,始终根据用户 locale 在渲染时格式化。测试时至少覆盖美国、欧洲、东亚和中东各一个 locale。
关于 RTL 语言注意事项,请参阅 React 应用中的 RTL 语言支持。多货币计费实现请参阅多货币计费指南。整体 i18n 架构请参阅 SaaS 国际化:架构模式对比。