Blog de 18ways
Cómo agregar i18n y localización multilingüe a una app existente de Next.js sin afectar el SEO ni reconstruirla desde cero.
I18n (internacionalización), l10n (localización), soporte multilingüe… como sea que le llames, tarde o temprano necesitas actualizar tu app de Next.js para que soporte más de un idioma.
La dificultad no es traducir una sola oración. Es 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, consulta 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);Agrega un proxy raíz para que / pueda redirigir a la configuración regional correcta:
// proxy.js
export { default, config } from '@18ways/next/proxy';Luego agrega 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 puedes traducir tu primera página. Herramientas como i18next te pedirán separar todo tu texto en claves de traducción y luego referenciarlas en tu código. Exactamente cómo se verá 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 las personas cambien el idioma que eligen:
// 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 base está lista, hay varias formas en que estos proyectos suelen salir mal.
La renderización del lado del servidor (SSR) es lo que permite que tu app de Next.js entregue HTML prerenderizado desde el servidor. Esto es vital tanto para el SEO como para que el usuario no vea destellos de contenido incorrecto o de un idioma equivocado.
Si estás usando una biblioteca como i18next, tienes que tener mucho cuidado de que tus traducciones se carguen y se rellenen durante el renderizado del lado del servidor. Puedes comprobarlo revisando 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 funcione en tu compilación de producción.
Si usas 18ways, no te preocupes por esto. Todo se maneja 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 lugar, como siempre.
Muchos sistemas de i18n requieren que dividas tu código en claves de traducción. Es importante tener cuidado con el nombre que les das a esas claves.
Las malas claves son vagas:
// bad-keys.en-GB.js
module.exports = {
title: 'Continue',
button: 'Pay now',
label: 'Home',
};Esas claves les dicen a los traductores y a los desarrolladores casi nada 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',
};También evita generar claves dinámicamente:
// bad-dynamic-keys.js
const key = `checkout.${status}.${buttonType}`;
const translatedText = t(key);Esto romperá las herramientas del IDE que intentan hacer las claves de traducción 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 manera 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 configuración regional significa decidir qué idioma debe ver un usuario antes de cambiarlo explícitamente.
Eso normalmente implica alguna combinación de:
Accept-Language del navegadorEn el middleware estándar de Next.js, eso a menudo se ve así:
// 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 encargan por ti.
Los distintos locales dan formato a fechas, números y dinero de maneras diferentes.
Por ejemplo:
04/05/2026 puede significar 4 de mayo o 5 de abril€1,999.00 y 1.999,00 € son válidos, dependiendo de la configuración regionalEn JavaScript puro, tú te encargas de eso 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 ensamblando fragmentos:
// 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.
Quizá en inglés dirías “👉👉Haz clic aquí👈👈 para empezar”, 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 hace que una buena traducción sea mucho más difícil.
Las oraciones 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 como este:
// 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 manejar esto, ya que cada biblioteca lo gestiona de forma muy distinta.
Si estás usando 18ways, simplemente puedes traducir 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 oración completa mientras insertas valores en tiempo de ejecución.
La mayoría de las bibliotecas de i18n lo admiten de alguna forma. Con 18ways:
// app/[lang]/page.jsx
<T>Hello {{ name: 'Ada' }}</T>Ese patrón funciona bien para:
La oración se mantiene legible, y el valor sigue siendo explícito.
Si también necesitas traducir la variable en sí, asegúrate de envolverla en un t(...):
const animal = t('dog');
<T>Favourite animal: {{ animal }}</T>Las reglas de pluralización cambian 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 similar a ICU para manejar 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 hacer esto simplemente:
// app/[lang]/page.jsx
<T>{{ unreadCount }} unread messages</T>18ways manejará los plurales por ti.
Agregar 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 tu routing y SSR, y luego trabaja en mover tu contenido a claves de traducción, cuidando evitar los errores comunes de i18n.
Si estás usando 18ways, entonces todo esto se encarga por ti, y solo necesitas empezar a envolver tu texto en bloques <T>