Blog de 18ways

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

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

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

La dificultad no está en traducir una sola oración. Está en agregar varios idiomas a una app real de Next.js sin romper el SEO, enredar tu base de código ni crearte un problema de mantenimiento.

Si quieres ir directo al código, revisa los ejemplos de 18ways-next en GitHub.

Configura primero tu infraestructura

Antes que nada, necesitamos instalar nuestros paquetes. Vamos a configurar las bibliotecas 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);

Agrega un proxy raíz para que / pueda redirigir al locale correcto:

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

Luego agrega un layout 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 puedes traducir tu primera página. Herramientas como i18next te pedirá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 estás usando 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 quieras permitir que las personas cambien su idioma preferido:

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

Cuidado con los tropiezos

Una vez que todo está conectado, hay varias formas en que normalmente salen mal estos proyectos.

Renderizado del servidor

La renderización del lado del servidor (SSR) es lo que permite que tu app de Next.js entregue HTML prerenderizado. Esto es vital tanto para el SEO como para que el usuario no vea parpadeos de contenido incorrecto o de un idioma equivocado.

Si estás usando una biblioteca como i18next, debes tener mucho cuidado de que tus traducciones se estén cargando y rellenando durante el renderizado del lado del servidor. Puedes probar esto revisando el view-source: de tu página, por ejemplo view-source:http://localhost:3000/. También deberías comprobar esto en producción para asegurarte de que funcione en tu compilación de producción.

Si estás usando 18ways, no te preocupes por esto. Todo se maneja por ti.

Claves de traducción

Si estás usando 18ways, no necesitas preocuparte por esto en absoluto. 18ways no necesita claves de traducción; puedes dejar tu texto en su lugar como siempre.

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

Las malas claves son vagas:

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

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

Las mejores claves 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',
};

También evita 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á que sea extremadamente 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; entonces no necesitas claves de traducción en absoluto:

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

Detección de locale

La detección de locale significa decidir qué idioma debe ver una persona usuaria antes de que cambie explícitamente.

Eso por lo general implica alguna combinación de:

En un middleware normal de Next.js, eso suele verse 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 distintos grados. Con 18ways, la detección inicial y la capa de redirección se manejan por ti.

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

Distintos idiomas y regiones formatean fechas, números y dinero de manera diferente.

Por ejemplo:

En JavaScript puro, tú mismo te encargas de eso 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 estás usando 18ways, esto se maneja 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>

Unir cadenas

No construyas interfaces traducidas 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 “👉👉Click aquí👈👈 para empezar” en inglés, pero en francés es más natural decir “Para empezar, 👉👉haz clic aquí👈👈”. Algunos idiomas como el japonés incluso necesitarán palabras tanto antes como después, como “始めるには👉👉こちら👈👈をクリックしてください”.

La estructura de la oración puede cambiar, así que dividirla en partes dificulta mucho una buena traducción.

Las oraciones completas se traducen mejor porque las personas traductoras pueden reordenar las palabras de forma natural 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 de i18n sobre cómo gestionar esto, ya que cada biblioteca lo maneja de forma muy distinta.

Si estás usando 18ways, simplemente puedes traducir todo el bloque JSX como de costumbre.

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 oración completa mientras sigues insertando valores en tiempo de ejecución.

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

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

Ese patrón funciona bien para:

La oración 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 pluralización cambian según el idioma. El inglés tiene una sola 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 de i18n te permiten especificar una sintaxis tipo ICU para manejar 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á de 18 maneras, pero en la mayoría de los casos también puedes hacerlo así:

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

¡18ways se encargará de los plurales por ti!

Empieza ahora

Agregar varios idiomas a una app de Next.js no tiene por qué convertirse en un proyecto de reescritura total.

Si no estás usando una solución como 18ways, asegúrate de configurar bien tu enrutamiento y SSR; luego trabaja en sacar tu copia a claves de traducción, cuidando evitar los tropiezos comunes de i18n.

Si estás usando 18ways, entonces todo esto se maneja por ti, y solo necesitas empezar a envolver tu texto en bloques <T>.

Cambiando idioma