# Fix the toLocaleString hydration mismatch

> A toLocaleString hydration mismatch happens when the server formats with its locale and the browser with the visitor's. Pass an explicit locale to both.

Source: https://hydration.jscrate.dev/docs/causes/locale
Last updated: 2026-09-18

A toLocaleString hydration mismatch happens when a number or date is formatted
without an explicit locale. The server formats with its own default (often
`en-US`), the browser with the visitor's language, and "1,234.5" becomes
"1.234,5" during hydration. Pass the same locale, from the URL or a cookie, to
the formatter on both sides.

## Symptoms

```text
Hydration failed because the server rendered text didn't match the client.
Text content does not match server-rendered HTML.
Warning: Text content did not match. Server: "Total: 1,234,567.891" Client: "Total: 1.234.567,891"
Minified React error #418; visit https://react.dev/errors/418
```

A toLocaleTimeString hydration error looks the same with a time: "5:00:00 PM" on
the server, "17:00:00" in the browser. React 19 lists this cause itself in the
error message: "Date formatting in a user's locale which doesn't match the
server."

hydration-proof reports it as [HP1001](https://hydration.jscrate.dev/docs/issues/hp1001) with the cause
**Locale-dependent formatting**. It recognizes the same digits with different
separators, and in the package's own test suite names this cause with 98%
confidence.

## Why a toLocaleString hydration mismatch happens

Every locale-sensitive API falls back to the runtime's default locale when you
leave the locale out. On the server that is whatever the container sets (`LANG`,
`LC_ALL`, or the Node.js build's default). In the browser it is the visitor's
language. They rarely agree.

These use the default locale:

- `value.toLocaleString()`, `toLocaleDateString()` and `toLocaleTimeString()`
- `a.localeCompare(b)` without its second argument
- `Intl.NumberFormat`, `Intl.DateTimeFormat`, `Intl.RelativeTimeFormat`,
  `Intl.PluralRules`, `Intl.Collator`, `Intl.ListFormat` and
  `Intl.DisplayNames` without a locale, or with `undefined`
- `Intl.NumberFormat().resolvedOptions().locale`, which returns the default itself

Dates have a second problem on top of this one: the time zone. A date formatted
with an explicit locale can still differ by the zone, covered in
[timezone differences](https://hydration.jscrate.dev/docs/causes/timezone).

## How to fix it

### Pass the locale from the request

The server knows the locale from the URL (`/de/pricing`), a cookie or the
`Accept-Language` header. Pass it down, and use it on both sides:

```tsx title="app/[locale]/pricing/page.tsx"
import { Price } from "./price";

export default async function Page({
  params,
}: {
  params: Promise<{ locale: string }>;
}) {
  const { locale } = await params;
  return <Price amount={1234.5} locale={locale} />;
}
```

```tsx title="app/[locale]/pricing/price.tsx"
"use client";

export function Price({ amount, locale }: { amount: number; locale: string }) {
  // Before: amount.toLocaleString() used each runtime's default
  const text = amount.toLocaleString(locale, {
    style: "currency",
    currency: "EUR",
  });
  return <p>Total: {text}</p>;
}
```

### Pin one locale where it does not matter

An admin tool, a log viewer or a fixed-market site can use one locale
everywhere:

```tsx title="percent.tsx"
const percent = new Intl.NumberFormat("en-US", { style: "percent" });

export function Percent({ value }: { value: number }) {
  return <span>{percent.format(value)}</span>;
}
```

The same goes for comparisons: `a.localeCompare(b, "en")`. A list sorted with
`localeCompare` and no locale can come out in a different order on each side,
which the [`require-deterministic-list-order`](https://hydration.jscrate.dev/docs/rules/require-deterministic-list-order)
rule reports.

### Fix an i18n hydration error in next-intl

A next-intl hydration error almost always means the server and the client
provider disagree about the locale, the time zone or "now". Configure all three
once, in the request config:

```ts title="src/i18n/request.ts"
import { getRequestConfig } from "next-intl/server";

export default getRequestConfig(async () => ({
  locale: "en",
  // One zone for both sides. Read it from the user's profile or a cookie if it varies.
  timeZone: "Europe/Berlin",
  // One "now" for both sides, used by relative time formatting.
  now: new Date(),
}));
```

A `NextIntlClientProvider` rendered by a Server Component inherits the locale,
messages, time zone, `now` and formats from that config, so Client Components
format exactly like the server did. If you render the provider from a Client
Component instead, pass `locale` and `timeZone` to it yourself. next-intl
warns with `ENVIRONMENT_FALLBACK` when no time zone is configured; treat that
warning as this bug. The next-intl guide to
[reliable date formatting](https://next-intl.dev/blog/date-formatting-nextjs)
covers the details.

### Format after mount when only the browser knows

If the page must use the browser's own locale and the server cannot know it,
render a neutral value first and format in an effect. See
[useEffect and two-pass rendering](https://hydration.jscrate.dev/docs/guides/useeffect-two-pass-rendering).

## Catch it with ESLint

[`no-locale-without-explicit-locale`](https://hydration.jscrate.dev/docs/rules/no-locale-without-explicit-locale)
reports every call above when the locale is missing, `undefined` or `[]`. A
variable is accepted (`toLocaleString(locale)`), because passing the locale
from the server is the fix. The rule suggests the locale from its
`defaultLocale` option (`en-US` by default) but never inserts it on its own:
the right locale is a product decision.

```bash
npm install -D eslint-plugin-hydration-proof
```

## Catch it in CI

The default scenario uses the machine's locale, and a server on the same
machine renders with the same one. Add the locales your visitors use:

```ts title="hydration-proof.config.ts"
import { defineConfig } from "hydration-proof";

export default defineConfig({
  matrix: {
    locale: ["en-US", "de-DE", "ar-EG"],
  },
});
```

A scenario's `locale` also sets the `Accept-Language` header, so a server that
negotiates the locale sees the same one. Findings are HP1001, with the locale
that separates failing runs from passing ones. With `--probe`, hydration-proof
reloads the page with only the browser locale changed; if the value follows
it, the locale is the proven cause. See [probes](https://hydration.jscrate.dev/docs/probes).

```bash
npx hydration-proof test --probe
```

## Related

- [Timezone hydration mismatches](https://hydration.jscrate.dev/docs/causes/timezone)
- [Date and time values in render](https://hydration.jscrate.dev/docs/causes/time)
- [The no-locale-without-explicit-locale rule](https://hydration.jscrate.dev/docs/rules/no-locale-without-explicit-locale)
- [Text content does not match server-rendered HTML](https://hydration.jscrate.dev/docs/errors/text-content-does-not-match-server-rendered-html)
- [Testing locales with the environment matrix](https://hydration.jscrate.dev/docs/environment-matrix)
