Localizing Date, Time, Number, and Currency: 全球化 SaaS 日期、时间、数字和货币本地化指南

全球化 SaaS 如何通过 Intl API、框架工具和数据库策略尊重各地区习惯,实现日期、时间、数字和货币的正确本地化。

Sam Rivera
Sam Rivera
June 1, 202612 min read
其他语言:Deutsch · English

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-USMM/DD/YYYY12h (3:00 PM)1,234.56$1,234.56
en-GBDD/MM/YYYY24h (15:00)1,234.56£1,234.56
de-DEDD.MM.YYYY24h (15:00)1.234,561.234,56 €
zh-CNYYYY/MM/DD24h (15:00)1,234.56¥1,234.56
ja-JPYYYY年MM月DD日24h (15:00)1,234.56¥1,234.56
fr-FRDD/MM/YYYY24h (15:00)1 234,561 234,56 €
ar-SADD/MM/YYYY12h (٣:٠٠ م)1,234.56١٬٢٣٤٫٥٦ ﷼

使用 Intl API

ts
// 日期格式化
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"

数字和货币格式化

ts
// 数字格式化
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

tsx
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 国际化:架构模式对比。