Localizationi18nDate FormattingCurrencyIntl API

Localizing Date, Time, Number, and Currency Formats

Properly localize dates, times, numbers, and currencies for global SaaS using Intl API, libraries, and database strategies to respect regional conventions.

Sam Rivera
Sam Rivera
June 13, 202612 min read
Also available in:Deutsch · 中文

TL;DR: Date, time, number, and currency formatting varies dramatically by locale — MM/DD vs DD/MM, commas vs periods as decimal separators, 12h vs 24h clocks. Use the Intl API for zero-dependency localization. Patterns from tanstackship.com.

Introduction

Formatting dates, numbers, and currencies according to local conventions is essential for SaaS products targeting global markets. A US user expects "March 5, 2026" while a German user expects "5. März 2026" and a Japanese user expects "2026年3月5日". For guidance on detecting which locale to serve, see our Language Detection Strategies Guide.

Regional Format Differences

LocaleDate FormatTime FormatDecimal SeparatorCurrency Symbol Position
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١٬٢٣٤٫٥٦ ﷼

Using the Intl API

ts
// Date formatting
const date = new Date('2026-06-01')

// US: June 1, 2026
new Intl.DateTimeFormat('en-US', {
  dateStyle: 'long'
}).format(date) // "June 1, 2026"

// German: 1. Juni 2026
new Intl.DateTimeFormat('de-DE', {
  dateStyle: 'long'
}).format(date) // "1. Juni 2026"

// Japanese: 2026年6月1日
new Intl.DateTimeFormat('ja-JP', {
  dateStyle: 'long'
}).format(date) // "2026年6月1日"

// Relative time (Intl.RelativeTimeFormat)
const rtf = new Intl.RelativeTimeFormat('es', { numeric: 'auto' })
rtf.format(-1, 'day')  // "ayer"
rtf.format(-7, 'day')  // "hace 7 días"

Number and Currency Formatting

ts
// Number formatting
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)  // "١٬٢٣٤٬٥٦٧٫٨٩"

// Currency formatting
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 has no decimals)

// Compact notation for dashboards
new Intl.NumberFormat('en-US', {
  notation: 'compact',
  compactDisplay: 'short'
}).format(1500000) // "1.5M"

React Hooks for Localized Formatting

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])
}

// Usage
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>
  )
}

Database Storage Recommendations

Data TypeStorage FormatDisplay
DatesISO 8601 (UTC)Format at render time
TimesISO 8601 + timezone offsetConvert to user's timezone
PricesInteger (cents)Format with locale-aware currency
PercentagesFloat (0-1)Format with locale-aware percentage
UnitsMetric/Imperial stored separatelyConvert at render time

Conclusion

The built-in Intl API handles 95% of localization formatting needs without additional libraries. Store dates in ISO 8601 UTC, amounts in minor currency units (cents), and always format at render time based on the user's locale. Test with at least one locale from each major region (US, Europe, East Asia, Middle East).

For RTL language considerations, see RTL Language Support in React Applications. Multi-currency billing implementation is covered in Multi-Currency Billing Guide. For the overall i18n architecture, refer to i18n for SaaS: Architecture Patterns Compared.