Índice
Si estás desarrollando una aplicación Next.js que necesita soportar múltiples idiomas, probablemente ya te has encontrado con un caos de archivos de configuración, cadenas de middleware rotas y traducciones que se niegan a cargar en los server components. Esta guía te lleva paso a paso por la configuración de internationalization (i18n) en Next.js 15 App Router usando @better-i18n/next — de cero a listo para producción en menos de 30 minutos.
Por qué i18n en Next.js App Router es diferente
Si usaste i18n con el Pages Router, olvida la mayor parte de lo que sabes. El App Router cambió las reglas del juego:
- Server Components son el estándar. No puedes usar hooks de React como
useTranslationsen ellos — necesitasgetTranslationsdesde el servidor. - Middleware ahora gestiona la detección de locale y el enrutamiento en lugar de la configuración
i18ndenext.config.js(eliminada en Next.js 13+). - ISR (Incremental Static Regeneration) te permite cachear traducciones en el edge y revalidarlas en segundo plano — sin reconstrucciones completas cuando cambian las traducciones.
- Streaming significa que tu layout puede renderizarse de inmediato mientras las traducciones se cargan de forma asíncrona.
La mayoría de las bibliotecas i18n tuvieron dificultades para adaptarse. @better-i18n/next fue construido específicamente para esta arquitectura, entregando traducciones desde una CDN global con caché ISR y una API de middleware composable.
Por qué importa i18n
Más del 60 % de los usuarios de internet prefieren navegar en su idioma nativo. Para productos SaaS, tiendas de comercio electrónico y plataformas de contenido desarrolladas con Next.js, la localización no es un extra — es un multiplicador de crecimiento. Un i18n bien implementado mejora:
- Posicionamiento SEO en mercados no anglófonos mediante etiquetas
hreflangy URLs localizadas. Una sólida localization SEO strategy multiplica el valor de cada página traducida. - Tasas de conversión en más de un 70 % cuando los usuarios ven el contenido en su idioma
- Retención de usuarios gracias a una experiencia que se siente nativa
El giro hacia herramientas developer-first también ha acelerado la adopción de i18n. Why developer-first localization wins in 2026 explica las fuerzas más amplias que alejan a los equipos de los flujos de trabajo centrados en traductores hacia configuraciones code-native como la que estás construyendo aquí.
Si vienes del desarrollo móvil, ten en cuenta que los patrones aquí difieren de React Native Expo localization — App Router usa carga de mensajes del lado del servidor y caché ISR en lugar de archivos de locale empaquetados, lo cual es una diferencia arquitectónica significativa. Para una introducción más amplia al vocabulario de la internacionalización antes de entrar en el código, localization and internationalisation fundamentals es un buen punto de partida.
Qué construirás
Al final de esta guía, tu aplicación Next.js tendrá:
- Detección automática de locale desde URL, cookies y cabeceras del navegador
- Carga de traducciones del lado del servidor con caché ISR
- Cambio de locale en el cliente de forma instantánea sin recargar la página
- Enrutamiento optimizado para SEO con
hreflangy URLs canónicas - Traducciones con tipado seguro y namespace scoping
1. Instalación
Comienza instalando @better-i18n/next y sus dependencias de pares:
npm install @better-i18n/next next-intl
@better-i18n/next requiere Next.js 15+ y next-intl 4+ como dependencias de pares. Se construye sobre el manejo de peticiones probado de next-intl añadiendo entrega de traducciones via CDN, detección automática de locale y una API de middleware composable.
2. Crear tu configuración i18n
Crea un archivo de configuración central al que hará referencia el resto de tu aplicación. Esta es la única fuente de verdad para tu configuración i18n.
// i18n/config.ts
import { createI18n } from "@better-i18n/next";
export const i18n = createI18n({
project: "acme/dashboard", // Tu identificador de proyecto Better i18n
defaultLocale: "en", // Locale de respaldo
localePrefix: "as-needed", // Estrategia de URL (ver Sección 6)
timeZone: "UTC", // Formateo de fecha/hora consistente
});
La función createI18n devuelve un objeto con todo lo que necesitas:
| Propiedad | Qué hace |
|---|---|
i18n.config | Configuración normalizada con valores por defecto aplicados |
i18n.requestConfig | Configuración de petición next-intl para App Router |
i18n.middleware | Middleware legado (basado en next-intl) |
i18n.betterMiddleware() | Middleware composable moderno con callback de autenticación |
i18n.getLocales() | Obtener locales disponibles desde la CDN |
i18n.getMessages() | Obtener traducciones para un locale con caché ISR |
Opciones de configuración
La interfaz I18nConfig extiende la configuración del núcleo con opciones específicas de Next.js:
interface I18nConfig {
project: string; // Formato "org/project"
defaultLocale: string; // ej. "en"
localePrefix?: "as-needed" | "always" | "never";
cookieName?: string; // Por defecto: "locale"
manifestRevalidateSeconds?: number; // ISR para manifest (por defecto: 3600)
messagesRevalidateSeconds?: number; // ISR para traducciones (por defecto: 30)
timeZone?: string; // Identificador de zona horaria IANA
storage?: TranslationStorage; // Almacenamiento de respaldo offline
staticData?: Record<string, Messages>; // Traducciones de respaldo empaquetadas
fetchTimeout?: number; // Timeout de CDN en ms (por defecto: 10000)
retryCount?: number; // Intentos de reintento (por defecto: 1)
}
3. Configurar el Middleware
El middleware gestiona la detección de locale y el enrutamiento de URL. Se ejecuta en cada petición, detecta el idioma preferido del usuario y asegura que la estructura de la URL coincida con tu estrategia de prefijo de locale.
Configuración simple
Para la mayoría de las aplicaciones, una sola línea es suficiente:
// middleware.ts
import { i18n } from "./i18n/config";
export default i18n.betterMiddleware();
export const config = {
matcher: ["/((?!api|_next|.*\\..*).*)`"],
};
Con autenticación (estilo Clerk callback)
Si necesitas combinar i18n con autenticación, betterMiddleware acepta un callback que te da acceso al locale detectado y a la respuesta i18n:
// middleware.ts
import { NextResponse } from "next/server";
import { i18n } from "./i18n/config";
export default i18n.betterMiddleware(async (request, { locale, response }) => {
const isProtected = request.nextUrl.pathname.includes("/dashboard");
const isLoggedIn = request.cookies.get("session")?.value;
if (isProtected && !isLoggedIn) {
return NextResponse.redirect(
new URL(`/${locale}/login`, request.url)
);
}
// No devolver nada = se usa la respuesta i18n (¡cabeceras preservadas!)
});
export const config = {
matcher: ["/((?!api|_next|.*\\..*).*)`"],
};
Este patrón reemplaza el enfoque obsoleto de composeMiddleware. El callback recibe el locale completamente resuelto y la respuesta con todas las cabeceras i18n ya configuradas, para que puedas concentrarte en tu lógica de autenticación sin preocuparte por conflictos de cabeceras.
Cómo funciona la detección de locale
El middleware detecta el locale del usuario usando una cadena de prioridad:
- Ruta URL —
/fr/aboutresuelve afr - Cookie — La cookie
locale(establecida en visitas anteriores) - Cabecera del navegador — La cabecera
Accept-Language - Por defecto — Cae en
defaultLocale
Los locales disponibles se obtienen de la CDN de Better i18n en cada petición (cacheados en memoria). Cuando se detecta un nuevo locale, se establece automáticamente una cookie para visitas futuras.
4. Configurar la Request Config
La request config le dice a next-intl cómo cargar traducciones para cada petición. Better i18n lo gestiona obteniendo mensajes de la CDN con caché ISR.
// i18n/request.ts
import { i18n } from "./config";
export default i18n.requestConfig;
Internamente, requestConfig hace lo siguiente en cada petición del servidor:
- Resuelve el locale desde la cabecera del middleware (o cae en la cookie, luego en el por defecto)
- Obtiene traducciones de la CDN con revalidación ISR de Next.js
- Resuelve la zona horaria para evitar desajustes de hidratación
- Devuelve
{ locale, messages, timeZone }a next-intl
La estrategia ISR significa que las traducciones se cachean en el servidor y se revalidan en segundo plano — los datos del manifest se revalidan cada 3600 segundos (1 hora) y los mensajes de traducción cada 30 segundos por defecto. Puedes ajustar esto con las opciones de configuración manifestRevalidateSeconds y messagesRevalidateSeconds.
5. Usar traducciones en componentes
Server Components
En server components, usa la función getTranslations de next-intl:
// app/[locale]/page.tsx
import { getTranslations } from "next-intl/server";
export default async function HomePage() {
const t = await getTranslations("home");
return (
<main>
<h1>{t("title")}</h1>
<p>{t("description")}</p>
</main>
);
}
Client Components
En client components, usa el hook useTranslations:
"use client";
import { useTranslations } from "next-intl";
export function WelcomeBanner() {
const t = useTranslations("home");
return (
<section>
<h2>{t("welcome")}</h2>
<p>{t("subtitle", { name: "Developer" })}</p>
</section>
);
}
Namespace scoping
Las traducciones se organizan por namespace. Cuando llamas a useTranslations("home") o getTranslations("home"), estás limitando el alcance al namespace home en tus archivos de traducción:
{
"home": {
"title": "Bienvenido a Acme",
"description": "El mejor dashboard para tu negocio",
"welcome": "¡Hola, {name}!",
"subtitle": "Empecemos"
},
"auth": {
"login": "Iniciar sesión",
"logout": "Cerrar sesión"
}
}
Esto evita colisiones de claves en diferentes partes de tu aplicación y mantiene los archivos de traducción manejables a medida que crece tu app.
6. Estrategias de prefijo de locale en URL
La opción localePrefix controla cómo aparecen los locales en las URLs. Elige la estrategia que se ajuste a tu aplicación:
"as-needed" (Por defecto)
El locale por defecto no tiene prefijo. Los demás locales reciben un prefijo.
| Locale | URL |
|---|---|
en (por defecto) | /about |
fr | /fr/about |
tr | /tr/about |
createI18n({ localePrefix: "as-needed", defaultLocale: "en" });
"always"
Cada locale recibe un prefijo, incluido el por defecto.
| Locale | URL |
|---|---|
en | /en/about |
fr | /fr/about |
tr | /tr/about |
createI18n({ localePrefix: "always", defaultLocale: "en" });
"never"
Ningún locale aparece en la URL. El locale se determina completamente por cookie y cabeceras del navegador.
| Locale | URL |
|---|---|
| Cualquiera | /about |
createI18n({ localePrefix: "never", defaultLocale: "en" });
Con "never", el middleware omite completamente la reescritura de URL de next-intl y establece el locale via la cabecera x-middleware-request-x-next-intl-locale. La request config cae en la lectura de la cookie locale cuando la cabecera del middleware no está disponible.
7. Cambio de locale en el cliente
Better i18n ofrece dos enfoques para cambiar el locale en el cliente, dependiendo de si deseas un cambio instantáneo o un enfoque con actualización del servidor.
Cambio instantáneo con BetterI18nProvider
Envuelve tu layout con BetterI18nProvider para habilitar el cambio de locale instantáneo sin recargar la página:
// app/[locale]/layout.tsx
import { getLocale, getMessages } from "next-intl/server";
import { BetterI18nProvider } from "@better-i18n/next/client";
export default async function LocaleLayout({
children,
}: {
children: React.ReactNode;
}) {
const locale = await getLocale();
const messages = await getMessages();
return (
<html lang={locale}>
<body>
<BetterI18nProvider
locale={locale}
messages={messages}
config={{ project: "acme/dashboard", defaultLocale: "en" }}
>
{children}
</BetterI18nProvider>
</body>
</html>
);
}
Luego usa el hook useSetLocale en cualquier parte de tu árbol de componentes:
"use client";
import { useSetLocale } from "@better-i18n/next/client";
export function LanguageSwitcher() {
const setLocale = useSetLocale();
return (
<div>
<button onClick={() => setLocale("en")}>English</button>
<button onClick={() => setLocale("fr")}>Français</button>
<button onClick={() => setLocale("tr")}>Türkçe</button>
</div>
);
}
Cuando se llama a setLocale:
- Establece una cookie
localepara persistencia del lado del servidor en la próxima navegación - Obtiene las nuevas traducciones desde la CDN en el cliente
- Vuelve a renderizar todo el árbol con el nuevo locale y mensajes — sin recarga de página
Crear un selector de idioma dinámico
Usa el hook useManifestLanguages para crear un selector de idioma que refleje automáticamente los idiomas configurados en tu proyecto Better i18n:
"use client";
import { useManifestLanguages, useSetLocale } from "@better-i18n/next/client";
export function DynamicLanguagePicker() {
const { languages, isLoading, error } = useManifestLanguages({
project: "acme/dashboard",
defaultLocale: "en",
});
const setLocale = useSetLocale();
if (isLoading) return <div>Cargando idiomas...</div>;
if (error) return <div>Error al cargar idiomas</div>;
return (
<select onChange={(e) => setLocale(e.target.value)}>
{languages.map((lang) => (
<option key={lang.code} value={lang.code}>
{lang.nativeName || lang.name || lang.code}
</option>
))}
</select>
);
}
La lista de idiomas se obtiene del manifest de la CDN con deduplicación de peticiones incorporada — múltiples componentes que llaman a useManifestLanguages compartirán una sola petición de red.
8. SEO: hreflang, URLs canónicas y Metadata
Una configuración SEO correcta es fundamental para aplicaciones Next.js multilingüe. Así se configuran las etiquetas hreflang y las URLs canónicas. Para un desglose completo de cómo encaja esto en una estrategia SEO multilingüe más amplia, consulta nuestra localization SEO strategy guide.
Al planificar la arquitectura de información general de tu aplicación multilingüe, una multilingual website design guide cubre patrones de navegación conscientes del locale, expansión de texto entre idiomas y consideraciones de escritura RTL que afectan directamente la estructura que construyes aquí.
Generar etiquetas hreflang
Añade enlaces de idiomas alternativos en tu root layout o en los metadatos de la página:
// app/[locale]/layout.tsx
import { i18n } from "@/i18n/config";
import type { Metadata } from "next";
export async function generateMetadata({
params,
}: {
params: { locale: string };
}): Promise<Metadata> {
const { locale } = await params;
const locales = await i18n.getLocales();
const languages: Record<string, string> = {};
for (const loc of locales) {
languages[loc] = `https://yourdomain.com/${loc}`;
}
// Añadir x-default para motores de búsqueda
languages["x-default"] = "https://yourdomain.com/en";
return {
alternates: {
canonical: `https://yourdomain.com/${locale}`,
languages,
},
};
}
Esto genera lo siguiente en tu HTML:
<link rel="alternate" hreflang="en" href="https://yourdomain.com/en" /> <link rel="alternate" hreflang="fr" href="https://yourdomain.com/fr" /> <link rel="alternate" hreflang="tr" href="https://yourdomain.com/tr" /> <link rel="alternate" hreflang="x-default" href="https://yourdomain.com/en" /> <link rel="canonical" href="https://yourdomain.com/en" />
Metadata localizada
Sirve títulos y descripciones de página traducidos para cada locale:
// app/[locale]/page.tsx
import { getTranslations } from "next-intl/server";
import type { Metadata } from "next";
export async function generateMetadata({
params,
}: {
params: { locale: string };
}): Promise<Metadata> {
const t = await getTranslations("meta");
return {
title: t("home.title"),
description: t("home.description"),
openGraph: {
title: t("home.title"),
description: t("home.description"),
},
};
}
9. Respaldo offline y resiliencia
Las aplicaciones en producción necesitan manejar las interrupciones de la CDN de forma elegante. @better-i18n/next proporciona una cadena de respaldo de tres niveles tanto para datos del manifest como de traducción:
- Caché en memoria — Caché TTL en proceso (más rápido)
- Obtención CDN — Con timeout y reintento configurables
- Almacenamiento persistente — Para escenarios offline/degradados
- Datos estáticos — Traducciones empaquetadas como último recurso
// i18n/config.ts
import { createI18n } from "@better-i18n/next";
export const i18n = createI18n({
project: "acme/dashboard",
defaultLocale: "en",
fetchTimeout: 5000, // Cancelar obtención de CDN después de 5s
retryCount: 2, // Reintentar dos veces en caso de fallo
staticData: { // Respaldo empaquetado
en: {
common: { error: "Algo salió mal" },
},
},
});
Si la CDN no es accesible, las traducciones se sirven desde el almacenamiento persistente (si está configurado) o caen en los staticData empaquetados. Tu aplicación nunca muestra claves rotas a los usuarios.
10. Traducción con IA mediante MCP
Better i18n incluye un servidor MCP (Model Context Protocol) que permite a los asistentes de IA gestionar tus traducciones directamente. En lugar de escribir archivos de traducción manualmente, puedes usar Claude, Cursor o cualquier herramienta compatible con MCP para:
- Crear claves de traducción con
createKeys - Proponer nuevos idiomas con
proposeLanguages - Actualizar traducciones existentes con
updateKeys - Publicar en la CDN con
publishTranslations
Flujo de trabajo de ejemplo
- Escribes tu componente React con cadenas en inglés
- Le preguntas a tu asistente de IA: "Añade traducciones al francés y turco para la página de inicio"
- El servidor MCP crea las claves, propone traducciones y las publica
- Tu aplicación Next.js recoge las nuevas traducciones en el siguiente ciclo de revalidación ISR (30 segundos por defecto)
Sin edición manual de JSON. Sin copiar y pegar entre archivos. Todo el flujo de trabajo de traducción ocurre a través de tu asistente de codificación con IA. Para ver este flujo de trabajo en acción con nuestro propio contenido de blog, lee how we use AI to write our own blog.
Una vez que tus traducciones estén en vivo en todos los idiomas, vale la pena ejecutar un i18n testing pass dedicado — las comprobaciones automatizadas detectan claves faltantes, interpolaciones rotas y casos límite de pluralización antes de que lleguen a los usuarios. Y si tus cadenas necesitan matices — etiquetas de UI que varían según el tono o el contexto — nuestro artículo sobre why translation context matters explica cómo proporcionar ese contexto a los traductores de IA de manera efectiva.
Juntando todo
Aquí está la estructura de archivos completa para un proyecto Next.js App Router con Better i18n:
your-app/
i18n/
config.ts # Configuración createI18n
request.ts # Configuración de petición next-intl
middleware.ts # Detección de locale y enrutamiento
app/
[locale]/
layout.tsx # Wrapper BetterI18nProvider
page.tsx # Server component con getTranslations
components/
LanguageSwitcher.tsx # Cambio de locale en el cliente
Referencia rápida
// i18n/config.ts
import { createI18n } from "@better-i18n/next";
export const i18n = createI18n({
project: "acme/dashboard",
defaultLocale: "en",
localePrefix: "as-needed",
});
// i18n/request.ts
import { i18n } from "./config";
export default i18n.requestConfig;
// middleware.ts
import { i18n } from "./i18n/config";
export default i18n.betterMiddleware();
export const config = {
matcher: ["/((?!api|_next|.*\\..*).*)`"],
};
Errores comunes (y cómo evitarlos)
Después de ayudar a cientos de equipos a configurar i18n en Next.js, estos son los problemas más frecuentes:
1. Desajustes de hidratación con formateo de fecha/hora
Si formateas fechas u horas sin establecer una timeZone, el servidor y el cliente pueden renderizar valores diferentes — provocando errores de hidratación de React.
Solución: Siempre establece timeZone en tu configuración de createI18n:
createI18n({
project: "acme/dashboard",
defaultLocale: "en",
timeZone: "UTC", // Previene desajuste servidor/cliente
});
Better i18n establece esto automáticamente en la request config y en BetterI18nProvider, cayendo en Intl.DateTimeFormat().resolvedOptions().timeZone si no especificas uno.
2. Prefijo de locale faltante en el locale por defecto
Con localePrefix: "as-needed" (por defecto), tu locale por defecto no tiene prefijo de URL. Esto significa que /about sirve inglés pero /fr/about sirve francés. Si olvidas esto y codificas segmentos de locale en los enlaces, tu locale por defecto se romperá.
Solución: Usa el componente Link de next-intl o construye las rutas dinámicamente:
// Haz esto:
<Link href="/about">{t("nav.about")}</Link>
// No esto:
<a href="/en/about">About</a>
3. Cookie que no persiste entre subdominios
La cookie locale por defecto se establece con path: / pero sin domain. Si tu aplicación abarca subdominios (ej. app.yourdomain.com y www.yourdomain.com), la cookie no se compartirá.
Solución: Para configuraciones multi-subdominio, usa localePrefix: "always" para que el locale esté siempre en la URL, o establece un dominio de cookie personalizado en tu callback de middleware.
4. Matcher de middleware que no excluye archivos estáticos
Si tu matcher de middleware es demasiado amplio, se ejecutará en cada petición — incluyendo imágenes, fuentes y rutas de API. Esto ralentiza tu aplicación y puede causar redirecciones de locale inesperadas.
Solución: Usa siempre el patrón de matcher recomendado:
export const config = {
matcher: ["/((?!api|_next|.*\\..*).*)`"],
};
Esto excluye /api/*, /_next/* y cualquier ruta que contenga una extensión de archivo.
Conclusión
Configurar i18n en Next.js App Router no tiene por qué ser doloroso. Con @better-i18n/next, obtienes:
- Detección de locale sin configuración desde URL, cookies y cabeceras del navegador
- Traducciones via CDN con caché ISR y respaldo offline
- Middleware composable que funciona bien con autenticación (Clerk, NextAuth, etc.)
- Cambio de locale en el cliente instantáneo sin recarga de página
- Listo para SEO con hreflang, URLs canónicas y metadata localizada
- Traducción con IA mediante la integración del servidor MCP
Toda la configuración requiere cinco archivos y menos de 50 líneas de código de configuración. Tus traducciones se sirven desde una CDN global, se cachean con ISR y se gestionan a través de tu asistente de IA.
¿Listo para empezar? Instala @better-i18n/next y ten tu primer locale funcionando en minutos:
npm install @better-i18n/next next-intl
Consulta la documentación completa o explora el repositorio de GitHub para patrones más avanzados.
Recursos relacionados
- Next.js i18n Landing Page — Resumen de características y comparación
- i18n for Developers — Por qué Better i18n es developer-first
- CLI Code Scanning — Detectar cadenas codificadas antes de que se publiquen