How to format numbers for different locales in React Router v7

Display numbers with locale-specific separators

Problem

Numbers are written differently around the world. What appears as 10,000.5 in the United States becomes 10.000,5 in Germany—commas and periods swap roles entirely. This is not a matter of preference or style, but of legibility. A German user seeing 10,000.5 might read it as ten, ignoring the grouping separators. An American user seeing 10.000,5 might read it as ten thousand, ignoring the decimal separator.

The same digits can convey opposite meanings depending on the reader's regional conventions. When applications display raw numeric values without locale-aware formatting, they risk confusing users and undermining trust in the data presented.

Solution

Format numbers based on the user's locale, using regional rules for decimal and grouping separators. This turns numeric values into strings that follow the formatting rules familiar to users in their region.

React-intl provides formatting methods that leverage the browser's built-in Intl.NumberFormat API, which handles the complexity of regional number conventions. By passing numeric values through these formatters, the application produces output that matches user expectations without manual separator logic.

Steps

1. Create a component that formats numbers with useIntl

The useIntl hook provides access to formatting methods including formatNumber, which accepts a numeric value and returns a locale-formatted string.

import { useIntl } from "react-intl";

export default function ProductPrice() {
  const intl = useIntl();
  const price = 1234.56;

  return (
    <div>
      <p>Price: {intl.formatNumber(price)}</p>
    </div>
  );
}

The formatNumber method automatically applies the correct separators for the current locale. A user with locale en-US sees "1,234.56" while a user with locale de-DE sees "1.234,56".

2. Format currency values with style options

The formatNumber method accepts options conforming to Intl.NumberFormatOptions, including currency formatting.

import { useIntl } from "react-intl";

export default function ProductPrice() {
  const intl = useIntl();
  const price = 1234.56;

  return (
    <div>
      <p>
        {intl.formatNumber(price, {
          style: "currency",
          currency: "USD",
        })}
      </p>
    </div>
  );
}

This produces "$1,234.56" for en-US and "1.234,56 $" for de-DE, applying both separator and currency symbol conventions.

3. Use FormattedNumber for declarative formatting

The FormattedNumber component provides a declarative alternative that accepts the same options as props.

import { FormattedNumber } from "react-intl";

export default function Statistics() {
  const totalUsers = 1500000;
  const growthRate = 0.23;

  return (
    <div>
      <p>
        Total users: <FormattedNumber value={totalUsers} />
      </p>
      <p>
        Growth rate: <FormattedNumber value={growthRate} style="percent" />
      </p>
    </div>
  );
}

The component renders the formatted number directly into the DOM. For en-US, this displays "1,500,000" and "23%". For de-DE, it displays "1.500.000" and "23 %".

4. Format numbers from loader data

In React Router 7, route components access loader data via the useLoaderData hook. Combine this with number formatting to display server-provided values.

import { useLoaderData } from "react-router";
import { FormattedNumber } from "react-intl";

export async function loader() {
  return {
    revenue: 45678.9,
    units: 12500,
  };
}

export default function Dashboard() {
  const { revenue, units } = useLoaderData<typeof loader>();

  return (
    <div>
      <h1>Dashboard</h1>
      <p>
        Revenue:{" "}
        <FormattedNumber value={revenue} style="currency" currency="USD" />
      </p>
      <p>
        Units sold: <FormattedNumber value={units} />
      </p>
    </div>
  );
}

The loader provides raw numeric data, and the component formats it according to the user's locale. This separation keeps data fetching independent of presentation concerns.