PatternsInternationalization
Date/Time Handling
Properly format dates and times for different locales using the Intl API
Date/Time Handling
Dates and times vary dramatically across cultures. The Intl.DateTimeFormat API provides locale-aware formatting without external libraries.
The Problem
// ❌ BAD: Manual date formatting
const date = new Date('2024-01-15');
const formatted = `${date.getMonth() + 1}/${date.getDate()}/${date.getFullYear()}`;
// US: 1/15/2024
// But users expect:
// UK: 15/1/2024
// Japan: 2024/1/15
// ISO: 2024-01-15Intl.DateTimeFormat
Basic Usage
const date = new Date('2024-01-15T14:30:00');
// US English
new Intl.DateTimeFormat('en-US').format(date);
// "1/15/2024"
// British English
new Intl.DateTimeFormat('en-GB').format(date);
// "15/01/2024"
// Japanese
new Intl.DateTimeFormat('ja-JP').format(date);
// "2024/1/15"
// Arabic (Saudi Arabia)
new Intl.DateTimeFormat('ar-SA').format(date);
// "١٥/١/٢٠٢٤" (Arabic numerals)
// Persian
new Intl.DateTimeFormat('fa-IR').format(date);
// "۱۴۰۲/۱۰/۲۵" (Persian calendar!)Custom Formats
// Long date
new Intl.DateTimeFormat('en-US', {
dateStyle: 'long'
}).format(date);
// "January 15, 2024"
new Intl.DateTimeFormat('pt-BR', {
dateStyle: 'long'
}).format(date);
// "15 de janeiro de 2024"
// Full date with day of week
new Intl.DateTimeFormat('en-US', {
dateStyle: 'full'
}).format(date);
// "Monday, January 15, 2024"
// Custom format
new Intl.DateTimeFormat('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
weekday: 'long'
}).format(date);
// "Monday, January 15, 2024"Time Formatting
const datetime = new Date('2024-01-15T14:30:00');
// 12-hour (US)
new Intl.DateTimeFormat('en-US', {
hour: 'numeric',
minute: 'numeric',
hour12: true
}).format(datetime);
// "2:30 PM"
// 24-hour (most of world)
new Intl.DateTimeFormat('en-GB', {
hour: 'numeric',
minute: 'numeric',
hour12: false
}).format(datetime);
// "14:30"
// With seconds
new Intl.DateTimeFormat('en-US', {
timeStyle: 'medium'
}).format(datetime);
// "2:30:00 PM"Date + Time
// Combined
new Intl.DateTimeFormat('en-US', {
dateStyle: 'medium',
timeStyle: 'short'
}).format(datetime);
// "Jan 15, 2024, 2:30 PM"
new Intl.DateTimeFormat('pt-BR', {
dateStyle: 'medium',
timeStyle: 'short'
}).format(datetime);
// "15 de jan. de 2024 14:30"Time Zones
// Show time in user's timezone
new Intl.DateTimeFormat('en-US', {
dateStyle: 'short',
timeStyle: 'short',
timeZone: 'America/New_York'
}).format(datetime);
// "1/15/24, 2:30 PM" (EST)
new Intl.DateTimeFormat('en-US', {
dateStyle: 'short',
timeStyle: 'short',
timeZone: 'Asia/Tokyo'
}).format(datetime);
// "1/16/24, 4:30 AM" (JST - next day!)
// Show timezone name
new Intl.DateTimeFormat('en-US', {
dateStyle: 'short',
timeStyle: 'short',
timeZone: 'America/New_York',
timeZoneName: 'short'
}).format(datetime);
// "1/15/24, 2:30 PM EST"Relative Time
// Intl.RelativeTimeFormat
const rtf = new Intl.RelativeTimeFormat('en', { numeric: 'auto' });
rtf.format(-1, 'day'); // "yesterday"
rtf.format(0, 'day'); // "today"
rtf.format(1, 'day'); // "tomorrow"
rtf.format(-7, 'day'); // "7 days ago"
rtf.format(2, 'week'); // "in 2 weeks"
rtf.format(-3, 'month'); // "3 months ago"
// Portuguese
const rtfPt = new Intl.RelativeTimeFormat('pt-BR', { numeric: 'auto' });
rtfPt.format(-1, 'day'); // "ontem"
rtfPt.format(1, 'day'); // "amanhã"
rtfPt.format(-7, 'day'); // "há 7 dias"Smart Relative Time
function getRelativeTime(date: Date, locale: string): string {
const now = Date.now();
const diff = date.getTime() - now;
const seconds = Math.round(diff / 1000);
const minutes = Math.round(seconds / 60);
const hours = Math.round(minutes / 60);
const days = Math.round(hours / 24);
const weeks = Math.round(days / 7);
const months = Math.round(days / 30);
const years = Math.round(days / 365);
const rtf = new Intl.RelativeTimeFormat(locale, { numeric: 'auto' });
if (Math.abs(seconds) < 60) {
return rtf.format(seconds, 'second');
} else if (Math.abs(minutes) < 60) {
return rtf.format(minutes, 'minute');
} else if (Math.abs(hours) < 24) {
return rtf.format(hours, 'hour');
} else if (Math.abs(days) < 7) {
return rtf.format(days, 'day');
} else if (Math.abs(weeks) < 4) {
return rtf.format(weeks, 'week');
} else if (Math.abs(months) < 12) {
return rtf.format(months, 'month');
} else {
return rtf.format(years, 'year');
}
}
// Usage
getRelativeTime(new Date(Date.now() - 30000), 'en');
// "30 seconds ago"
getRelativeTime(new Date(Date.now() + 3600000), 'pt-BR');
// "em 1 hora"React Components
Date Formatter
// components/FormattedDate.tsx
import { useLocale } from 'next-intl';
interface Props {
date: Date | string;
format?: 'short' | 'medium' | 'long' | 'full';
}
export function FormattedDate({ date, format = 'medium' }: Props) {
const locale = useLocale();
const dateObj = typeof date === 'string' ? new Date(date) : date;
const formatted = new Intl.DateTimeFormat(locale, {
dateStyle: format,
}).format(dateObj);
return <time dateTime={dateObj.toISOString()}>{formatted}</time>;
}
// Usage
<FormattedDate date={new Date()} format="long" />Relative Time Component
'use client';
import { useState, useEffect } from 'react';
import { useLocale } from 'next-intl';
export function RelativeTime({ date }: { date: Date }) {
const locale = useLocale();
const [text, setText] = useState(() => getRelativeTime(date, locale));
useEffect(() => {
// Update every minute
const interval = setInterval(() => {
setText(getRelativeTime(date, locale));
}, 60000);
return () => clearInterval(interval);
}, [date, locale]);
return (
<time dateTime={date.toISOString()} title={date.toLocaleString(locale)}>
{text}
</time>
);
}Calendar Systems
Different cultures use different calendars:
- Gregorian: Most of the world
- Islamic/Hijri: Saudi Arabia, many Muslim countries
- Persian: Iran, Afghanistan
- Hebrew: Israel
- Buddhist: Thailand
// Islamic calendar
new Intl.DateTimeFormat('ar-SA-u-ca-islamic', {
dateStyle: 'long'
}).format(new Date('2024-01-15'));
// "٣ رجب ١٤٤٥ هـ"
// Persian calendar
new Intl.DateTimeFormat('fa-IR-u-ca-persian', {
dateStyle: 'long'
}).format(new Date('2024-01-15'));
// "۲۵ دی ۱۴۰۲ ه.ش."
// Buddhist calendar
new Intl.DateTimeFormat('th-TH-u-ca-buddhist', {
dateStyle: 'long'
}).format(new Date('2024-01-15'));
// "15 มกราคม 2567" (year is 543 years ahead)Input Fields
// Date input with locale-aware format
function DateInput() {
const locale = useLocale();
const [date, setDate] = useState<Date | null>(null);
const formatForInput = (date: Date): string => {
return date.toISOString().split('T')[0]; // YYYY-MM-DD
};
const formatForDisplay = (date: Date): string => {
return new Intl.DateTimeFormat(locale, {
dateStyle: 'medium'
}).format(date);
};
return (
<div>
<input
type="date"
value={date ? formatForInput(date) : ''}
onChange={(e) => setDate(new Date(e.target.value))}
/>
{date && (
<p>Selected: {formatForDisplay(date)}</p>
)}
</div>
);
}Parsing Dates
// ❌ BAD: Assumes MM/DD/YYYY
function parseDate(str: string): Date {
const [month, day, year] = str.split('/');
return new Date(+year, +month - 1, +day);
}
// ✅ GOOD: Use ISO format for parsing
function parseDate(isoString: string): Date {
return new Date(isoString);
}
// Store dates as ISO 8601
const isoDate = date.toISOString(); // "2024-01-15T14:30:00.000Z"
// Display with Intl
const displayed = new Intl.DateTimeFormat(locale).format(new Date(isoDate));Best Practices
- Store in UTC: Always store dates in UTC/ISO format
- Display in User's Timezone: Use Intl with timeZone
- Use Intl API: Don't manually format dates
- Show Timezone: For scheduled events
- Support Calendars: Consider non-Gregorian calendars
- Relative Time: "2 hours ago" for recent events
- Accessibility: Use
<time>element with datetime attribute
Common Pitfalls
❌ Using Date constructor with string: Unpredictable parsing
✅ Use ISO 8601 format
❌ Hardcoding format: MM/DD/YYYY doesn't work globally
✅ Use Intl.DateTimeFormat
❌ Ignoring timezones: "Meeting at 3 PM" - which timezone?
✅ Always specify timezone
❌ Assuming Gregorian calendar: Not universal
✅ Support local calendars
Libraries (If Needed)
While Intl API covers most cases, these libraries help with complex scenarios:
- date-fns: Comprehensive date utilities (+ date-fns-tz)
- dayjs: Lightweight alternative to Moment (+ plugins)
- Luxon: Modern, timezone-aware
- Temporal: Future JavaScript API (Stage 3)
// Example with date-fns
import { formatDistance, format } from 'date-fns';
import { ptBR, ar } from 'date-fns/locale';
formatDistance(new Date(), new Date(2024, 0, 15), {
locale: ptBR,
addSuffix: true
});
// "há 3 dias"Proper date/time handling is critical for global apps—always use locale-aware formatting.