Language negotiation

How your app decides which language to serve: browser Accept-Language, a saved choice, or the URL, matched against the locales you actually ship.

auto-detecting the visitor's languageAccept-Language headerhow the site knows I speak Frenchbrowser language detectionpicking the language automaticallylanguage auto-switch on first visitcontent negotiation for language

See it

Live demo coming soon

What it is

The browser sends a ranked wish list in the Accept-Language header (something like 'fr-CA, fr;q=0.9, en;q=0.7'), and your job is to match it against the locales you actually ship. Do not string-compare: use proper BCP 47 matching (lookup or best-fit) via something like @formatjs/intl-localematcher or Negotiator, so fr-CA lands on fr rather than falling to English.

The precedence order that survives contact with users: a locale already in the URL wins, full stop. The URL is what people bookmark, share, and link, so a saved cookie must never quietly rewrite it. Negotiation only runs on an unlocalized entry point, and there the order is an explicit stored choice, then Accept-Language, then your default. Negotiate once, redirect to a real locale URL, and remember the answer.

Gotchas: never hard-redirect on IP country, because a German speaker in Tokyo gets trapped in Japanese and crawlers get pinned to whichever country their datacenter sits in. Never serve two languages from the same URL based on headers alone; if you must, send Vary: Accept-Language or your CDN will cache one language for everyone. And always leave a visible language switcher, because detection is a guess and users need an override.

Ask AI for it

Add language negotiation middleware to this app. If the requested URL already contains a supported locale, render it as-is and skip negotiation entirely: a cookie must never replace an explicit locale in the URL. For an unlocalized path, resolve the locale in this precedence order: the saved explicit preference cookie, then the Accept-Language header matched against my supported locales with proper BCP 47 best-fit matching, then a configured default. Redirect to the matching locale URL with a 307, persist the resolved choice in the cookie, and set 'Vary: Accept-Language' on any negotiated response. Never redirect based on IP geolocation.

You might have meant

locale routinglanguage switcherlocale fallback chaincontent negotiationlocale