# General Translation Knowledge Base: Buenas prácticas para el selector de idioma: UX, SEO y accesibilidad
URL: https://generaltranslation.com/es/kb/language-switcher.mdx
Docs index: https://generaltranslation.com/llms.txt
Description: 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.
Category: Internacionalización
Last updated: 2026-10-20
Authors: Kevin Wu, Jackie Chen
Published: 2026-10-20

Quien más necesita tu selector de idioma es precisamente quien no puede leer la página a la que ha llegado. Por eso, todas las decisiones que verás a continuación siguen una sola regla: el control debe entenderlo alguien que no lee tu idioma predeterminado.

## ¿Qué hace que un selector de idioma sea bueno?

### 1. Muestra cada idioma con su propio nombre

Un lector japonés que visita una página en inglés busca 日本語, no &quot;Japanese&quot;, y un lector alemán busca &quot;Deutsch&quot;. La [guía del W3C sobre menús de idiomas](https://www.w3.org/International/questions/qa-navigation-select) recomienda escribir &quot;français&quot; en lugar de &quot;French&quot;. Estos nombres que cada idioma se da a sí mismo se denominan endónimos, y [`Intl.DisplayNames`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DisplayNames) de JavaScript los genera a partir del código de configuración regional:

```js
const locales = ['en', 'fr', 'de', 'ja', 'ar', 'pt-BR', 'zh-Hant'];

for (const code of locales) {
  // Nombra cada idioma en su propia lengua (su endónimo)
  const own = new Intl.DisplayNames([code], { type: 'language' }).of(code);
  // El mismo nombre en inglés, para comparar
  const english = new Intl.DisplayNames(['en'], { type: 'language' }).of(code);
  console.log(`${code}: ${own} (${english})`);
}
```

Salida en Node.js 24 (ICU 78.3, CLDR 48.0):

```text
en: English (English)
fr: français (French)
de: Deutsch (German)
ja: 日本語 (Japanese)
ar: العربية (Arabic)
pt-BR: português (Brasil) (Brazilian Portuguese)
zh-Hant: 繁體中文 (Traditional Chinese)
```

El francés y el portugués no escriben con mayúscula inicial los nombres de idiomas en mitad de una oración, así que CLDR devuelve &quot;français&quot;. En un menú, donde cada nombre aparece por separado, escribe la primera letra en mayúscula. Añade la región solo cuando ofrezcas dos variantes del mismo idioma, como &quot;Português (Brasil)&quot; junto a &quot;Português (Portugal)&quot;.

### 2. No uses banderas para representar idiomas

Las [buenas prácticas para declarar el idioma](https://www.w3.org/TR/i18n-html-tech-lang/) del W3C lo dicen sin rodeos: «No uses iconos de banderas para indicar idiomas». Y explican [por qué](https://www.w3.org/International/questions/qa-link-lang): «Las banderas representan países, no idiomas». Un mismo idioma se habla en muchos países (el español en España, México y Argentina), un país puede tener varios idiomas oficiales (Suiza) y una bandera puede tener connotaciones nacionales que incomoden a otros hablantes del mismo idioma. Las banderas sí tienen sentido en un selector de país o de tienda, donde lo que se elige es la región; en ese caso, acompáñalas con los nombres de los idiomas disponibles en cada una.

### 3. Colócalo donde miran los usuarios, con un activador reconocible

El W3C [recomienda](https://www.w3.org/International/questions/qa-navigation-select) colocar el control «en la parte superior de la página o cerca de ella» y, como una etiqueta de texto depende del idioma, acompañarla de una imagen reconocible. Todavía no hay ningún símbolo reconocido universalmente, aunque el W3C señala que los iconos de globo terráqueo son cada vez más populares. Un icono de globo junto al nombre del idioma actual funciona para lectores de cualquier idioma. Muchos sitios repiten el selector en el pie de página como segundo punto de entrada, pero evita que el pie de página sea el único.

### 4. Cambia a la misma página, no a la página de inicio

Si alguien está leyendo tu página de precios en inglés y cambia a francés, lo que quiere es ver la página de precios en francés. Si lo envías a la página de inicio en francés, pierde el punto en el que estaba y tiene que volver a navegar en un idioma que quizá solo ahora es capaz de leer. Enlaza cada opción a la versión traducida de la URL actual y, si una página todavía no está traducida, enlaza a la página superior traducida más cercana e indícalo.

### 5. Usa enlaces rastreables con `hreflang` y `lang`

Por lo general, Google solo puede [rastrear un enlace si es un elemento `<a>` con un `href`](https://developers.google.com/search/docs/crawling-indexing/links-crawlable), y pide a los sitios multilingües que [añadan hipervínculos a las versiones en otros idiomas](https://developers.google.com/search/docs/specialty/international/managing-multi-regional-sites) para que los usuarios puedan elegir una con un clic. Un `<select>` cuyo controlador de cambio asigna `window.location` no es un enlace, así que no ofrece a los rastreadores nada que seguir.

Añade dos atributos a cada enlace. El atributo `hreflang` en `<a>` [indica el idioma de la página enlazada](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/a); no tiene ningún comportamiento integrado en el navegador y es independiente de las anotaciones `<link rel="alternate" hreflang>` que leen los motores de búsqueda (consulta [Qué es hreflang](/kb/what-is-hreflang)). El atributo `lang` marca el idioma del texto del enlace, de modo que un lector de pantalla pueda pronunciar «Deutsch» con las reglas del alemán. El criterio [Idioma de las partes](https://www.w3.org/WAI/WCAG22/Understanding/language-of-parts.html) de las WCAG pone como ejemplo los enlaces de idioma escritos en su propio idioma. También señala una limitación: cuando una tecnología de asistencia anuncia un enlace solo por su nombre accesible, como en una lista de los enlaces de la página, no cambia la pronunciación.

### 6. Recuerda la elección explícita

Cuando un usuario elija un idioma, guarda esa preferencia en una cookie o en su cuenta y úsala en visitas posteriores, tanto para decidir el destino desde una raíz neutral como `/` como para mostrar la opción actual en el selector. No permitas que la cookie reemplace una URL localizada que el usuario haya abierto. El W3C [recomienda](https://www.w3.org/International/questions/qa-accept-lang-locales) «almacenar los resultados en una base de datos o en una cookie para visitas posteriores», y [MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Accept-Language) señala que un servidor «nunca debería reemplazar una elección explícita de idioma hecha por el usuario».

### 7. No cambies el idioma automáticamente en cada visita

Redirigir según el idioma del navegador en cada solicitud deja atrapados a quienes están de viaje, a quienes usan computadoras compartidas y a cualquiera que haya abierto un enlace en un idioma concreto. Usa la preferencia del navegador únicamente para mostrar una sugerencia que se pueda descartar (&quot;Diese Seite gibt es auch auf Deutsch&quot;) y deja que el usuario decida. [Enrutamiento por configuración regional y SEO](/kb/locale-routing-seo) explica qué señal debe prevalecer y el único caso en que una redirección es segura.

### 8. Hazlo accesible con el teclado y los lectores de pantalla

Una lista de enlaces simples, o un `<select>` nativo, funciona de serie con el teclado y las tecnologías de asistencia. Si creas un menú desplegable, sigue el [patrón de divulgación](https://www.w3.org/WAI/ARIA/apg/patterns/disclosure/) (disclosure) de WAI-ARIA: un `<button>` muestra u oculta la lista, las teclas Intro y Espacio la abren y la cierran, y `aria-expanded` indica su estado. Asigna al control un nombre accesible en el idioma de la página (`aria-label="Sprache"` en una página en alemán) y marca el idioma actual con `aria-current="page"`.

## ¿Cómo se crea un selector de idioma?

### HTML puro

Este menú aplica las ocho prácticas: endónimos, un icono de globo en lugar de banderas, enlaces a la misma página, `hreflang` y `lang` en cada enlace, un botón desplegable y una cookie que guarda la elección explícita del usuario.

```html title="On /de/pricing"
<nav aria-label="Sprache">
  <button type="button" aria-expanded="false" aria-controls="language-menu">
    <svg aria-hidden="true" width="16" height="16"><!-- icono de globo --></svg>
    Deutsch
  </button>
  <ul id="language-menu" hidden>
    <li><a href="/en/pricing" hreflang="en" lang="en">English</a></li>
    <li><a href="/fr/pricing" hreflang="fr" lang="fr">Français</a></li>
    <li><a href="/de/pricing" hreflang="de" lang="de" aria-current="page">Deutsch</a></li>
    <li><a href="/ja/pricing" hreflang="ja" lang="ja">日本語</a></li>
  </ul>
</nav>
```

```js title="switcher.js"
const button = document.querySelector('[aria-controls="language-menu"]');
const menu = document.getElementById('language-menu');

// Abre y cierra el menú (Enter y Espacio activan un <button> de forma nativa)
button.addEventListener('click', () => {
  const open = button.getAttribute('aria-expanded') === 'true';
  button.setAttribute('aria-expanded', String(!open));
  menu.hidden = open;
});

// Escape cierra el desplegable y devuelve el foco a su botón.
button.closest('nav').addEventListener('keydown', (event) => {
  if (event.key === 'Escape' && !menu.hidden) {
    menu.hidden = true;
    button.setAttribute('aria-expanded', 'false');
    button.focus();
  }
});

// Guarda la elección explícita para futuras visitas
menu.addEventListener('click', (event) => {
  const link = event.target.closest('a[hreflang]');
  if (link) {
    document.cookie = `locale=${link.hreflang}; path=/; max-age=31536000; samesite=lax`;
  }
});
```

### React

Este componente usa enlaces simples. Pasa la ruta actual, la cadena de consulta y el fragmento como `currentHref`, junto con la configuración regional actual. Da por hecho que cada configuración regional tiene un prefijo de URL, que todas las páginas existen en cada una de las configuraciones regionales indicadas y que los slugs son idénticos en todos los idiomas. Si usas slugs traducidos o faltan páginas, resuelve cada destino a partir de los datos de tus rutas o de tu CMS en lugar de reemplazar un prefijo.

```jsx title="LanguageSwitcher.jsx"
const LOCALES = ['en', 'fr', 'de', 'ja'];

// Conserva la ruta, la barra final, la consulta y el fragmento al cambiar la configuración regional.
function hrefForLocale(currentHref, locale) {
  const url = new URL(currentHref, 'https://example.invalid');
  const trailingSlash = url.pathname.endsWith('/');
  const segments = url.pathname.split('/').filter(Boolean);
  if (LOCALES.includes(segments[0])) segments.shift();
  url.pathname = `/${[locale, ...segments].join('/')}${trailingSlash ? '/' : ''}`;
  return `${url.pathname}${url.search}${url.hash}`;
}

// Nombre del idioma en su propio idioma, con mayúscula inicial para mostrarlo en un menú
function endonym(locale) {
  const name = new Intl.DisplayNames([locale], { type: 'language' }).of(locale);
  return name.charAt(0).toLocaleUpperCase(locale) + name.slice(1);
}

function rememberChoice(locale) {
  document.cookie = `locale=${locale}; path=/; max-age=31536000; samesite=lax`;
}

export function LanguageSwitcher({ currentHref, currentLocale, label }) {
  return (
    <nav aria-label={label}>
      <ul>
        {LOCALES.map((locale) => (
          <li key={locale}>
            <a
              href={hrefForLocale(currentHref, locale)}
              hrefLang={locale}
              lang={locale}
              aria-current={locale === currentLocale ? 'page' : undefined}
              onClick={() => rememberChoice(locale)}
            >
              {endonym(locale)}
            </a>
          </li>
        ))}
      </ul>
    </nav>
  );
}
```

Con `currentHref="/fr/pricing/?plan=pro#faq"`, `currentLocale="fr"` y `label="Langue"`, el enlace en alemán es `/de/pricing/?plan=pro#faq` y el enlace en francés se marca con `aria-current="page"`. Conserva los parámetros de consulta y los fragmentos solo si tienen el mismo significado en la página de destino. Si los ID de ancla están traducidos, resuelve también el fragmento equivalente.

### Con General Translation

Para las páginas públicas de Next.js, el componente [`<Link>`](/docs/react/nextjs/link) de GT genera enlaces adaptados a la configuración regional. Con `prefixDefaultLocale: true`, estos enlaces apuntan a `/en/pricing` y `/de/pricing`. En este ejemplo se da por hecho que las páginas de precios en inglés y alemán ya están publicadas. Pasa la configuración regional actual y una etiqueta de navegación localizada:

```tsx
import Link from 'gt-next/link';

export function PricingLanguages({ currentLocale, label }: { currentLocale: string; label: string }) {
  return (
    <nav aria-label={label}>
      <Link href="/pricing" locale="en" hrefLang="en" lang="en" aria-current={currentLocale === 'en' ? 'page' : undefined}>English</Link>
      {' · '}
      <Link href="/pricing" locale="de" hrefLang="de" lang="de" aria-current={currentLocale === 'de' ? 'page' : undefined}>Deutsch</Link>
    </nav>
  );
}
```

Localiza la etiqueta de navegación de la página actual. Si necesitas un control nativo compacto, por ejemplo dentro de una aplicación autenticada, usa el selector de configuración regional:

[`<LocaleSelector>`](/docs/react/reference/components/locale-selector), disponible en `gt-react`, `gt-next` y el experimental `gt-tanstack-start`, renderiza un `<select>` nativo con las configuraciones regionales que hayas definido, muestra cada una en su propio idioma y guarda la elección en una cookie. Los atributos estándar de `<select>` se transfieren, así que puedes pasar un `aria-label` traducido con [`useGT`](/docs/react/reference/hooks/use-gt):

```tsx
'use client';
import { LocaleSelector, useGT } from 'gt-next';

export function Header() {
  const gt = useGT();
  return <LocaleSelector aria-label={gt('Language')} />;
}
```

Para las páginas públicas, usa los enlaces anteriores para ofrecer una navegación rastreable y declara las versiones alternativas con hreflang en el head de la página, como se muestra en [SEO multilingüe con Next.js](/kb/multilingual-nextjs-seo). Si quieres un diseño personalizado, [`useLocaleSelector`](/docs/react/reference/hooks/use-locale-selector) devuelve la configuración regional activa, la lista de configuraciones regionales y `setLocale`. En Vue, crea un selector con [`useLocale`](/docs/vue/reference/composables/use-locale) y [`useSetLocale`](/docs/vue/reference/composables/use-set-locale) siguiendo la [guía de configuraciones regionales de Vue](/docs/vue/guides/managing-locales#switcher).

## Puntos clave

* Etiqueta las opciones con endónimos como Français y 日本語; `Intl.DisplayNames` los genera, en minúsculas en el caso del francés y el portugués.
* Usa un icono de globo terráqueo, no banderas: el W3C indica «Do not use flag icons to indicate languages» («No uses iconos de banderas para indicar idiomas»).
* Enlaza a la misma página en cada idioma con enlaces `<a href>` simples que incluyan `hreflang` y `lang`, para que los rastreadores puedan seguirlos y los lectores de pantalla pronuncien correctamente cada nombre.
* Guarda la elección explícita del usuario y nunca redirijas fuera de una URL localizada en función del idioma del navegador.
* El componente [`<LocaleSelector>`](/docs/react/reference/components/locale-selector) de General Translation renderiza un `<select>` nativo con etiquetas en el idioma propio de cada opción y guarda la elección en una cookie; añade enlaces simples para los rastreadores.

## Related questions

- [¿Cómo detecto el idioma del navegador de un usuario?](https://generaltranslation.com/es/kb/detect-user-language.mdx): 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.
- [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.
- [¿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: [¿Qué es un sistema de gestión de traducciones (TMS)?](https://generaltranslation.com/es/kb/what-is-a-translation-management-system.mdx)
- Next question: [Internacionalización vs. localización (i18n vs. l10n)](https://generaltranslation.com/es/kb/internationalization-vs-localization.mdx)

## Sitemap

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