next-intl v4 es la biblioteca de referencia para agregar internacionalización a proyectos Next.js con App Router. Instálala con npm install next-intl, configura el enrutamiento de locales en i18n/routing.ts, agrega un middleware para detección de idioma, y usa getTranslations (Server Components) o useTranslations (Client Components) para mostrar texto traducido.
32blog.com soporta japonés, inglés y español. Las URLs siguen un patrón de prefijo: /ja/nextjs/slug, /en/nextjs/slug y /es/nextjs/slug.
Este artículo recorre la configuración completa — configuración de rutas, uso de traducciones a nivel de componente, selector de idioma y SEO con hreflang — todo lo que realmente ejecuta 32blog.com.
¿Qué es next-intl?
Next.js soporta internacionalización de forma nativa mediante la opción i18n en next.config.js — pero solo en Pages Router. App Router eliminó esa opción. Para agregar i18n a App Router, tienes que construir tu propio enrutamiento basado en locales o usar una biblioteca como next-intl.
next-intl es una biblioteca de i18n diseñada para App Router con estas características clave:
- Enrutamiento de locale basado en prefijo de URL (
/ja/...,/en/...) - Hooks de traducción que funcionan tanto en Server como en Client Components
- Soporte TypeScript (claves de traducción verificadas por tipos)
- Utilidades SEO (helpers de hreflang)
Instalación y estructura de archivos
npm install next-intl
Esta es la estructura de archivos en 32blog.com:
src/
├── app/
│ ├── [locale]/ # Prefijo de locale
│ │ ├── layout.tsx
│ │ ├── page.tsx
│ │ └── [category]/
│ │ └── [slug]/
│ │ └── page.tsx
│ └── api/
├── i18n/
│ ├── routing.ts # Configuración de rutas
│ └── request.ts # Configuración de request
├── messages/
│ ├── ja.json # Traducciones en japonés
│ ├── en.json # Traducciones en inglés
│ └── es.json # Traducciones en español
└── middleware.ts # Detección de locale y redirecciones
Configuración de rutas
Comienza con src/i18n/routing.ts para definir los locales soportados y el predeterminado. Consulta la documentación de routing de next-intl para todas las opciones disponibles.
// src/i18n/routing.ts
import { defineRouting } from "next-intl/routing";
export const routing = defineRouting({
locales: ["ja", "en", "es"],
defaultLocale: "ja",
// "always" significa que /ja/..., /en/..., /es/... todos tienen prefijos explícitos
localePrefix: "always",
});
Luego, crea middleware.ts. Esto intercepta las solicitudes, detecta el locale y redirige a la URL correcta. La documentación del middleware cubre todas las opciones de configuración.
// src/middleware.ts
import createMiddleware from "next-intl/middleware";
import { routing } from "./i18n/routing";
export default createMiddleware(routing);
export const config = {
// Aplicar middleware a estas rutas (excluir archivos estáticos y APIs internas)
matcher: [
"/",
"/(ja|en|es)/:path*",
"/((?!api|_next|_vercel|.*\\..*).*)",
],
};
Configuración de request
src/i18n/request.ts define cómo los Server Components cargan las traducciones para la solicitud actual.
// src/i18n/request.ts
import { getRequestConfig } from "next-intl/server";
import { routing } from "./routing";
export default getRequestConfig(async ({ requestLocale }) => {
// Obtener locale de la URL
let locale = await requestLocale;
// Recurrir al locale predeterminado si se detecta un locale no soportado
if (!locale || !routing.locales.includes(locale as "ja" | "en" | "es")) {
locale = routing.defaultLocale;
}
return {
locale,
messages: (await import(`../../messages/${locale}.json`)).default,
};
});
Crear archivos de traducción
Define el texto de traducción en messages/ja.json:
{
"nav": {
"home": "ホーム",
"about": "このブログについて",
"articles": "記事一覧"
},
"home": {
"hero": {
"title": "ググって最後にたどり着くページ",
"subtitle": "エラー解決・技術Tips・実装例を実体験ベースで書いています"
},
"latestArticles": "新着記事",
"allArticles": "すべての記事を見る"
},
"article": {
"publishedAt": "公開日",
"updatedAt": "更新日",
"readingTime": "{minutes}分で読めます",
"tableOfContents": "目次",
"relatedArticles": "関連記事"
}
}
Y en messages/en.json:
{
"nav": {
"home": "Home",
"about": "About",
"articles": "Articles"
},
"home": {
"hero": {
"title": "The last page you land on after Googling",
"subtitle": "Error fixes, tech tips, and real-world implementations based on actual experience"
},
"latestArticles": "Latest Articles",
"allArticles": "View all articles"
},
"article": {
"publishedAt": "Published",
"updatedAt": "Updated",
"readingTime": "{minutes} min read",
"tableOfContents": "Table of Contents",
"relatedArticles": "Related Articles"
}
}
Integración en el layout
Configura el provider de next-intl en app/[locale]/layout.tsx:
// app/[locale]/layout.tsx
import { NextIntlClientProvider } from "next-intl";
import { getMessages } from "next-intl/server";
import { notFound } from "next/navigation";
import { routing } from "@/i18n/routing";
export default async function LocaleLayout({
children,
params,
}: {
children: React.ReactNode;
params: Promise<{ locale: string }>;
}) {
const { locale } = await params;
// Devolver 404 para locales no soportados
if (!routing.locales.includes(locale as "ja" | "en" | "es")) {
notFound();
}
// Obtener mensajes de traducción desde Server Component
const messages = await getMessages();
return (
<html lang={locale}>
<body>
{/* El Provider permite que los Client Components accedan a las traducciones */}
<NextIntlClientProvider messages={messages}>
{children}
</NextIntlClientProvider>
</body>
</html>
);
}
Uso de traducciones en Server Components
// app/[locale]/page.tsx
import { getTranslations } from "next-intl/server";
// Los Server Components usan getTranslations
export default async function HomePage({
params,
}: {
params: Promise<{ locale: string }>;
}) {
const { locale } = await params;
const t = await getTranslations({ locale, namespace: "home" });
return (
<main>
<section>
<h1>{t("hero.title")}</h1>
<p>{t("hero.subtitle")}</p>
</section>
</main>
);
}
Uso de traducciones en Client Components
// components/NavMenu.tsx
"use client";
import { useTranslations } from "next-intl";
import { Link } from "@/i18n/routing";
export function NavMenu() {
// Los Client Components usan useTranslations
const t = useTranslations("nav");
return (
<nav>
{/* El Link de next-intl agrega automáticamente el prefijo del locale actual */}
<Link href="/">{t("home")}</Link>
<Link href="/about">{t("about")}</Link>
</nav>
);
}
Usar el componente Link de next-intl significa que los prefijos de locale se agregan automáticamente. Escribir /about enlazará a /ja/about para usuarios en japonés y /en/about para usuarios en inglés — sin código adicional.
Construir el selector de idioma
Aquí está el selector de idioma que permite a los usuarios alternar entre idiomas:
// components/LocaleSwitcher.tsx
"use client";
import { useLocale } from "next-intl";
import { useRouter, usePathname } from "@/i18n/routing";
export function LocaleSwitcher() {
const locale = useLocale();
const router = useRouter();
const pathname = usePathname();
const switchLocale = (newLocale: string) => {
// Permanecer en la misma ruta pero cambiar el locale
router.replace(pathname, { locale: newLocale });
};
return (
<div>
<button
onClick={() => switchLocale("ja")}
aria-current={locale === "ja" ? "true" : undefined}
>
日本語
</button>
<button
onClick={() => switchLocale("en")}
aria-current={locale === "en" ? "true" : undefined}
>
English
</button>
</div>
);
}
Pasa una opción de locale a router.replace y next-intl maneja la transformación de la ruta. Si un usuario está leyendo /ja/nextjs/hydration-error-fix y hace clic en "English," llegará a /en/nextjs/hydration-error-fix.
Integración con TypeScript para claves tipadas
next-intl v4 soporta claves de traducción verificadas por TypeScript mediante module augmentation en la interfaz AppConfig. Los errores tipográficos en nombres de claves se convierten en errores de compilación.
// global.d.ts
import { routing } from "@/i18n/routing";
import en from "../messages/en.json";
declare module "next-intl" {
interface AppConfig {
Locale: (typeof routing.locales)[number];
Messages: typeof en;
}
}
Esto registra dos cosas a la vez: Locale tipifica estrictamente todas las cadenas de locale (escribir "fr" sería un error de compilación si no está en tu config de routing), y Messages habilita autocompletado y verificación de tipos para las claves de traducción.
// ❌ La clave no existe — error de TypeScript
const title = t("nav.hme"); // Error: Argument of type '"hme"' is not assignable...
// ✅ Clave correcta — sin error
const title = t("nav.home"); // OK
Con el tipo Locale registrado, puedes usar el helper hasLocale() de next-intl para hacer narrowing de cadenas desconocidas de forma segura:
import { hasLocale } from "next-intl";
// Después del narrowing, locale tiene tipo "ja" | "en" | "es"
if (hasLocale(routing.locales, locale)) {
// TypeScript sabe que locale es válido aquí
}
SEO: Configurar hreflang
Los sitios multilingües necesitan etiquetas hreflang para que Google pueda asociar correctamente las versiones de idioma de la misma página. next-intl facilita esto mediante la API de metadatos de Next.js.
// app/[locale]/[category]/[slug]/page.tsx
import type { Metadata } from "next";
export async function generateMetadata({
params,
}: {
params: Promise<{ locale: string; category: string; slug: string }>;
}): Promise<Metadata> {
const { locale, category, slug } = await params;
const post = await getPost(category, slug, locale);
const baseUrl = "https://32blog.com";
return {
title: post.title,
description: post.description,
alternates: {
canonical: `${baseUrl}/${locale}/${category}/${slug}`,
languages: {
// etiquetas hreflang
"ja": `${baseUrl}/ja/${category}/${slug}`,
"en": `${baseUrl}/en/${category}/${slug}`,
"es": `${baseUrl}/es/${category}/${slug}`,
"x-default": `${baseUrl}/ja/${category}/${slug}`,
},
},
};
}
Esto genera automáticamente etiquetas <link rel="alternate" hreflang="ja" href="..."> en el <head> de cada página. Google puede entonces entender correctamente que las versiones de idioma son traducciones del mismo contenido, no páginas duplicadas.
FAQ
¿Funciona next-intl con Next.js 16?
Sí. next-intl v4 soporta Next.js 13 a 16. Una consideración: la directiva use cache de Next.js 16 aún no funciona de manera fluida con getTranslations() porque internamente lee de headers(). El patrón existente con setRequestLocale sigue funcionando.
¿Cuál es la diferencia entre getTranslations y useTranslations?
getTranslations es una función asíncrona para Server Components. useTranslations es un hook de React para Client Components. Usar el incorrecto en el contexto equivocado causa un error en tiempo de ejecución.
¿Puedo usar next-intl sin el prefijo de URL [locale]?
Sí. Configura localePrefix: "as-needed" en tu configuración de routing y solo los locales no predeterminados tendrán prefijo. O usa localePrefix: "never" para depender de detección basada en dominio o cookies.
¿Cómo agrego un tercer (o cuarto) idioma después?
Agrega el locale a routing.ts, crea su archivo messages/{locale}.json y actualiza la regex del matcher del middleware. next-intl lo detecta automáticamente — no se necesitan cambios en los componentes.
¿next-intl aumenta el tamaño del bundle del cliente?
next-intl en sí es ligero (~2 kB gzipped para el runtime del cliente). El impacto principal en el bundle viene del JSON de traducciones que pasas a NextIntlClientProvider. Dividir los mensajes por namespace reduce esto.
¿Cómo maneja next-intl el SEO para sitios multilingües?
Proporciona utilidades de hreflang a través de la API de metadatos de Next.js. Combinado con URLs con prefijo de locale y etiquetas canonical adecuadas, Google asocia correctamente todas las versiones de idioma de cada página.
¿Debería usar next-intl o next-i18next?
next-i18next está enfocado en Pages Router y es un wrapper de i18next. Para proyectos con App Router, next-intl está diseñado específicamente y tiene soporte nativo para Server Components, lo que lo convierte en la mejor opción para proyectos nuevos.
Conclusión
Aquí tienes un resumen de los archivos clave en la configuración de next-intl v4:
| Archivo | Función |
|---|---|
i18n/routing.ts | Definir locales soportados y locale predeterminado |
middleware.ts | Detectar locale de las solicitudes y redirigir |
i18n/request.ts | Configurar cómo los Server Components cargan mensajes |
messages/ja.json | Cadenas de traducción en japonés |
app/[locale]/layout.tsx | Configurar NextIntlClientProvider |
global.d.ts | Registrar AppConfig para claves tipadas |
Lo principal que confunde a la gente: usa getTranslations en Server Components y useTranslations en Client Components. De la misma manera, usa el Link de next-intl en lugar del Link incorporado de Next.js — de lo contrario los prefijos de locale no se agregarán automáticamente.
Una vez que tengas lo básico funcionando, revisa las trampas comunes con next-intl e i18n — cosas como desajustes de hidratación por formato dependiente del locale y patrones de organización de claves de traducción que fallan a escala. Si estás trabajando con Server Components más ampliamente, la guía de SSR en Next.js sobre Server Components cubre el modelo de renderizado sobre el que next-intl se construye.
32blog.com ejecuta esta configuración en todas las páginas sirviendo miles de páginas vistas. La versión App Router de next-intl ha sido sólida y estable.