# General Translation Knowledge Base: ¿Cómo detecto el idioma del navegador de un usuario?
URL: https://generaltranslation.com/es/kb/detect-user-language.mdx
Docs index: https://generaltranslation.com/llms.txt
Description: Lee la lista ordenada de idiomas del navegador (navigator.languages en JavaScript o la cabecera Accept-Language en el servidor), ordenada por valor q, y busca la coincidencia con tus configuraciones regionales compatibles, con una configuración regional de respaldo. Safari y el modo incógnito de Chrome solo indican un idioma, y la geolocalización por IP revela la ubicación, no el idioma.
Category: Internacionalización
Last updated: 2026-10-20
Authors: Kevin Wu, Jackie Chen
Published: 2026-10-20

El navegador ya tiene la respuesta: una lista ordenada de los idiomas que el usuario configuró en el navegador o en el sistema operativo. La tarea consiste en leer bien esa lista, compararla con las configuraciones regionales para las que tienes traducciones y saber cuándo debe prevalecer una señal más fiable.

## ¿De dónde proviene la preferencia de idioma de un usuario?

Hay cinco señales que pueden indicarte qué idioma mostrar. Solo algunas de ellas tienen que ver realmente con el idioma.

| Señal                            | Dónde se lee                         | Qué indica                              | Fiabilidad                                                        | Úsala para                                                            |
| -------------------------------- | ------------------------------------ | --------------------------------------- | ----------------------------------------------------------------- | --------------------------------------------------------------------- |
| `navigator.languages`            | JavaScript del navegador             | Preferencias de idioma ordenadas        | Buena, aunque algunos navegadores solo exponen un idioma          | Páginas renderizadas en el cliente en la primera visita               |
| Cabecera `Accept-Language`       | Cada solicitud HTTP                  | La misma lista, ponderada con valores q | Buena, aunque a menudo escapa al control del usuario cuando viaja | Páginas server-rendered en la primera visita                          |
| Configuración regional en la URL | La ruta de la solicitud, como `/fr/` | La versión que solicitó el visitante    | Alta                                                              | Páginas públicas que se pueden compartir                              |
| Preferencia guardada             | Una cookie o un ajuste de la cuenta  | Una elección previa del usuario         | Alta                                                              | Visitas posteriores                                                   |
| Geolocalización por IP           | Un servicio de búsqueda              | Ubicación aproximada                    | No es una señal de idioma                                         | Valores predeterminados por región, como la moneda, pero no el idioma |

### En el navegador: `navigator.language` y `navigator.languages`

[`navigator.languages`](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/languages) devuelve los idiomas preferidos del usuario como etiquetas BCP 47, ordenadas de mayor a menor preferencia, y [`navigator.language`](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/language) es el primer elemento de esa lista:

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

Cuando el usuario cambia sus preferencias, el navegador dispara un evento `languagechange` en `window`. Para reducir el *fingerprinting*, Safari siempre informa de un único idioma, al igual que Chrome en modo incógnito, así que ten en cuenta que la lista puede ser más corta que las preferencias completas del usuario.

### En el servidor: la cabecera `Accept-Language`

Los navegadores envían estas mismas preferencias en cada solicitud. [RFC 9110](https://www.rfc-editor.org/rfc/rfc9110.html#name-accept-language) define la cabecera como una lista de rangos de idioma separados por comas, cada uno con un valor de calidad (`q`) opcional entre 0 y 1. Si se omite `q`, se asume 1, y `q=0` significa &quot;no aceptable&quot;. Este es el ejemplo que da el propio RFC:

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

Es decir: &quot;Prefiero el danés, pero acepto inglés británico y otras variantes del inglés&quot;. Un comodín `*` [representa](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Accept-Language) cualquier idioma no incluido en la lista. Los navegadores también añaden alternativas de respaldo con solo el idioma: según [MDN](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/languages), Chrome y Safari envían `en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7` para un usuario cuyo `navigator.languages` es `["en-US", "zh-CN"]`. Ordena las entradas por `q`, ya que el RFC indica que &quot;no se puede confiar&quot; en el orden en que aparecen.

La cabecera es solo una pista. MDN afirma que un servidor &quot;nunca debe reemplazar una elección explícita de idioma por parte del usuario&quot; y señala que la cabecera a menudo escapa al control del usuario, por ejemplo, cuando está de viaje. Si almacenas en caché páginas elegidas en función de la cabecera, añade [`Vary: Accept-Language`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Vary) para que la caché no sirva a un visitante el idioma del anterior.

### Por qué la geolocalización por IP es la señal equivocada

Una ubicación no es un idioma. Como [señala](https://www.w3.org/International/questions/qa-link-lang) el W3C, «numerosos países tienen más de un idioma oficial», y los viajeros, los expatriados y los usuarios de VPN leen en un idioma que su dirección IP no permite predecir. Google lo dice sin rodeos: «No uses el análisis de IP para adaptar tu contenido», en parte porque «el rastreador Googlebot suele proceder de EE. UU.», según su [guía sobre sitios multirregionales](https://developers.google.com/search/docs/specialty/international/managing-multi-regional-sites). Una página que elige el idioma según la IP muestra a los rastreadores una sola versión y oculta las demás. La ubicación sigue sirviendo para elegir una moneda predeterminada o una región de envío razonables, pero esa decisión debe mantenerse separada de la del idioma.

## Compara las preferencias con tus configuraciones regionales

Un usuario que solicita `pt-PT` en un sitio que tiene `pt-BR` necesita una regla de contenido alternativo. Existen dos algoritmos, y las [API de `Intl`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl) usan los mismos nombres:

* **Búsqueda** (búsqueda) sigue el [RFC 4647](https://www.rfc-editor.org/rfc/rfc4647.html#section-3.4): prueba cada etiqueta preferida por orden y va eliminando subetiquetas desde el final (`zh-Hant-CN`, luego `zh-Hant`, luego `zh`) hasta que una coincida; si ninguna coincide, devuelve el valor predeterminado.
* **Best fit** (mejor ajuste) permite que la implementación devuelva una configuración regional «al menos igual de adecuada, aunque posiblemente más» que la que devolvería búsqueda; por ejemplo, una variante regional hermana.

La diferencia se aprecia con las variantes regionales. Con [`@formatjs/intl-localematcher`](https://www.npmjs.com/package/@formatjs/intl-localematcher) 0.9.0, frente a `['en', 'fr', 'de', 'pt-BR']` y con `en` como valor predeterminado:

| Solicitado    | Búsqueda | Best fit |
| ------------- | ------ | -------- |
| `pt-PT`       | `en`   | `pt-BR`  |
| `en-GB`       | `en`   | `en`     |
| `fr-CA`, `de` | `fr`   | `fr`     |
| `zh-TW`       | `en`   | `en`     |

Búsqueda es predecible y fácil de explicar. Best fit ofrece a más personas un idioma que pueden leer; si un lector de Portugal prefiere el portugués de Brasil al inglés es una decisión de producto, así que prueba ambos con tu lista de configuraciones regionales.

## Ejemplo: negociar en el servidor

Este servidor de Node.js comprueba primero la URL, después una cookie guardada y, por último, la cabecera. Utiliza [`negotiator`](https://www.npmjs.com/package/negotiator) 1.1.0 para analizar la cabecera `Accept-Language` y ordenar sus valores por valor q, y `@formatjs/intl-localematcher` para buscar la mejor coincidencia para el resultado con el algoritmo best fit.

```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';

// Descarta "*" y las etiquetas mal formadas, ya que con ellas match() lanza un RangeError.
function isValidTag(tag) {
  try {
    Intl.getCanonicalLocales(tag);
    return true;
  } catch {
    return false;
  }
}

function pickLocale(req) {
  // 1. Una configuración regional en la ruta de la URL, como /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. Una preferencia guardada en una visita anterior
  const fromCookie = /(?:^|;\s*)locale=([^;]+)/.exec(req.headers.cookie ?? '')?.[1];
  if (SUPPORTED.includes(fromCookie)) return { locale: fromCookie, source: 'cookie' };

  // 3. Accept-Language, ordenado por valor q y cotejado con tus configuraciones regionales
  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);
```

Ejecútalo con `node server.mjs` y luego envía solicitudes con `curl -H "Accept-Language: ..." localhost:3000/`. Salida en 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)
```

El filtro `isValidTag` es fundamental: sin él, una cabecera que contenga `*` hace que `match()` lance `RangeError: Incorrect locale information provided`, lo que haría fallar la solicitud.

## Ejemplo: detección en el navegador

En una página renderizada en el cliente, puedes aplicar la misma idea a `navigator.languages`. Esta versión implementa una búsqueda sencilla según el RFC 4647, sin dependencias:

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

// Búsqueda: prueba cada etiqueta preferida y elimina subetiquetas desde el final.
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);
}

// En el navegador: preferredLocale(navigator)
```

Salida obtenida para listas de preferencias de ejemplo, al ejecutarse en Node.js 24 con un objeto `navigator` simulado:

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

Comprueba si hay una preferencia guardada antes de llamar a `preferredLocale` y usa el resultado como punto de partida: renderiza en ese idioma y muestra el selector. No redirijas en función de ese resultado.

## ¿Qué señal debe prevalecer?

La detección solo sirve para hacer una suposición en la primera visita. Cuando las señales no coinciden, sigue este orden:

1. **La URL.** Un enlace compartido a `/de/` muestra el contenido en alemán, y al elegir un idioma en el selector se navega a la URL de ese idioma.
2. **La elección guardada.** Una cookie o un ajuste de la cuenta. Determina adónde se redirige a alguien desde una raíz neutral o qué idioma sugerir, pero nunca reescribe una URL localizada.
3. **La cabecera `Accept-Language`**, o `navigator.languages` en el cliente. Solo se usa cuando no hay ninguna elección guardada, y únicamente en la raíz o como sugerencia.
4. **La configuración regional predeterminada.** El valor de respaldo cuando nada coincide.

El W3C recomienda usar la cabecera como [punto de partida](https://www.w3.org/International/questions/qa-accept-lang-locales), permitir que los usuarios cambien el idioma y almacenar el resultado «en una base de datos o en una cookie para visitas posteriores». Las redirecciones, el almacenamiento en caché y el SEO relacionados con este orden se tratan en [enrutamiento por configuración regional y SEO](/kb/locale-routing-seo), y el selector propiamente dicho, en [crear un selector de idioma](/kb/language-switcher).

## Cómo resuelven la configuración regional gt-next, gt-react y gt-node

En `gt-next` de General Translation, [`createNextMiddleware`](/docs/react/nextjs/reference/functions/create-next-middleware) resuelve la configuración regional de cada solicitud en este orden: una configuración regional que el visitante acaba de seleccionar (registrada mediante una cookie de restablecimiento), la configuración regional de la URL (como `/es/about`), la cookie de configuración regional guardada, una cookie de configuración regional de referencia procedente de la ruta de cliente anterior, la cabecera `Accept-Language` y, por último, tu `defaultLocale`. De forma predeterminada, guarda el resultado en la cookie `generaltranslation.locale`, cuyo nombre puede cambiarse mediante [`headersAndCookies`](/docs/react/nextjs/config#headers-and-cookies). Establece [`ignoreBrowserLocales`](/docs/react/nextjs/config#ignore-browser-locales) para ignorar por completo la cabecera.

Cuando la configuración regional solicitada no es compatible, las bibliotecas eligen la mejor coincidencia compatible según las preferencias del navegador; si no la hay, una configuración regional del mismo idioma (`en-GB` para `en-US`) y, en último caso, la predeterminada. Obtén el resultado con [`useLocale`](/docs/react/reference/hooks/use-locale) o, en código asíncrono, con [`getLocale`](/docs/react/nextjs/reference/functions/get-locale) desde `gt-next/server`.

En una aplicación de una sola página de React, [`initializeGTSPA`](/docs/react/reference/config#initialize-spa) de `gt-react` resuelve la configuración regional a partir de las cookies y del navegador antes del primer renderizado. En un servidor Node.js, [`getRequestLocale`](/docs/node/reference/functions/get-request-locale) de `gt-node` negocia `Accept-Language` frente a las configuraciones regionales pasadas a [`initializeGT`](/docs/node/reference/functions/initialize-gt). Con `en`, `fr`, `de` y `pt-BR` configuradas, devolvió `fr` para `fr-CH, fr;q=0.9, en;q=0.8` y `pt-BR` para `pt-PT,pt;q=0.9`. Añade antes una comprobación de cookie o de ruta, como se muestra en la [guía de configuración regional de gt-node](/docs/node/guides/detecting-locale).

## Conclusiones clave

* `navigator.languages` y `Accept-Language` contienen las mismas preferencias ordenadas; Safari y el modo incógnito de Chrome solo exponen un idioma.
* Analiza los valores q (de 0 a 1, con 1 como valor predeterminado), ordena según esos valores y descarta `*` y las etiquetas mal formadas antes de la coincidencia; `@formatjs/intl-localematcher` lanza una excepción con `*`.
* La búsqueda (lookup) asigna `pt-PT` a tu configuración regional predeterminada cuando solo tienes `pt-BR`; best fit lo asigna a `pt-BR`. Elige con criterio.
* Resuelve el idioma en este orden: URL, elección guardada, `Accept-Language` y, por último, el valor predeterminado. La geolocalización por IP nunca es una señal de idioma.
* Si no hay una configuración regional recién seleccionada, `gt-next` comprueba la URL, la cookie `generaltranslation.locale`, `Accept-Language` y, por último, `defaultLocale`.

## Related questions

- [Redirecciones de idioma y SEO: ¿URL, cookie o Accept-Language?](https://generaltranslation.com/es/kb/locale-routing-seo.mdx): Deja que la URL determine el idioma de cada página que quieras indexar, porque Googlebot no envía la cabecera Accept-Language y rastrea sobre todo desde direcciones de EE. UU. Recurre a la preferencia guardada en una cookie y, en su defecto, a Accept-Language solo para decidir adónde redirige una raíz neutral como /, mediante una redirección temporal 302 o 307.
- [Buenas prácticas para el selector de idioma: UX, SEO y accesibilidad](https://generaltranslation.com/es/kb/language-switcher.mdx): Escribe el nombre de cada idioma en ese mismo idioma, como Deutsch o 日本語, y no uses banderas, que representan países. Enlaza cada opción a la misma página en ese idioma mediante un enlace rastreable, guarda la elección en una cookie y nunca redirijas automáticamente una URL localizada.
- [¿Qué es hreflang y cómo se configura?](https://generaltranslation.com/es/kb/what-is-hreflang.mdx): Hreflang le indica a Google qué URL son versiones de la misma página en otros idiomas o regiones, para que pueda mostrar la adecuada en los resultados de búsqueda. Agrégalo con etiquetas link de HTML, cabeceras HTTP Link o un mapa del sitio, asegúrate de que todos los enlaces sean recíprocos y usa códigos válidos como en-GB, no en-UK.
- [¿Cómo hago que un sitio web sea multilingüe?](https://generaltranslation.com/es/kb/make-website-multilingual.mdx): Elige dónde se alojarán las traducciones (en un proxy, un plugin de CMS, un creador de sitios web o tu propio código) y después asigna a cada idioma su propia URL, tal como recomienda Google. Traduce las páginas completas, además de los títulos y las metadescripciones, vincula las versiones con hreflang y ofrece un selector de idioma en lugar de redirigir automáticamente a los visitantes.

## Previous and next questions

- Previous question: [¿Cómo debo nombrar las claves de traducción de i18n?](https://generaltranslation.com/es/kb/i18n-key-naming.mdx)
- Next question: [¿Cómo doy soporte a idiomas RTL (right-to-left) en CSS?](https://generaltranslation.com/es/kb/right-to-left-css.mdx)

## Sitemap

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