blog de 18ways
Cómo añadir i18n y localización multilingüe a una aplicación Next.js existente sin perjudicar el SEO ni reconstruirla desde cero.
I18n (internacionalización), l10n (localización), compatibilidad multilingüe… llámalo como quieras, al final tendrás que actualizar tu app de Next.js para que admita más de un idioma.
La dificultad no está en traducir una frase. Está en añadir varios idiomas a una aplicación real de Next.js sin perjudicar 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 GitHub de 18ways-next.
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.
npm install @18ways/next @18ways/reactCrea tu archivo de configuración:
// 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:
// 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 a la configuración regional correcta:
// proxy.js
export { default, config } from '@18ways/next/proxy';Luego añade un diseño localizado:
// 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:
Ahora ya puedes traducir tu primera página. Herramientas como i18next te obligarán a sacar todo tu texto a claves de traducción y luego referenciarlas 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 quieres traducir en un componente <T>:
// 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 los usuarios cambien la opción de idioma:
// src/components/Footer.jsx
import { T, LanguageSwitcher } from '@18ways/react';
export default function Footer() {
return (
<footer>
<T>My footer content</T>
<LanguageSwitcher />
</footer>
);
}Una vez que la fontanería está en su sitio, hay varias formas en que estos proyectos suelen salir mal.
El renderizado en servidor (SSR) es lo que permite que tu aplicación Next.js sirva HTML prerenderizado. Esto es vital tanto para el SEO como para que el usuario no vea destellos de contenido incorrecto o un idioma equivocado.
Si usas una biblioteca como i18next, tienes que tener muchísimo cuidado de que tus traducciones se carguen y se rellenen durante el renderizado del lado del servidor. Puedes comprobarlo mirando el view-source: de tu página, por ejemplo view-source:http://localhost:3000/. Deberías comprobarlo también 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.
Si estás usando 18ways, no tienes que preocuparte por esto en absoluto. 18ways no necesita claves de traducción; puedes dejar tu texto en su sitio, como siempre.
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:
// bad-keys.en-GB.js
module.exports = {
title: 'Continue',
button: 'Pay now',
label: 'Home',
};Esas claves apenas le dicen nada a traductores y desarrolladores sobre dónde aparece el texto.
Las mejores claves incluyen contexto:
// 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:
// bad-dynamic-keys.js
const key = `checkout.${status}.${buttonType}`;
const translatedText = t(key);Esto romperá las herramientas de 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.
// 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 forma de evitar que las claves de traducción se vuelvan inmanejables.
Aún mejor es usar una herramienta como 18ways; así nunca necesitas claves de traducción en absoluto:
// app/[lang]/checkout/page.jsx
<T>Pay now</T>La detección de la configuración regional consiste en decidir qué idioma debe ver una persona usuaria antes de que cambie manualmente.
Eso suele implicar alguna combinación de:
Accept-Language del navegadorEn el middleware normal de Next.js, eso suele tener este aspecto:
// 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 incluyen ayudantes para facilitar esto, en distintos grados. Con 18ways, la detección inicial y la capa de redirección se gestionan por ti.
Los distintos locales formatean las fechas, los números y el dinero de forma diferente.
Por ejemplo:
04/05/2026 puede significar 4 de mayo o 5 de abril€1.999,00 y 1.999,00 € son ambas válidas, según la configuración regionalEn JavaScript puro, eso lo gestionas tú mismo con Intl:
// 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:
const someTimestamp = new Date('2026-04-13T09:00:00Z');
const someMoney = {
amount: 1999,
currency: 'EUR',
};
<T>My text with {{ someTimestamp }} and {{ someMoney }}</T>No construyas una interfaz traducida uniendo fragmentos:
// bad-string-joining.jsx
const clickHereText = t('click.here');
const toGetStartedText = t('to.get.started')
<p><a href="#">{clickHereText}</a> {toGetStartedText}.</p>Esto fallará 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, incluso necesitarán palabras tanto antes como después, como «始めるには👉👉こちら👈👈をクリックしてください».
La estructura de la frase puede cambiar, así que dividirla en fragmentos hace que una buena traducción sea mucho más difícil.
Las frases completas se traducen mejor porque los traductores pueden reordenar las palabras de forma natural y ver el significado como una unidad completa.
Si estás traduciendo JSX así:
// 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 una manera muy distinta.
Si estás usando 18ways, puedes traducir simplemente todo el bloque JSX como siempre.
// rich-text-message.jsx
<p>
<T>Something <strong>that we want to be bold</strong></T>
</p>Las variables te permiten mantener la frase completa a la vez que insertas valores en tiempo de ejecución.
La mayoría de las bibliotecas de i18n admiten esto de alguna forma. Con 18ways:
// 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(...):
const animal = t('dog');
<T>Favourite animal: {{ animal }}</T>Las reglas de plural 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 de i18n te permiten especificar una sintaxis parecida a ICU para gestionar los plurales:
// 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 simplemente así:
// app/[lang]/page.jsx
<T>{{ unreadCount }} unread messages</T>¡18ways gestionará los plurales por ti!
Añadir varios idiomas a una app de Next.js no tiene por qué convertirse en un proyecto de reescritura.
Si no estás usando una solución como 18ways, asegúrate de configurar bien el enrutamiento y el SSR, y luego ve pasando tu contenido a claves de traducción, procurando evitar los errores comunes de i18n.
Si estás usando 18ways, todo esto se gestiona por ti, y solo tienes que empezar a envolver tu texto en bloques <T>.