Blog de 18ways

Cómo añadir varios idiomas a una app de Next.js

Cómo añadir i18n multilingüe y localización a una app de Next.js existente sin perjudicar el SEO ni reconstruirla desde cero.

I18n (internacionalización), l10n (localización), compatibilidad multilingüe… como quieras llamarlo, tarde o temprano tendrás que actualizar tu app de Next.js para dar soporte a más de un idioma.

La dificultad no está en traducir una frase. Está en añadir varios idiomas a una app Next.js real sin romper el SEO, enredar tu base de código o crearte un problema de mantenimiento.

Si quieres ir directamente al código, echa un vistazo a los ejemplos de 18ways-next en GitHub.

Configura primero tu infraestructura

Antes que nada, necesitamos instalar nuestros paquetes. Vamos a configurar las librerías de 18ways, pero herramientas como i18next funcionan bien si tu proyecto es sencillo y solo tiene contenido estático.

bash
npm install @18ways/next @18ways/react

Crea tu archivo de configuración:

js
// 18ways.config.js
module.exports = {
  apiKey: 'pk_dummy_demo_token',
  baseLocale: 'en-GB',
  router: 'app', // 'app', or 'path' depending on which Next.js router you are using
};

Envuelve tu configuración de Next.js:

js
// next.config.js
const { withWays } = require('@18ways/next/config');
 
const nextConfig = {
  /*
   * your normal Next.js config here
   */ 
};
 
module.exports = withWays(nextConfig);

Añade un proxy raíz para que / pueda redirigir al locale correcto:

js
// proxy.js
export { default, config } from '@18ways/next/proxy';

Luego añade un diseño localizado:

jsx
// app/layout.jsx
import './styles.css';
import { WaysRoot } from '@18ways/next/server';
 
export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body className="next-demo-body">
        <WaysRoot>{children}</WaysRoot>
      </body>
    </html>
  );
}

¡Y listo! Ahora tienes:

Traduce tu primera página

Ahora ya puedes traducir tu primera página. Herramientas como i18next exigirán que saques todo tu texto a claves de traducción y luego las referencies en tu código. El aspecto exacto de esto dependerá de la biblioteca que hayas elegido.

Si usas 18ways, simplemente puedes envolver el texto que quieras traducir en un componente <T>:

jsx
// src/components/MyExampleComponent.jsx
import { useT, T } from '@18ways/react';
 
export default function MyExampleComponent() {
  const t = useT();
 
  return (
    <div>
      <T>Hello world!</T>
      <img
        src="https://example.com/image.png"
        alt={t('Example image')}
      />
    </div>
  );
}

Probablemente querrás permitir que los usuarios cambien su elección de idioma:

jsx
// src/components/Footer.jsx
import { T, LanguageSwitcher } from '@18ways/react';
 
export default function Footer() {
  return (
    <footer>
      <T>My footer content</T>
      <LanguageSwitcher />
    </footer>
  );
}

Ojo con los problemas

Una vez que todo está montado, hay varias formas en que estos proyectos suelen salir mal.

Renderizado del servidor

La renderización en servidor (SSR) es lo que permite que tu app de Next.js sirva HTML prerenderizado. Esto es fundamental tanto para el SEO como para que el usuario no vea destellos de contenido incorrecto o de un idioma erróneo.

Si usas una biblioteca como i18next, tienes que tener mucho cuidado de que tus traducciones se estén cargando y rellenando durante el renderizado del lado del servidor. Puedes comprobarlo mirando el view-source: de tu página, por ejemplo, view-source:http://localhost:3000/. También deberías comprobarlo en producción para asegurarte de que funciona igualmente en tu compilación de producción.

Si usas 18ways, no te preocupes por esto. Se encarga de todo por ti.

Claves de traducción

Si usas 18ways, no necesitas preocuparte por esto en absoluto. 18ways no necesita claves de traducción; puedes dejar tu texto en su sitio como de costumbre.

Muchos sistemas de i18n requieren que descompongas tu código en claves de traducción. Es importante tener cuidado con cómo nombras esas claves.

Las malas claves son vagas:

js
// bad-keys.en-GB.js
module.exports = {
  title: 'Continue',
  button: 'Pay now',
  label: 'Home',
};

Esas claves no les dicen prácticamente nada a traductores y desarrolladores sobre dónde aparece el texto.

Las claves mejores incluyen contexto:

js
// better-keys.en-GB.js
module.exports = {
  'checkout.payment.primaryButton': 'Pay now',
  'checkout.payment.stepTitle': 'Complete your payment',
  'account.sidebar.homeLink': 'Home',
};

Evita también generar claves dinámicamente:

js
// bad-dynamic-keys.js
const key = `checkout.${status}.${buttonType}`;
const translatedText = t(key);

Esto romperá las herramientas del IDE que intentan hacer que las claves de traducción sean menos insoportables. También hará muy difícil buscar y limpiar claves de traducción antiguas.

js
// better-dynamic-keys.js
const keyMap = {
  success: {
    primary: t('checkout.success.primary'),
    default: t('checkout.success.default'),
  },
  error: {
    primary: t('checkout.error.primary'),
    default: t('checkout.error.default'),
  },
};
 
const translatedText = keyMap[status][buttonType];

Esto es menos DRY, pero es la mejor manera de evitar que las claves de traducción se vuelvan inmanejables.

Aún mejor es usar una herramienta como 18ways; así nunca necesitarás claves de traducción:

jsx
// app/[lang]/checkout/page.jsx
<T>Pay now</T>

Detección de locale

La detección de locale consiste en decidir qué idioma debe ver un usuario antes de que cambie manualmente.

Eso normalmente implica alguna combinación de:

En el middleware estándar de Next.js, eso suele ser algo así:

js
// middleware.js
 
/**
 * You don't need any of this if you're using 18ways 
 */
 
import { NextResponse } from 'next/server';
 
const acceptedLocales = ['en-GB', 'fr-FR'];
 
export function middleware(request) {
  const savedLocale =
    request.cookies.get('preferred-locale')?.value;
  const browserLocale =
    request.headers
      .get('accept-language')
      ?.split(',')[0] || 'en-GB';
 
  const locale = acceptedLocales.includes(savedLocale)
    ? savedLocale
    : acceptedLocales.includes(browserLocale)
      ? browserLocale
      : 'en-GB';
 
  if (request.nextUrl.pathname === '/') {
    return NextResponse.redirect(
      new URL(`/${locale}`, request.url)
    );
  }
 
  return NextResponse.next();
}

Algunas bibliotecas tienen ayudas para facilitar esto, en distinto grado. Con 18ways, la detección inicial y la capa de redirección se encargan por ti.

Fechas y primitivas específicas de la configuración regional

Distintos locales formatean las fechas, los números y el dinero de forma distinta.

Por ejemplo:

En JavaScript puro, eso lo gestionas tú con Intl:

js
// formatting-dates-and-currency.js
const myLocale = getCurrentLocale(); // depends on your lib
 
const someTimestamp = new Date('2026-04-13T09:00:00Z');
const dateLabel = new Intl.DateTimeFormat('fr-FR', {
  dateStyle: 'long',
}).format(someTimestamp);
 
const someMoney = {
  amount: 1999,
  currency: 'EUR',
};
const moneyLabel = new Intl.NumberFormat('de-DE', {
  style: 'currency',
  currency: someMoney.currency,
}).format(someMoney.amount);
 
const translatedText = t(
  'my.translation.key',
  { dateLabel, moneyLabel }
);

Si usas 18ways, esto se encarga por ti:

jsx
const someTimestamp = new Date('2026-04-13T09:00:00Z');
const someMoney = {
  amount: 1999,
  currency: 'EUR',
};
 
<T>My text with {{ someTimestamp }} and {{ someMoney }}</T>

Uniendo cadenas

No construyas una interfaz traducida juntando fragmentos:

jsx
// bad-string-joining.jsx
const clickHereText = t('click.here');
const toGetStartedText = t('to.get.started')
<p><a href="#">{clickHereText}</a> {toGetStartedText}.</p>

Esto se romperá en varios idiomas.

Podrías decir «👉👉Haz clic aquí👈👈 para empezar» en inglés, pero en francés es más natural decir «Pour commencer, 👉👉cliquez ici👈👈». Algunos idiomas, como el japonés, necesitarán incluso palabras tanto antes como después, como «始めるには👉👉こちら👈👈をクリックしてください».

La estructura de la frase puede cambiar, así que dividirla en partes hace que traducirla bien sea mucho más difícil.

Las frases completas se traducen mejor porque los traductores pueden reordenar las palabras con naturalidad y ver el significado como una unidad completa.

Si estás traduciendo JSX así:

jsx
// rich-text-message.jsx
<p>
  Something <strong>that we want to be bold</strong>
</p>

tendrás que consultar tu biblioteca i18n sobre cómo gestionar esto, ya que distintas bibliotecas lo manejan de forma muy diferente.

Si usas 18ways, puedes simplemente traducir todo el bloque JSX como harías normalmente.

jsx
// rich-text-message.jsx
<p>
  <T>Something <strong>that we want to be bold</strong></T>
</p>

Variables

Las variables te permiten mantener la frase completa y, al mismo tiempo, insertar valores en tiempo de ejecución.

La mayoría de las bibliotecas de i18n lo admiten de alguna forma. Con 18ways:

jsx
// app/[lang]/page.jsx
<T>Hello {{ name: 'Ada' }}</T>

Ese patrón funciona bien para:

La frase sigue siendo legible, y el valor sigue siendo explícito.

Si necesitas que la propia variable también se traduzca, asegúrate de envolverla en un t(...):

jsx
const animal = t('dog');
<T>Favourite animal: {{ animal }}</T>

Plurales

Las reglas de plurales difieren según el idioma. El inglés tiene una forma plural (1 year, 2 years, etc.). El polaco tiene varias formas plurales (1 rok, 2 lata, 5 lat). El japonés no tiene forma plural en absoluto (1 年, 2 年, 3 年).

La mayoría de las bibliotecas i18n te permiten especificar una sintaxis parecida a ICU para gestionar los plurales:

jsx
// app/[lang]/page.jsx
<T>
  {{
    unreadCount,
    format:
      'plural, =0{No unread messages} =1{One unread message} other{{unreadCount} unread messages}',
  }}
</T>

Lo anterior funcionará en 18ways, pero en la mayoría de los casos también puedes hacer simplemente esto:

jsx
// app/[lang]/page.jsx
<T>{{ unreadCount }} unread messages</T>

¡18ways se encarga de los plurales por ti!

Empieza ahora

Añadir varios idiomas a una app de Next.js no tiene por qué convertirse en un proyecto de reescritura completa.

Si no estás usando una solución como 18ways, asegúrate de dejar bien configurados el enrutado y el SSR; después, ve moviendo tu copy a claves de traducción, procurando evitar los problemas habituales de i18n.

Si usas 18ways, esto ya se encarga por ti, y solo tienes que empezar a envolver tu texto en bloques <T>!

Cambiando idioma