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
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/418A 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 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()andtoLocaleTimeString()a.localeCompare(b)without its second argumentIntl.NumberFormat,Intl.DateTimeFormat,Intl.RelativeTimeFormat,Intl.PluralRules,Intl.Collator,Intl.ListFormatandIntl.DisplayNameswithout a locale, or withundefinedIntl.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.
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:
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} />;
}"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:
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
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:
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
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.
Catch it with ESLint
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.
npm install -D eslint-plugin-hydration-proofCatch 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:
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.
npx hydration-proof test --probe