---
title: "How do I detect the user's preferred language in the browser?"
description: "Read `navigator.languages`, the user's ordered list of BCP 47 tags (`navigator.language` is its first entry), or the `Accept-Language` header on the server. Match that list against your supported locales with a language-only fallback, and let the user override the result."
category: general
date: 2026-10-09
canonical: https://localizely.com/i18n-questions/general/how-do-i-detect-the-users-preferred-language-in-the-browser/
---

# How do I detect the user's preferred language in the browser?

In the browser, [`navigator.languages`](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/languages) is the user's ordered preference list as BCP 47 tags, most preferred first, and `navigator.language` is its first entry. On the server the same list arrives in the [`Accept-Language`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Accept-Language) request header with q-values, for example `fr-CH, fr;q=0.9, en;q=0.8, *;q=0.5`; the default weight is 1 and `*` matches anything else.

```js
navigator.languages; // ["en-US", "zh-CN", "ja-JP"]
navigator.language;  // "en-US"
```

Do not use `Intl.DateTimeFormat().resolvedOptions().locale` for this. It returns the locale the formatter actually uses, which with no arguments is the runtime's default locale (for example `nl-BE`), so it reflects system formatting settings rather than the language the user wants to read. And do not guess from IP geolocation: someone travelling through Spain still wants the language configured in their browser, and Google explicitly advises against redirecting users based on what you think their language is.

The preference list is only useful once matched against the locales you ship. Try each preferred tag exactly, then with likely subtags filled in (so `zh-TW` matches `zh-Hant`), then the language alone (so `de-AT` matches `de`):

```js
const supported = ["en", "de", "pt-BR", "zh-Hans", "zh-Hant"];

function pickLocale(preferred = navigator.languages, fallback = "en") {
  const lookup = new Map(supported.map((tag) => [tag.toLowerCase(), tag]));
  for (const tag of preferred) {
    const { language, script, region } = new Intl.Locale(tag).maximize();
    const candidates = [
      tag,
      `${language}-${script}-${region}`,
      `${language}-${region}`,
      `${language}-${script}`,
      language,
    ];
    for (const candidate of candidates) {
      const match = lookup.get(candidate.toLowerCase());
      if (match) return match;
    }
  }
  return fallback;
}
```

`Intl.Locale` has been available in all major browsers since 2020. Compare case-insensitively, because old Safari versions reported `en-us`, and keep the loop order: a user who lists `["de-CH", "en"]` should get `de`, not `en`. On the server, parse `Accept-Language` into tags sorted by weight and feed them to the same function, guarding against malformed tags, which make `Intl.Locale` throw.

Treat detection as a default, not a decision. Show a language switcher, store the choice (cookie, local storage or the user profile) and read it before the browser list on later visits; the `languagechange` event tells you when the browser settings change. Where to persist the choice is covered in [Where to save preferred language in React app?](/i18n-questions/react/where-to-save-preferred-language-in-react-app/).

Source: https://localizely.com/i18n-questions/general/how-do-i-detect-the-users-preferred-language-in-the-browser/
