Front-end Engineering Lab
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-15

Intl.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

  1. Store in UTC: Always store dates in UTC/ISO format
  2. Display in User's Timezone: Use Intl with timeZone
  3. Use Intl API: Don't manually format dates
  4. Show Timezone: For scheduled events
  5. Support Calendars: Consider non-Gregorian calendars
  6. Relative Time: "2 hours ago" for recent events
  7. 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.

On this page