# General Translation Knowledge Base: Как определить язык браузера пользователя?
URL: https://generaltranslation.com/ru/kb/detect-user-language.mdx
Docs index: https://generaltranslation.com/llms.txt
Description: Прочитайте упорядоченный список языков браузера (navigator.languages в JavaScript или заголовок Accept-Language на сервере с сортировкой по q-значению) и сопоставьте его с поддерживаемыми локалями, предусмотрев резервный вариант. Safari и Chrome в режиме инкогнито сообщают только один язык, а геолокация по IP указывает на местоположение, а не на язык.
Category: Интернационализация
Last updated: 2026-10-20
Authors: Kevin Wu, Jackie Chen
Published: 2026-10-20

Ответ уже есть в браузере: это упорядоченный список языков, заданный пользователем в настройках браузера или операционной системы. Остаётся правильно прочитать этот список, сопоставить его с локалями, для которых у вас есть переводы, и понимать, когда стоит отдать приоритет более надёжному сигналу.

## Откуда берутся языковые предпочтения пользователя?

Подсказать, какой язык показывать, могут пять сигналов. Но лишь часть из них действительно говорит о языке.

| Сигнал                      | Где его получить                   | Что он сообщает                            | Надёжность                                                          | Для чего использовать                                             |
| --------------------------- | ---------------------------------- | ------------------------------------------ | ------------------------------------------------------------------- | ----------------------------------------------------------------- |
| `navigator.languages`       | JavaScript в браузере              | Упорядоченный список предпочитаемых языков | Хорошая, но некоторые браузеры отдают только один язык              | Страницы с клиентским рендерингом при первом посещении            |
| Заголовок `Accept-Language` | Каждый HTTP-запрос                 | Тот же список с весами (q-значениями)          | Хорошая, но в поездках пользователь часто не может на него повлиять | Страницы, отрисованные на сервере, при первом посещении           |
| Локаль в URL                | Путь запроса, например `/fr/`      | Версия, которую запросил посетитель        | Высокая                                                             | Публичные страницы, которыми можно поделиться                     |
| Сохранённое предпочтение    | Cookie-файл или настройка аккаунта | Выбор, который пользователь сделал ранее   | Высокая                                                             | Повторные посещения                                               |
| Геолокация по IP            | Сервис геолокации                  | Приблизительное местоположение             | Не сигнал о языке                                                   | Региональные значения по умолчанию (например, валюта), но не язык |

### В браузере: `navigator.language` и `navigator.languages`

[`navigator.languages`](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/languages) возвращает предпочитаемые пользователем языки в виде тегов BCP 47 в порядке убывания приоритета, а [`navigator.language`](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/language) — первый элемент этого массива:

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

Когда пользователь меняет свои настройки, браузер генерирует событие `languagechange` на объекте `window`. Чтобы затруднить фингерпринтинг, Safari всегда сообщает только один язык; так же ведёт себя Chrome в режиме инкогнито. Поэтому учитывайте, что список может оказаться короче полного перечня языковых предпочтений пользователя.

### На сервере: заголовок `Accept-Language`

Браузеры передают эти же предпочтения в каждом запросе. [RFC 9110](https://www.rfc-editor.org/rfc/rfc9110.html#name-accept-language) определяет этот заголовок как список языковых диапазонов через запятую, у каждого из которых может быть необязательный коэффициент качества (`q`) от 0 до 1. Если `q` не указан, он равен 1, а `q=0` означает «неприемлемо». Пример из самого RFC:

```http
Accept-Language: da, en-gb;q=0.8, en;q=0.7
```

Это означает: &quot;Я предпочитаю датский, но приму британский английский и другие варианты английского&quot;. Подстановочный знак `*` [обозначает](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Accept-Language) любой язык, не указанный в списке. Браузеры также добавляют резервные варианты без указания региона: по данным [MDN](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/languages), Chrome и Safari отправляют `en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7` для пользователя, у которого `navigator.languages` равно `["en-US", "zh-CN"]`. Сортируйте записи по `q`: согласно RFC, на порядок их перечисления &quot;полагаться нельзя&quot;.

Заголовок — лишь подсказка. MDN указывает, что сервер &quot;никогда не должен переопределять явный выбор языка пользователем&quot;, и отмечает, что пользователь часто не может управлять этим заголовком — например, в поездке. Если вы кэшируете страницы, выбранные на основе заголовка, добавьте [`Vary: Accept-Language`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Vary), чтобы кэш не отдавал следующему посетителю страницу на языке предыдущего.

### Почему геолокация по IP — неподходящий сигнал

Местоположение — это не язык. Как [отмечает](https://www.w3.org/International/questions/qa-link-lang) W3C, «во многих странах больше одного официального языка», а путешественники, экспаты и пользователи VPN читают на языке, который по их IP-адресу не угадать. Google говорит об этом прямо: «Не используйте анализ IP-адресов для адаптации контента» — в том числе потому, что «краулер Googlebot обычно обращается из США», как указано в его [рекомендациях для многорегиональных сайтов](https://developers.google.com/search/docs/specialty/international/managing-multi-regional-sites). Страница, которая выбирает язык по IP, показывает краулерам одну версию и скрывает остальные. Местоположение по-прежнему можно использовать, чтобы выбрать подходящую валюту или регион доставки по умолчанию, но это решение следует принимать отдельно от выбора языка.

## Сопоставление предпочтений с вашими локалями

Если пользователь запрашивает `pt-PT`, а на сайте есть только `pt-BR`, нужно резервное правило. Существует два алгоритма, и [API `Intl`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl) используют те же названия:

* **Lookup** работает по [RFC 4647](https://www.rfc-editor.org/rfc/rfc4647.html#section-3.4): по очереди перебирает предпочтительные теги, отбрасывая подтеги с конца (`zh-Hant-CN`, затем `zh-Hant`, затем `zh`), пока не найдётся совпадение; если совпадения нет, возвращает значение по умолчанию.
* **Best fit** позволяет реализации вернуть локаль, которая подходит «как минимум так же, а возможно, и лучше», чем результат lookup, например родственный региональный вариант.

Разница проявляется на региональных вариантах. Результаты [`@formatjs/intl-localematcher`](https://www.npmjs.com/package/@formatjs/intl-localematcher) 0.9.0 для `['en', 'fr', 'de', 'pt-BR']` со значением по умолчанию `en`:

| Запрошено     | Lookup | Best fit |
| ------------- | ------ | -------- |
| `pt-PT`       | `en`   | `pt-BR`  |
| `en-GB`       | `en`   | `en`     |
| `fr-CA`, `de` | `fr`   | `fr`     |
| `zh-TW`       | `en`   | `en`     |

Lookup предсказуем, и его поведение легко объяснить. Best fit чаще отдаёт пользователям язык, который они понимают. Но предпочтёт ли читатель из Португалии бразильский португальский английскому — это продуктовое решение, поэтому протестируйте оба алгоритма на своём списке локалей.

## Пример: согласование на сервере

Этот сервер на Node.js сначала проверяет URL, затем сохранённый cookie-файл, а потом заголовок. Для разбора `Accept-Language` и сортировки значений по q-значению он использует [`negotiator`](https://www.npmjs.com/package/negotiator) 1.1.0, а для сопоставления результата по алгоритму best fit — `@formatjs/intl-localematcher`.

```bash
npm install negotiator @formatjs/intl-localematcher
```

```js title="server.mjs"
import { createServer } from 'node:http';
import Negotiator from 'negotiator';
import { match } from '@formatjs/intl-localematcher';

const SUPPORTED = ['en', 'fr', 'de', 'pt-BR'];
const DEFAULT_LOCALE = 'en';

// Отбрасываем "*" и некорректные теги: на них match() выбрасывает RangeError.
function isValidTag(tag) {
  try {
    Intl.getCanonicalLocales(tag);
    return true;
  } catch {
    return false;
  }
}

function pickLocale(req) {
  // 1. Локаль в пути URL, например /fr/pricing
  const { pathname } = new URL(req.url, 'http://localhost');
  const fromPath = pathname.split('/')[1];
  if (SUPPORTED.includes(fromPath)) return { locale: fromPath, source: 'url' };

  // 2. Выбор, сохранённый при предыдущем посещении
  const fromCookie = /(?:^|;\s*)locale=([^;]+)/.exec(req.headers.cookie ?? '')?.[1];
  if (SUPPORTED.includes(fromCookie)) return { locale: fromCookie, source: 'cookie' };

  // 3. Accept-Language: сортируем по q-значению и сопоставляем с вашими локалями
  const requested = new Negotiator(req).languages().filter(isValidTag);
  if (requested.length === 0) return { locale: DEFAULT_LOCALE, source: 'default' };
  return { locale: match(requested, SUPPORTED, DEFAULT_LOCALE), source: 'header' };
}

createServer((req, res) => {
  const { locale, source } = pickLocale(req);
  res.setHeader('Content-Language', locale);
  res.setHeader('Vary', 'Accept-Language, Cookie');
  res.end(`${locale} (from ${source})\n`);
}).listen(3000);
```

Запустите сервер командой `node server.mjs`, а затем отправьте запросы с помощью `curl -H "Accept-Language: ..." localhost:3000/`. Результат в Node.js 24:

```text
da, en-gb;q=0.8, en;q=0.7                      -> en (from header)
pt-PT,pt;q=0.9,en;q=0.8                        -> pt-BR (from header)
fr-CH, fr;q=0.9, en;q=0.8, de;q=0.7, *;q=0.5   -> fr (from header)
ja, de;q=0.5                                   -> de (from header)
en;q=0.5, de                                   -> de (from header)
*                                              -> en (from default)
(no header)                                    -> en (from default)
fr, with cookie locale=de                      -> de (from cookie)
/fr/pricing, with cookie locale=de             -> fr (from url)
```

Фильтр `isValidTag` здесь необходим: без него заголовок со значением `*` заставит `match()` выбросить `RangeError: Incorrect locale information provided`, и обработка запроса завершится аварийно.

## Пример: определение в браузере

На странице с клиентским рендерингом тот же подход можно применить к `navigator.languages`. Эта версия реализует упрощённый алгоритм Lookup из RFC 4647 без внешних зависимостей:

```js title="detect-locale.js"
const SUPPORTED = ['en', 'fr', 'de', 'pt-BR'];
const DEFAULT_LOCALE = 'en';

// Lookup: перебираем предпочтительные теги, отбрасывая подтеги с конца.
function lookup(requested, supported, fallback) {
  const lower = supported.map((tag) => tag.toLowerCase());
  for (const tag of requested) {
    const parts = tag.split('-');
    while (parts.length) {
      const i = lower.indexOf(parts.join('-').toLowerCase());
      if (i !== -1) return supported[i];
      parts.pop();
    }
  }
  return fallback;
}

function preferredLocale(nav) {
  const requested = nav.languages?.length ? nav.languages : [nav.language];
  return lookup(requested, SUPPORTED, DEFAULT_LOCALE);
}

// В браузере: preferredLocale(navigator)
```

Вывод для нескольких примеров списков предпочтений (запуск в Node.js 24 с объектом-заглушкой `navigator`):

```text
["fr-CA","en-US"] -> fr
["pt-BR","pt"]    -> pt-BR
["pt-PT","en"]    -> en
["ja-JP"]         -> en
["de-AT","de"]    -> de
```

Прежде чем вызывать `preferredLocale`, проверьте, нет ли сохранённого выбора, а результат используйте как отправную точку: выполните рендеринг на этом языке и покажите переключатель языка. Не делайте на его основе перенаправление.

## Какой сигнал должен иметь приоритет?

Автоопределение языка — лишь предположение при первом посещении. Если сигналы расходятся, руководствуйтесь следующим порядком:

1. **URL.** Ссылка `/de/`, которой с кем-то поделились, открывает немецкую версию, а выбор языка в переключателе ведёт на URL этого языка.
2. **Сохранённый выбор.** Cookie-файл или настройка учётной записи. Он определяет, куда нейтральный корневой путь перенаправит пользователя или что ему предложить, но никогда не подменяет локализованный URL.
3. **Заголовок `Accept-Language`** или `navigator.languages` на клиенте. Используется, только если сохранённого выбора нет, и только на корневом пути или для подсказки.
4. **Локаль по умолчанию.** Резервный вариант, если ничего не подошло.

W3C рекомендует использовать заголовок как [отправную точку](https://www.w3.org/International/questions/qa-accept-lang-locales), давать пользователям возможность сменить язык и сохранять результат «в базе данных или cookie-файле для последующих посещений». Перенаправления, кэширование и SEO при таком порядке рассматриваются в статье [маршрутизация по локалям и SEO](/kb/locale-routing-seo), а сам переключатель — в статье [создание переключателя языка](/kb/language-switcher).

## Как gt-next, gt-react и gt-node разрешают локаль

В `gt-next` от General Translation функция [`createNextMiddleware`](/docs/react/nextjs/reference/functions/create-next-middleware) разрешает локаль каждого запроса в следующем порядке: локаль, которую посетитель только что выбрал (отслеживается с помощью cookie-файла сброса), локаль из URL (например, `/es/about`), сохранённый cookie-файл локали, cookie-файл с локалью реферера из предыдущего клиентского маршрута, заголовок `Accept-Language` и, наконец, ваша `defaultLocale`. По умолчанию результат сохраняется в cookie-файле `generaltranslation.locale`; его можно переименовать через [`headersAndCookies`](/docs/react/nextjs/config#headers-and-cookies). Чтобы полностью игнорировать этот заголовок, задайте [`ignoreBrowserLocales`](/docs/react/nextjs/config#ignore-browser-locales).

Если запрошенная локаль не поддерживается, библиотеки выбирают наиболее подходящую из поддерживаемых локалей с учётом предпочтений браузера, затем — локаль с тем же языком (`en-GB` для `en-US`) и в последнюю очередь — локаль по умолчанию. Получить результат можно с помощью [`useLocale`](/docs/react/reference/hooks/use-locale), а в асинхронном коде — с помощью [`getLocale`](/docs/react/nextjs/reference/functions/get-locale) из `gt-next/server`.

В одностраничном приложении на React функция [`initializeGTSPA`](/docs/react/reference/config#initialize-spa) из `gt-react` определяет локаль по cookie-файлам и настройкам браузера ещё до первого рендеринга. На сервере Node.js функция [`getRequestLocale`](/docs/node/reference/functions/get-request-locale) из `gt-node` сопоставляет `Accept-Language` с локалями, переданными в [`initializeGT`](/docs/node/reference/functions/initialize-gt). Например, при настроенных `en`, `fr`, `de` и `pt-BR` она вернула `fr` для `fr-CH, fr;q=0.9, en;q=0.8` и `pt-BR` для `pt-PT,pt;q=0.9`. Добавьте перед ней проверку cookie-файла или пути, как показано в [руководстве по определению локали в gt-node](/docs/node/guides/detecting-locale).

## Ключевые выводы

* `navigator.languages` и `Accept-Language` передают одни и те же предпочтения в одном и том же порядке; Safari и режим инкогнито в Chrome сообщают только один язык.
* Разбирайте q-значения (от 0 до 1, по умолчанию 1), сортируйте по ним и до сопоставления отбрасывайте `*` и некорректные теги: `@formatjs/intl-localematcher` выбрасывает исключение на `*`.
* Если у вас есть только `pt-BR`, алгоритм lookup сопоставит `pt-PT` с локалью по умолчанию, а алгоритм best fit — с `pt-BR`. Выбирайте осознанно.
* Разрешайте язык в следующем порядке: URL, сохранённый выбор, `Accept-Language`, значение по умолчанию. IP-геолокация никогда не указывает на язык.
* Если локаль не была только что выбрана, `gt-next` проверяет URL, cookie-файл `generaltranslation.locale`, `Accept-Language` и, наконец, `defaultLocale`.

## Related questions

- [Языковые перенаправления и SEO: URL, cookie-файл или Accept-Language?](https://generaltranslation.com/ru/kb/locale-routing-seo.mdx): Язык каждой страницы, которую вы хотите видеть в индексе, должен определять URL, ведь Googlebot не отправляет заголовок Accept-Language и сканирует сайты в основном с американских адресов. Сохранённый в cookie-файле выбор, а за ним Accept-Language используйте только для того, чтобы решить, куда перенаправлять с нейтрального корневого пути вроде /, причём временным перенаправлением 302 или 307.
- [Переключатель языка: лучшие практики UX, SEO и доступности](https://generaltranslation.com/ru/kb/language-switcher.mdx): Называйте каждый язык на нём самом, например Deutsch или 日本語, и не используйте флаги — они обозначают страны. Каждый вариант должен вести на ту же страницу на выбранном языке через индексируемую ссылку. Запоминайте выбор в cookie-файле и никогда не перенаправляйте автоматически с локализованного URL.
- [Что такое hreflang и как его настроить?](https://generaltranslation.com/ru/kb/what-is-hreflang.mdx): Hreflang сообщает Google, какие URL являются языковыми или региональными версиями одной и той же страницы, чтобы в результатах поиска показывалась нужная. Добавьте его с помощью HTML-тегов link, HTTP-заголовков Link или карты сайта, сделайте все ссылки взаимными и используйте корректные коды, например en-GB, а не en-UK.
- [Как сделать сайт многоязычным?](https://generaltranslation.com/ru/kb/make-website-multilingual.mdx): Выберите, где будут храниться переводы — в прокси, плагине CMS, конструкторе сайтов или в вашем собственном коде, — а затем выделите каждому языку собственный URL, как рекомендует Google. Переводите страницы целиком, включая заголовки и метаописания, связывайте языковые версии с помощью hreflang и предлагайте переключатель языка вместо автоматического перенаправления посетителей.

## Previous and next questions

- Previous question: [Как найти захардкоженные строки, которые не переведены?](https://generaltranslation.com/ru/kb/find-hardcoded-strings.mdx)
- Next question: [Как перевести JSON-файлы i18n?](https://generaltranslation.com/ru/kb/translate-json-files.mdx)

## Sitemap

See the [Knowledge Base sitemap](https://generaltranslation.com/ru/kb/sitemap.md) for every article.
