Skip to main content
Beta: Front-End Checklist is currently in beta. Some issues are still being fixed. Thanks for your patience.
I18nMedium

Use Intl APIs for currency, number, and date formatting

Format monetary values, numbers, and dates using the browser's built-in Intl.NumberFormat and Intl.DateTimeFormat APIs instead of manual string manipulation.

Utilities
Quick take
Typical fix time 20 min
  • Never hardcode currency symbols or thousand separators like $ or comma
  • Use Intl.NumberFormat with the currency style for monetary values
  • Use Intl.DateTimeFormat for locale-aware date and time display
  • Pass the user locale explicitly rather than relying on the browser default
  • Use Intl.Collator and locale fallback chains for sorting and searching
Why it matters: Number and currency formatting rules differ significantly across locales — a value formatted as "$1,234.56" in the US is written "1.234,56 $" in Germany and "¥1,235" in Japan. Hardcoded formatting causes incorrect display for international users and is expensive to maintain manually as you add locales.

Rule Details

The Intl namespace provides a family of locale-aware formatting constructors built into every modern browser and Node.js. They handle the enormous variation in how different locales represent numbers, currencies, dates, and percentages — no third-party library required.

Code Example

The Intl.NumberFormat constructor accepts a style: 'currency' option along with a currency code. The formatter handles symbol placement, decimal digits, and thousands grouping automatically for each locale:

// formatCurrency.ts
 
/**
 * Format a numeric amount as a locale-aware currency string.
 * @param amount   - The numeric value (e.g. 1234.5)
 * @param currency - ISO 4217 currency code (e.g. 'USD', 'EUR', 'JPY')
 * @param locale   - BCP 47 language tag (e.g. 'en-US', 'de-DE', 'ja-JP')
 */
export function formatCurrency(
  amount: number,
  currency: string,
  locale: string
): string {
  return new Intl.NumberFormat(locale, {
    style: 'currency',
    currency,
    // Optional: control how many fraction digits to display
    // JPY has no minor units, so maximumFractionDigits defaults to 0
  }).format(amount);
}
 
// Output comparison for the same value across locales
const amount = 1234.5;
 
formatCurrency(amount, 'USD', 'en-US'); // "$1,234.50"
formatCurrency(amount, 'EUR', 'de-DE'); // "1.234,50 €"
formatCurrency(amount, 'JPY', 'ja-JP'); // "¥1,235"
formatCurrency(amount, 'GBP', 'en-GB'); // "£1,234.50"
formatCurrency(amount, 'CHF', 'fr-CH'); // "CHF 1'234.50"

Why It Matters

Number and currency formatting rules differ significantly across locales — a value formatted as "$1,234.56" in the US is written "1.234,56 $" in Germany and "¥1,235" in Japan. Hardcoded formatting causes incorrect display for international users and is expensive to maintain manually as you add locales.

Reusing Formatter Instances

Creating a new Intl.NumberFormat instance on every render is wasteful. Cache the instance per locale and currency combination:

const formatterCache = new Map<string, Intl.NumberFormat>();
 
export function getCurrencyFormatter(
  currency: string,
  locale: string
): Intl.NumberFormat {
  const key = `${locale}-${currency}`;
  if (!formatterCache.has(key)) {
    formatterCache.set(
      key,
      new Intl.NumberFormat(locale, { style: 'currency', currency })
    );
  }
  return formatterCache.get(key)!;
}
 
// Usage in a React component
import { useLocale } from '@/hooks/useLocale'; // your app's locale hook
 
function PriceDisplay({ amount, currency }: { amount: number; currency: string }) {
  const locale = useLocale();
  const formatted = getCurrencyFormatter(currency, locale).format(amount);
  return <span>{formatted}</span>;
}

General Number Formatting

Use Intl.NumberFormat for any numeric value — percentages, large counts, units:

// Percentage
new Intl.NumberFormat('en-US', { style: 'percent' }).format(0.742); // "74%"
new Intl.NumberFormat('de-DE', { style: 'percent' }).format(0.742); // "74 %"
 
// Compact notation for large numbers
new Intl.NumberFormat('en-US', { notation: 'compact' }).format(1_500_000); // "1.5M"
new Intl.NumberFormat('ja-JP', { notation: 'compact' }).format(1_500_000); // "150万"
 
// Unit formatting (metres, kilograms, etc.)
new Intl.NumberFormat('en-US', {
  style: 'unit',
  unit: 'kilometer',
  unitDisplay: 'long',
}).format(42); // "42 kilometers"

Date and Time Formatting

Intl.DateTimeFormat handles locale-specific date and time patterns:

const date = new Date('2025-03-11T14:30:00Z');
 
// Short date
new Intl.DateTimeFormat('en-US').format(date); // "3/11/2025"
new Intl.DateTimeFormat('de-DE').format(date); // "11.3.2025"
new Intl.DateTimeFormat('ja-JP').format(date); // "2025/3/11"
 
// Long date with time
new Intl.DateTimeFormat('en-GB', {
  dateStyle: 'long',
  timeStyle: 'short',
}).format(date); // "11 March 2025 at 14:30"
 
// Relative time ("3 days ago")
const rtf = new Intl.RelativeTimeFormat('en', { numeric: 'auto' });
rtf.format(-3, 'day'); // "3 days ago"
rtf.format(1, 'day');  // "tomorrow"

Locale-Aware Sorting and Fallback Locales

Formatting is only part of localization. Sorting and searching should respect locale collation rules, and your app should fall back gracefully when the exact locale is unavailable:

const requestedLocales = ['fr-CA', 'fr', 'en']
const resolvedLocale =
  Intl.NumberFormat.supportedLocalesOf(requestedLocales)[0] ?? 'en'
 
const collator = new Intl.Collator(resolvedLocale, {
  sensitivity: 'base',
  numeric: true,
})
 
const products = ['eclair', 'Éclair', 'eclair 2', 'eclair 10']
products.sort(collator.compare)
Always pass the locale explicitly

Relying on Intl.NumberFormat() without a locale argument uses the runtime's default locale, which will be the server locale during SSR and the user's browser locale on the client — causing a hydration mismatch in Next.js and similar frameworks. Always pass the locale string explicitly.

Anti-Patterns to Avoid

// ❌ Hardcoded symbol and separator — breaks for non-US locales
const price = `$${(amount).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ',')}`;
 
// ❌ Using toLocaleString() without an explicit locale
const price = amount.toLocaleString(); // different on server vs client
 
// ✅ Explicit locale from user preferences or URL segment
const price = new Intl.NumberFormat(userLocale, {
  style: 'currency',
  currency: userCurrency,
}).format(amount);

Standards

  • Use these references as the standard for the rendered internationalization behavior, not just the source strings or config.
  • Check the implementation against MDN: Intl.NumberFormat before treating the rule as satisfied.
  • Check the implementation against MDN: Intl.DateTimeFormat before treating the rule as satisfied.

Verification

  1. Search the codebase for toFixed, regex-based comma insertion, and hardcoded $, , or £ symbols — replace with Intl.NumberFormat.
  2. Render price components in a Storybook story with locale='de-DE' and locale='ja-JP' to confirm formatting changes correctly.
  3. Check that no Intl.NumberFormat() or Intl.DateTimeFormat() call is missing an explicit locale argument to prevent SSR/client hydration mismatches.
  4. Verify that formatter instances are cached per locale to avoid performance regressions in list views with many formatted values.
  5. Search for .sort() on localized strings and confirm locale-aware ordering uses Intl.Collator with a defined fallback locale chain.

Use with AI

Copy these prompts to use with your AI assistant, or install the MCP server to use directly from Claude, Cursor, or Windsurf.

Check

Verify implementation

Identify all places in this codebase where numbers, currencies, or dates are formatted manually instead of using Intl.NumberFormat or Intl.DateTimeFormat. Also flag locale-sensitive sorting that uses plain string comparison instead of Intl.Collator and any formatter that lacks a fallback locale.

Fix

Auto-fix issues

Replace manual number and currency formatting with Intl.NumberFormat and replace manual date formatting with Intl.DateTimeFormat, passing the user locale explicitly. Use Intl.Collator for locale-aware sorting and resolve a supported fallback locale when the requested locale is unavailable.

Explain

Learn more

Explain why locale-aware formatting with Intl APIs is required for international applications, what goes wrong when formatting is hardcoded, and why sorting/searching should also use locale-aware comparison.

Review

Code review

Review utility functions and components that render numbers, prices, percentages, or dates. Flag any hardcoded currency symbols, separators, or date format strings, and show Intl-based alternatives.

Sources

References used to support the guidance in this rule.

Further Reading

Tools and supplementary material for exploring the topic in more depth.

Intl Explorerintl-explorer.comTool

Rules that often go hand-in-hand with this one.

Design UI components to accommodate text expansion from translation

Ensure that layouts use flexible sizing so that translated text — which can be 30–50% longer than English — does not overflow, clip, or break the UI.

I18n
Handle plural forms with Intl.PluralRules or ICU MessageFormat

Select the correct grammatical plural category for every language using Intl.PluralRules or an ICU-aware i18n library instead of simple singular/plural branching.

I18n
Use semantic input type attributes

Set the correct type attribute on input elements to trigger the right mobile keyboard, enable browser validation, and improve autofill accuracy.

HTML
Use locale-neutral images and provide cultural overrides when needed

Default to abstract, culture-neutral icons and illustrations, and supply locale-specific image variants only when visual content carries meaning that differs across regions.

I18n

Was this rule helpful?

Your feedback helps improve rule quality. This stays internal for now.

Loading feedback...
0 / 385