IntlPull
Technical
13 min read

Date, Time & Number Formatting Across Locales: Developer Guide

Master locale-aware formatting with Intl API. Learn DateTimeFormat, NumberFormat, currency formatting, timezone handling, and library comparisons.

IntlPull Team
IntlPull Team
Feb 12, 2026
On this page
Summary

Master locale-aware formatting with Intl API. Learn DateTimeFormat, NumberFormat, currency formatting, timezone handling, and library comparisons.

Understanding Locale-Aware Formatting

Formatting dates, times, and numbers correctly across locales is essential for creating truly international applications, yet it's one of the most error-prone aspects of i18n. The JavaScript Intl API provides built-in locale-aware formatting that handles the vast complexity of global conventions—from month-before-day versus day-before-month date formats to comma-versus-period decimal separators and varying currency symbol placements. Different locales use different calendar systems (Gregorian, Persian, Islamic), number grouping separators (1,000 vs 1 000 vs 1.000), decimal marks (3.14 vs 3,14), and date component ordering (MM/DD/YYYY vs DD.MM.YYYY vs YYYY-MM-DD). The Intl.DateTimeFormat API handles date and time formatting with timezone support, while Intl.NumberFormat covers numbers, percentages, currencies, and units. Intl.RelativeTimeFormat provides human-friendly "3 days ago" strings, and Intl.ListFormat handles comma/conjunction lists that vary by language ("A, B, and C" vs "A, B und C"). Understanding these APIs eliminates the need for heavyweight libraries while ensuring your application respects user locale preferences automatically. Proper formatting signals attention to detail and cultural awareness, transforming a generic app into one that feels native to users worldwide.

Intl.DateTimeFormat Deep Dive

The Intl.DateTimeFormat API formats dates and times according to locale conventions:

JavaScript
1const date = new Date('2026-02-12T15:30:00Z');
2
3// Basic formatting
4new Intl.DateTimeFormat('en-US').format(date);
5// "2/12/2026"
6
7new Intl.DateTimeFormat('en-GB').format(date);
8// "12/02/2026"
9
10new Intl.DateTimeFormat('de-DE').format(date);
11// "12.2.2026"
12
13new Intl.DateTimeFormat('ja-JP').format(date);
14// "2026/2/12"
15
16new Intl.DateTimeFormat('ar-SA').format(date);
17// "١٢‏/٢‏/٢٠٢٦" (Eastern Arabic numerals)

Custom Date Formatting Options

JavaScript
1const date = new Date('2026-02-12T15:30:00Z');
2
3// Full date and time
4new Intl.DateTimeFormat('en-US', {
5  dateStyle: 'full',
6  timeStyle: 'long',
7  timeZone: 'America/New_York'
8}).format(date);
9// "Thursday, February 12, 2026 at 10:30:00 AM EST"
10
11// Custom components
12new Intl.DateTimeFormat('en-US', {
13  weekday: 'long',
14  year: 'numeric',
15  month: 'long',
16  day: 'numeric'
17}).format(date);
18// "Thursday, February 12, 2026"
19
20// 24-hour time format (German)
21new Intl.DateTimeFormat('de-DE', {
22  hour: '2-digit',
23  minute: '2-digit',
24  hour12: false
25}).format(date);
26// "15:30"
27
28// 12-hour time format (US)
29new Intl.DateTimeFormat('en-US', {
30  hour: 'numeric',
31  minute: '2-digit',
32  hour12: true
33}).format(date);
34// "3:30 PM"

Timezone Handling

JavaScript
1const date = new Date('2026-02-12T15:30:00Z');
2
3// Same moment, different timezones
4new Intl.DateTimeFormat('en-US', {
5  timeStyle: 'long',
6  timeZone: 'UTC'
7}).format(date);
8// "3:30:00 PM UTC"
9
10new Intl.DateTimeFormat('en-US', {
11  timeStyle: 'long',
12  timeZone: 'America/New_York'
13}).format(date);
14// "10:30:00 AM EST"
15
16new Intl.DateTimeFormat('en-US', {
17  timeStyle: 'long',
18  timeZone: 'Asia/Tokyo'
19}).format(date);
20// "12:30:00 AM JST"
21
22// Display timezone name
23new Intl.DateTimeFormat('en-US', {
24  timeZoneName: 'long',
25  timeZone: 'America/New_York'
26}).format(date);
27// "2/12/2026, Eastern Standard Time"

Date Format Options Reference

OptionValuesExample Output
weekday"narrow", "short", "long""T", "Thu", "Thursday"
year"numeric", "2-digit""2026", "26"
month"numeric", "2-digit", "narrow", "short", "long""2", "02", "F", "Feb", "February"
day"numeric", "2-digit""12", "12"
hour"numeric", "2-digit""3", "03"
minute"numeric", "2-digit""30", "30"
second"numeric", "2-digit""0", "00"
timeZoneName"short", "long""EST", "Eastern Standard Time"
hour12true, false"3:30 PM", "15:30"

Intl.NumberFormat Deep Dive

The Intl.NumberFormat API handles numbers, percentages, and units:

JavaScript
1const number = 1234567.89;
2
3// Basic number formatting
4new Intl.NumberFormat('en-US').format(number);
5// "1,234,567.89"
6
7new Intl.NumberFormat('de-DE').format(number);
8// "1.234.567,89"
9
10new Intl.NumberFormat('fr-FR').format(number);
11// "1 234 567,89"
12
13new Intl.NumberFormat('ar-SA').format(number);
14// "١٬٢٣٤٬٥٦٧٫٨٩"

Currency Formatting

JavaScript
1const amount = 1234.56;
2
3// US Dollar
4new Intl.NumberFormat('en-US', {
5  style: 'currency',
6  currency: 'USD'
7}).format(amount);
8// "$1,234.56"
9
10// Euro (German locale)
11new Intl.NumberFormat('de-DE', {
12  style: 'currency',
13  currency: 'EUR'
14}).format(amount);
15// "1.234,56 €"
16
17// Japanese Yen (no decimals)
18new Intl.NumberFormat('ja-JP', {
19  style: 'currency',
20  currency: 'JPY'
21}).format(amount);
22// "¥1,235"
23
24// British Pound (accounting format)
25new Intl.NumberFormat('en-GB', {
26  style: 'currency',
27  currency: 'GBP',
28  currencySign: 'accounting'
29}).format(-1234.56);
30// "-£1,234.56"

Percentage Formatting

JavaScript
1const percentage = 0.856;
2
3new Intl.NumberFormat('en-US', {
4  style: 'percent'
5}).format(percentage);
6// "86%"
7
8new Intl.NumberFormat('en-US', {
9  style: 'percent',
10  minimumFractionDigits: 1,
11  maximumFractionDigits: 1
12}).format(percentage);
13// "85.6%"
14
15new Intl.NumberFormat('de-DE', {
16  style: 'percent',
17  minimumFractionDigits: 2
18}).format(percentage);
19// "85,60 %"

Unit Formatting

JavaScript
1const distance = 1234;
2
3// Kilometers
4new Intl.NumberFormat('en-US', {
5  style: 'unit',
6  unit: 'kilometer',
7  unitDisplay: 'long'
8}).format(distance);
9// "1,234 kilometers"
10
11// Megabytes
12new Intl.NumberFormat('en-US', {
13  style: 'unit',
14  unit: 'megabyte',
15  unitDisplay: 'short'
16}).format(500);
17// "500 MB"
18
19// Temperature
20new Intl.NumberFormat('en-US', {
21  style: 'unit',
22  unit: 'celsius'
23}).format(25);
24// "25°C"
25
26// Speed
27new Intl.NumberFormat('en-US', {
28  style: 'unit',
29  unit: 'kilometer-per-hour',
30  unitDisplay: 'narrow'
31}).format(120);
32// "120km/h"

Compact Notation

JavaScript
1const bigNumber = 1234567890;
2
3new Intl.NumberFormat('en-US', {
4  notation: 'compact',
5  compactDisplay: 'short'
6}).format(bigNumber);
7// "1.2B"
8
9new Intl.NumberFormat('en-US', {
10  notation: 'compact',
11  compactDisplay: 'long'
12}).format(bigNumber);
13// "1.2 billion"
14
15new Intl.NumberFormat('de-DE', {
16  notation: 'compact'
17}).format(bigNumber);
18// "1,2 Mrd."

Intl.RelativeTimeFormat

Format relative time strings ("3 days ago", "in 2 hours"):

JavaScript
1const rtf = new Intl.RelativeTimeFormat('en', { numeric: 'auto' });
2
3rtf.format(-1, 'day');    // "yesterday"
4rtf.format(0, 'day');     // "today"
5rtf.format(1, 'day');     // "tomorrow"
6rtf.format(-3, 'day');    // "3 days ago"
7rtf.format(2, 'week');    // "in 2 weeks"
8
9// Always numeric
10const rtfNumeric = new Intl.RelativeTimeFormat('en', { numeric: 'always' });
11rtfNumeric.format(-1, 'day');  // "1 day ago"
12rtfNumeric.format(0, 'day');   // "in 0 days"
13
14// German
15const rtfDe = new Intl.RelativeTimeFormat('de', { numeric: 'auto' });
16rtfDe.format(-1, 'day');   // "gestern"
17rtfDe.format(-3, 'day');   // "vor 3 Tagen"
18rtfDe.format(2, 'hour');   // "in 2 Stunden"

Building a Smart Relative Time Function

TypeScript
1function getRelativeTime(date: Date, locale: string = 'en'): string {
2  const now = new Date();
3  const diffMs = date.getTime() - now.getTime();
4  const diffSec = Math.floor(diffMs / 1000);
5  const diffMin = Math.floor(diffSec / 60);
6  const diffHour = Math.floor(diffMin / 60);
7  const diffDay = Math.floor(diffHour / 24);
8
9  const rtf = new Intl.RelativeTimeFormat(locale, { numeric: 'auto' });
10
11  if (Math.abs(diffSec) < 60) {
12    return rtf.format(diffSec, 'second');
13  } else if (Math.abs(diffMin) < 60) {
14    return rtf.format(diffMin, 'minute');
15  } else if (Math.abs(diffHour) < 24) {
16    return rtf.format(diffHour, 'hour');
17  } else if (Math.abs(diffDay) < 30) {
18    return rtf.format(diffDay, 'day');
19  } else {
20    // Fall back to absolute date for older dates
21    return new Intl.DateTimeFormat(locale).format(date);
22  }
23}
24
25// Usage
26const pastDate = new Date(Date.now() - 3600000); // 1 hour ago
27console.log(getRelativeTime(pastDate));  // "1 hour ago"
28
29const futureDate = new Date(Date.now() + 86400000 * 2); // 2 days from now
30console.log(getRelativeTime(futureDate)); // "in 2 days"

Intl.ListFormat

Format lists with locale-appropriate conjunctions:

JavaScript
1const items = ['Apple', 'Banana', 'Orange'];
2
3// English (conjunction)
4new Intl.ListFormat('en', { style: 'long', type: 'conjunction' })
5  .format(items);
6// "Apple, Banana, and Orange"
7
8// English (disjunction)
9new Intl.ListFormat('en', { style: 'long', type: 'disjunction' })
10  .format(items);
11// "Apple, Banana, or Orange"
12
13// German
14new Intl.ListFormat('de', { style: 'long', type: 'conjunction' })
15  .format(items);
16// "Apple, Banana und Orange"
17
18// Spanish
19new Intl.ListFormat('es', { style: 'long', type: 'conjunction' })
20  .format(items);
21// "Apple, Banana y Orange"
22
23// Short style
24new Intl.ListFormat('en', { style: 'short' })
25  .format(items);
26// "Apple, Banana, Orange"

Calendar Systems

Different cultures use different calendar systems:

JavaScript
1const date = new Date('2026-02-12');
2
3// Gregorian (default)
4new Intl.DateTimeFormat('en-US', {
5  calendar: 'gregory',
6  dateStyle: 'full'
7}).format(date);
8// "Thursday, February 12, 2026"
9
10// Islamic (Hijri) calendar
11new Intl.DateTimeFormat('ar-SA', {
12  calendar: 'islamic',
13  dateStyle: 'full'
14}).format(date);
15// "الخميس، ٢٠ شعبان ١٤٤٧ هـ"
16
17// Persian calendar
18new Intl.DateTimeFormat('fa-IR', {
19  calendar: 'persian',
20  dateStyle: 'full'
21}).format(date);
22// "پنج‌شنبه ۲۳ بهمن ۱۴۰۴"
23
24// Japanese calendar (Reiwa era)
25new Intl.DateTimeFormat('ja-JP', {
26  calendar: 'japanese',
27  dateStyle: 'full'
28}).format(date);
29// "令和8年2月12日木曜日"

Common Formatting Pitfalls

Pitfall 1: Month-First vs Day-First Confusion

JavaScript
1// ❌ DANGER: Ambiguous date format
2const dateString = "02/12/2026"; // Feb 12 or Dec 2?
3
4// ✅ GOOD: Use ISO 8601 for storage/transmission
5const isoDate = "2026-02-12";
6const date = new Date(isoDate);
7
8// ✅ GOOD: Use Intl API for display
9new Intl.DateTimeFormat(userLocale).format(date);

Pitfall 2: Hardcoded Number Separators

JavaScript
1// ❌ BAD: Hardcoded formatting
2const price = "$" + (1234.56).toFixed(2);
3// "$1234.56" (missing thousands separator)
4
5// ✅ GOOD: Locale-aware formatting
6new Intl.NumberFormat('en-US', {
7  style: 'currency',
8  currency: 'USD'
9}).format(1234.56);
10// "$1,234.56"

Pitfall 3: Timezone Confusion

JavaScript
1// ❌ BAD: Using user's local timezone for server data
2const serverDate = new Date("2026-02-12T15:30:00"); // Ambiguous!
3
4// ✅ GOOD: Always use UTC for server communication
5const serverDateUTC = new Date("2026-02-12T15:30:00Z");
6
7// ✅ GOOD: Display in user's timezone
8new Intl.DateTimeFormat('en-US', {
9  timeStyle: 'long',
10  timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone
11}).format(serverDateUTC);

Library Comparison: date-fns vs Day.js vs Luxon

Featuredate-fnsDay.jsLuxon
Size17KB (modular)2KB23KB
Immutability✅ Yes✅ Yes✅ Yes
i18n Support✅ 100+ locales✅ 80+ locales✅ Built-in Intl
Timezone❌ Requires plugin❌ Requires plugin✅ Native
Relative Time✅ formatDistanceToNow✅ .from()✅ .toRelative()
Duration❌ Limited✅ Yes✅ Yes
Intl API❌ No❌ No✅ Yes

date-fns Example

JavaScript
1import { format, formatDistanceToNow } from 'date-fns';
2import { de } from 'date-fns/locale';
3
4const date = new Date('2026-02-12T15:30:00');
5
6format(date, 'PPP', { locale: de });
7// "12. Februar 2026"
8
9formatDistanceToNow(date, { addSuffix: true, locale: de });
10// "in etwa 1 Jahr"

Day.js Example

JavaScript
1import dayjs from 'dayjs';
2import 'dayjs/locale/de';
3import relativeTime from 'dayjs/plugin/relativeTime';
4
5dayjs.extend(relativeTime);
6dayjs.locale('de');
7
8const date = dayjs('2026-02-12');
9
10date.format('LL');
11// "12. Februar 2026"
12
13date.fromNow();
14// "in einem Jahr"

Luxon Example

JavaScript
1import { DateTime } from 'luxon';
2
3const date = DateTime.fromISO('2026-02-12T15:30:00', { zone: 'utc' });
4
5date.setLocale('de').toLocaleString(DateTime.DATE_FULL);
6// "12. Februar 2026"
7
8date.setLocale('de').toRelative();
9// "in 1 Jahr"

Server-Side Considerations

Node.js Intl Support

Node.js includes full Intl support by default (since v13):

JavaScript
1// Server-side formatting
2function formatCurrency(amount, currency, locale) {
3  return new Intl.NumberFormat(locale, {
4    style: 'currency',
5    currency
6  }).format(amount);
7}
8
9// API endpoint example
10app.get('/api/price', (req, res) => {
11  const price = 1234.56;
12  const locale = req.headers['accept-language'] || 'en-US';
13
14  res.json({
15    amount: price,
16    formatted: formatCurrency(price, 'USD', locale)
17  });
18});

Database Storage Best Practices

SQL
1-- ✅ GOOD: Store dates in UTC
2CREATE TABLE orders (
3  id SERIAL PRIMARY KEY,
4  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
5  amount DECIMAL(10,2),
6  currency CHAR(3)
7);
8
9-- ❌ BAD: Storing formatted strings
10CREATE TABLE orders_bad (
11  created_at VARCHAR(50), -- "$1,234.56" <- Unsortable, un-queryable
12  amount VARCHAR(50)
13);

IntlPull's Format Preview

IntlPull provides real-time format previews for dates, numbers, and currencies:

  • Date Preview: See how dates render in all target locales
  • Number Formatting: Preview currency amounts across locales
  • Character Limits: Detect overflow from longer formatted values (e.g., "February" vs "Feb")
  • Validation: Warn when placeholder formats don't match expected patterns

Example: A translation Order placed on {date} shows previews for each locale, ensuring German "12. Februar 2026" doesn't overflow UI constraints designed for English "Feb 12, 2026".

FAQ

Q: Should I format dates on the client or server? A: Format on the client when possible to respect user's locale preferences. Server can send ISO 8601 strings, and client formats with user's locale.

Q: How do I handle users in different timezones? A: Store all dates in UTC. Display in user's timezone using Intl.DateTimeFormat with their timezone. Let users select their timezone in settings.

Q: Do I need a date library or is Intl API enough? A: Intl API covers most formatting needs. Use libraries for date manipulation (adding days, parsing), not just formatting.

Q: How do I format currencies for multiple countries? A: Use Intl.NumberFormat with the target locale and currency code. Never hardcode currency symbols or decimal separators.

Q: What's the best way to handle fractional currencies? A: Use Intl.NumberFormat which knows JPY has 0 decimals, USD has 2, and BHD has 3. It handles this automatically.

Q: How do I test formatting across locales? A: Write tests that check formatting output for representative locales (en-US, de-DE, ja-JP, ar-SA). Use snapshot testing for regression detection.

Q: Should I show relative time or absolute dates? A: Relative for recent activity (<7 days). Absolute for older dates. Consider showing both: "3 days ago (Feb 9, 2026)".

Tags
formatting
date
time
number
currency
intl-api
locales
i18n
IntlPull Team
IntlPull Team
Engineering

Building tools to help teams ship products globally. Follow us for more insights on localization and i18n.