Las trampas más comunes de next-intl v4 son: matchers de middleware incorrectos, mezclar el Link de Next.js con el de next-intl, usar useTranslations en Server Components asíncronos, olvidar el locale en generateStaticParams, y configuración incompleta de hreflang. Cada una rompe tu sitio multilingüe silenciosamente, sin errores obvios.
Cuando agregué soporte para japonés, inglés y español a 32blog.com usando next-intl v4, me encontré con trampas que no estaban cubiertas en la documentación oficial.
Muchas eran del peor tipo — "parece que funciona pero en realidad está roto." Este artículo documenta cada trampa que encontré, con las soluciones.
Trampa 1: El patrón del matcher en el middleware está mal
Esto fue lo primero que hice mal. Un patrón de matcher incompleto en el middleware significa que algunas solicitudes se saltan la detección de locale — así que visitar / no redirige a /ja/.
El patrón roto común
// src/middleware.ts
export const config = {
matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
};
Esto parece razonable pero pasa archivos estáticos como .png y .svg por el middleware innecesariamente. Es más lento en desarrollo y puede causar casos extremos.
El patrón correcto
// src/middleware.ts
import createMiddleware from "next-intl/middleware";
import { routing } from "./i18n/routing";
export default createMiddleware(routing);
export const config = {
matcher: [
// Ruta raíz
"/",
// Rutas con prefijo de locale
"/(ja|en|es)/:path*",
// Rutas sin extensiones de archivo (excluye archivos estáticos)
"/((?!api|_next|_vercel|.*\\..*).*)"],
};
La parte .*\\..* es la clave — excluye rutas que contienen un punto (extensión de archivo), lo que omite archivos estáticos. Este es el patrón recomendado en la documentación de middleware de next-intl.
Trampa 2: Mezclar el Link de next-intl con el Link de Next.js
Usar el Link de Next.js directamente rompe el comportamiento del prefijo de locale durante la navegación.
Problema: usar el Link de Next.js directamente
// ❌ Esto no preserva el prefijo del locale
import Link from "next/link";
export function ArticleCard({ slug, category }) {
return (
<Link href={`/${category}/${slug}`}>
Leer artículo
</Link>
);
}
Si un usuario japonés está en /ja/ y hace clic en este enlace, aterriza en /nextjs/hydration-error-fix — el prefijo /ja/ desapareció.
Solución: usa el Link de next-intl
// ✅ El Link de next-intl agrega automáticamente el prefijo del locale actual
import { Link } from "@/i18n/routing";
export function ArticleCard({ slug, category }) {
return (
<Link href={`/${category}/${slug}`}>
Leer artículo
</Link>
);
}
Link de @/i18n/routing antepone el locale actual automáticamente. Escribir /nextjs/hydration-error-fix se convierte en /ja/nextjs/hydration-error-fix para usuarios japoneses. Consulta la documentación de navegación de next-intl para la API completa.
Lo mismo aplica para useRouter y redirect:
// ❌ Los hooks de Next.js pierden el locale
import { useRouter } from "next/navigation";
import { redirect } from "next/navigation";
// ✅ Usa las versiones de next-intl en su lugar
import { useRouter, redirect } from "@/i18n/routing";
Trampa 3: Usar useTranslations en un Server Component asíncrono
En next-intl v4, useTranslations funciona en Server Components — pero fuerza la página al renderizado dinámico. Si quieres que tus páginas se rendericen estáticamente, necesitas getTranslations.
// ⚠️ Funciona, pero fuerza renderizado dinámico
// Sin "use client" = Server Component
import { useTranslations } from "next-intl";
export default function HomePage() {
const t = useTranslations("home");
// Esta página nunca se renderizará estáticamente
return <h1>{t("title")}</h1>;
}
// ✅ Los Server Components deberían usar getTranslations (soporta renderizado estático)
import { getTranslations } from "next-intl/server";
export default async function HomePage({
params,
}: {
params: Promise<{ locale: string }>;
}) {
const { locale } = await params;
// getTranslations requiere await
const t = await getTranslations({ locale, namespace: "home" });
return <h1>{t("title")}</h1>;
}
Referencia rápida (ver documentación de Server & Client Components):
| Server Component | Client Component | |
|---|---|---|
| Traducciones | getTranslations (requiere await) | useTranslations |
| Locale actual | getLocale | useLocale |
| Todos los mensajes | getMessages | useMessages |
Trampa 4: Falta el locale en generateStaticParams
En sitios multilingües, generateStaticParams debe incluir locale como parámetro.
Generar sin locale (los artículos del locale no predeterminado dan 404)
// app/[locale]/[category]/[slug]/page.tsx
// ❌ Solo genera para el locale predeterminado (ja)
export async function generateStaticParams() {
const posts = await getAllPosts();
return posts.map((post) => ({
// ¡Falta locale! Solo se genera el locale predeterminado
category: post.category,
slug: post.slug,
}));
}
Generar todas las combinaciones locale × slug
// ✅ Generar rutas para todos los locales × todos los artículos
export async function generateStaticParams() {
const locales = ["ja", "en", "es"] as const;
const paths = [];
for (const locale of locales) {
const posts = await getPostsByLocale(locale);
for (const post of posts) {
paths.push({
locale,
category: post.category,
slug: post.slug,
});
}
}
return paths;
}
Si los artículos en inglés o español devuelven 404 después de agregarlos, esta es casi siempre la causa. La documentación de generateStaticParams de Next.js explica cómo interactúa con los segmentos dinámicos.
Trampa 5: Falta x-default en hreflang
Un error SEO común: omitir x-default de la configuración de hreflang.
// ❌ Sin x-default (Google no puede determinar el predeterminado)
export async function generateMetadata({ params }) {
const { locale, category, slug } = await params;
return {
alternates: {
languages: {
"ja": `https://32blog.com/ja/${category}/${slug}`,
"en": `https://32blog.com/en/${category}/${slug}`,
"es": `https://32blog.com/es/${category}/${slug}`,
// ¡Falta x-default!
},
},
};
}
// ✅ Incluir x-default para especificar el idioma de respaldo
export async function generateMetadata({ params }) {
const { locale, category, slug } = await params;
const baseUrl = "https://32blog.com";
return {
alternates: {
canonical: `${baseUrl}/${locale}/${category}/${slug}`,
languages: {
"ja": `${baseUrl}/ja/${category}/${slug}`,
"en": `${baseUrl}/en/${category}/${slug}`,
"es": `${baseUrl}/es/${category}/${slug}`,
// A dónde enviar usuarios cuyo idioma no está soportado
"x-default": `${baseUrl}/ja/${category}/${slug}`,
},
},
};
}
x-default le dice a Google a dónde enviar usuarios cuyo idioma no coincide con ningún locale soportado (por ejemplo, visitantes franceses). Sin él, Google ve un sitio multilingüe pero no puede determinar el predeterminado, lo que puede afectar el posicionamiento. La especificación de hreflang de Google cubre esto en detalle.
Trampa 6: notFound no obtiene contexto de locale
Llamar a notFound() renderiza la página 404 — pero app/not-found.tsx está fuera de [locale], así que las traducciones de next-intl no funcionan ahí.
app/
├── not-found.tsx ← Fuera de [locale] → traducciones no funcionarán
└── [locale]/
└── not-found.tsx ← Dentro de [locale] → traducciones funcionan
// ❌ app/not-found.tsx
// No puede usar traducciones de next-intl aquí
export default function NotFound() {
return (
<div>
<h1>404 - Page Not Found</h1>
{/* Los usuarios en inglés ven esto, pero también los japoneses */}
</div>
);
}
// ✅ app/[locale]/not-found.tsx
import { getTranslations } from "next-intl/server";
export default async function NotFound() {
const t = await getTranslations("notFound");
return (
<div>
<h1>{t("title")}</h1>
<p>{t("description")}</p>
</div>
);
}
Nota: not-found.tsx es un Server Component, así que usa getTranslations en lugar de useTranslations. app/[locale]/not-found.tsx se usa cuando se llama a notFound() desde dentro del segmento [locale]. Los 404 que ocurren antes de que el middleware procese el locale (como rutas a nivel raíz) aún usan app/not-found.tsx. Mantén ambos archivos. Consulta la guía de not-found de next-intl para la configuración recomendada.
Trampa 7: Locale obsoleto en la memoización de Client Components
Después de cambiar de idioma, los Client Components pueden retener el locale antiguo.
// ❌ El valor memoizado no se actualiza después del cambio de locale
"use client";
import { useLocale } from "next-intl";
import { useMemo } from "react";
export function LocalizedDate({ date }: { date: Date }) {
const locale = useLocale();
const formatted = useMemo(
() => date.toLocaleDateString(locale),
[] // ❌ locale no está en deps — memo nunca recalcula
);
return <time>{formatted}</time>;
}
// ✅ Incluir locale en el array de dependencias
"use client";
import { useLocale } from "next-intl";
import { useMemo } from "react";
export function LocalizedDate({ date }: { date: Date }) {
const locale = useLocale();
const formatted = useMemo(
() => date.toLocaleDateString(locale),
[date, locale] // ✅ locale incluido
);
return <time>{formatted}</time>;
}
Cuando locale falta de las dependencias de useMemo o useCallback, cambiar de idioma muestra datos formateados obsoletos. Este es sutil — la página navega correctamente, pero la fecha o número formateado se queda en el formato del locale anterior. La documentación de useMemo de React explica cómo funcionan los arrays de dependencias.
FAQ
¿next-intl funciona con el Pages Router de Next.js?
next-intl v4 soporta tanto Pages Router como App Router. Sin embargo, la API difiere significativamente entre ellos. Este artículo cubre solo App Router. Si estás en Pages Router, consulta la documentación de Pages Router de next-intl.
¿Cómo depuro cuando el middleware no redirige al locale correcto?
Agrega console.log dentro de tu función de middleware para confirmar que se está llamando. Verifica que tu patrón de matcher no esté excluyendo las rutas que esperas redirigir. También puedes verificar el header de respuesta x-next-intl-locale — si falta, el middleware no procesó la solicitud.
¿Puedo usar next-intl con Turbopack?
Sí. next-intl v4 funciona con Turbopack (el bundler predeterminado en Next.js 16). No se necesita configuración especial. Si encuentras problemas, asegúrate de estar en la última versión de next-intl.
¿Cómo configuro una regla ESLint para prevenir importaciones directas de next/link?
Usa la regla no-restricted-imports incorporada en tu configuración de ESLint:
{
"rules": {
"no-restricted-imports": ["error", {
"paths": [{
"name": "next/link",
"message": "Usa Link de '@/i18n/routing' en su lugar."
}, {
"name": "next/navigation",
"importNames": ["useRouter", "redirect"],
"message": "Usa desde '@/i18n/routing' en su lugar."
}]
}]
}
}
¿useTranslations lanza un error en Server Components?
No, no en next-intl v4. useTranslations funciona en Server Components, pero fuerza la página al renderizado dinámico. Para páginas renderizadas estáticamente, usa getTranslations de next-intl/server en su lugar. Consulta Server & Client Components en la documentación.
¿Cómo funciona la detección de locale para visitantes nuevos?
El middleware de next-intl detecta automáticamente el locale del header Accept-Language, cookies o tu valor predeterminado configurado. Puedes personalizar la estrategia de detección en la configuración de routing. En 32blog, configuramos japonés como predeterminado y dejamos que el middleware redirija según la preferencia de idioma del navegador.
¿Cuál es la diferencia entre next-intl y next-i18next?
next-i18next fue popular para Pages Router pero no ha adoptado completamente los patrones de App Router. next-intl fue construido para App Router desde el principio, soporta React Server Components nativamente y tiene mantenimiento activo (v4.8.3 a marzo de 2026). Para proyectos nuevos con App Router, next-intl es la opción recomendada.
Conclusión
Aquí tienes cada trampa resumida:
| Trampa | Síntoma | Solución |
|---|---|---|
| Matcher incompleto | La redirección raíz no funciona | Usa el patrón correcto de tres partes |
| Link de Next.js en lugar del Link de next-intl | El prefijo de locale desaparece en la navegación | Usa Link de @/i18n/routing |
useTranslations en Server Component asíncrono | La página se fuerza a renderizado dinámico | Usa getTranslations (con await) |
Sin locale en generateStaticParams | El locale no predeterminado devuelve 404 | Genera combinaciones locale × slug |
Falta x-default | SEO de hreflang incompleto | Agrega x-default a alternates.languages |
not-found.tsx fuera de [locale] | Página 404 no traducida | Coloca not-found.tsx dentro de [locale] |
locale falta de las dependencias de memo | Locale obsoleto después del cambio de idioma | Agrega locale a las deps de useMemo/useCallback |
next-intl es una gran biblioteca una vez configurada correctamente. La prevención más impactante es restringir el Link, useRouter y redirect nativos de Next.js mediante ESLint — detectar el error de importación al escribir en lugar de en tiempo de ejecución.
Artículos relacionados: