Respuesta rápida
next-intl es la mejor librería de internacionalización (i18n) para Next.js App Router. Instálala con npm install next-intl, crea una carpeta [locale] en tu directorio app, configura el middleware para detección de locale y usa getTranslations() en Server Components o useTranslations() en Client Components. next-intl está diseñado específicamente para Next.js 13-15+ con soporte nativo de Server Component, enrutamiento automático y un tamaño de bundle de ~2KB.
¿Qué es next-intl?
next-intl es una librería de internacionalización ligera construida específicamente para Next.js. A diferencia de las librerías i18n de React de propósito general, next-intl fue diseñada desde cero para el App Router y Server Components.
¿Por qué next-intl sobre otras librerías?
| Característica | next-intl | react-i18next | react-intl |
|---|---|---|---|
| Construido para Next.js | Sí | No (general React) | No (general React) |
| Server Components | Nativo | Necesita wrapper | Necesita wrapper |
| Tamaño del bundle | ~2KB | ~8KB | ~12KB |
| Soporte App Router | De primera clase | Parcial | Parcial |
| TypeScript | Excelente | Bueno | Bueno |
| Integración de enrutamiento | Integrado | Manual | Manual |
Beneficios clave:
- Cero JS del lado del cliente para traducciones renderizadas en servidor - las traducciones se quedan en el servidor
- Enrutamiento automático de locale - /en/about, /es/about manejado automáticamente
- Formato de mensaje ICU - pluralización adecuada, género y formateo
- Type-safe - soporte completo de TypeScript con autocompletado
Cuando escalas más allá de archivos JSON, IntlPull se integra perfectamente con next-intl para la gestión de traducción, traducción IA y colaboración en equipo.
Instalación y Configuración
Paso 1: Instalar next-intl
Terminal1npm install next-intl 2# o 3yarn add next-intl 4# o 5pnpm add next-intl
Paso 2: Estructura del Proyecto
Crea esta estructura de carpetas:
/your-nextjs-app
├── /app
│ └── /[locale] # Segmento de locale dinámico
│ ├── layout.tsx # Layout consciente del locale
│ ├── page.tsx # Página de inicio
│ └── /about
│ └── page.tsx # Página sobre nosotros
├── /messages # Archivos de traducción
│ ├── en.json
│ ├── es.json
│ └── de.json
├── /i18n
│ ├── config.ts # Configuración de locale
│ └── request.ts # Configuración del lado del servidor
├── middleware.ts # Detección de locale y enrutamiento
└── next.config.js
Paso 3: Configurar Locales
Crea i18n/config.ts:
TypeScript1export const locales = ['en', 'es', 'de', 'fr', 'ja'] as const; 2export type Locale = (typeof locales)[number]; 3export const defaultLocale: Locale = 'en'; 4 5// Opcional: metadatos de locale para UI 6export const localeNames: Record<Locale, string> = { 7 en: 'English', 8 es: 'Español', 9 de: 'Deutsch', 10 fr: 'Français', 11 ja: '日本語', 12};
Paso 4: Crear Configuración de Solicitud
Crea i18n/request.ts:
TypeScript1import { getRequestConfig } from 'next-intl/server'; 2import { notFound } from 'next/navigation'; 3import { locales } from './config'; 4 5export default getRequestConfig(async ({ locale }) => { 6 // Validar que el locale entrante está soportado 7 if (!locales.includes(locale as any)) { 8 notFound(); 9 } 10 11 return { 12 messages: (await import(`../messages/${locale}.json`)).default, 13 }; 14});
Paso 5: Configurar next.config.js
JavaScript1const createNextIntlPlugin = require('next-intl/plugin'); 2 3const withNextIntl = createNextIntlPlugin('./i18n/request.ts'); 4 5/** @type {import('next').NextConfig} */ 6const nextConfig = { 7 // Tu otra config de Next.js 8}; 9 10module.exports = withNextIntl(nextConfig);
Paso 6: Crear Middleware
Crea middleware.ts en la raíz de tu proyecto:
TypeScript1import createMiddleware from 'next-intl/middleware'; 2import { locales, defaultLocale } from './i18n/config'; 3 4export default createMiddleware({ 5 locales, 6 defaultLocale, 7 localePrefix: 'always', // o 'as-needed' o 'never' 8}); 9 10export const config = { 11 // Coincide con todas las rutas excepto 12 // - API routes 13 // - _next (internos de Next.js) 14 // - Archivos estáticos (imágenes, etc.) 15 matcher: ['/((?!api|_next|.*\\..*).*)'], 16};
Paso 7: Crear Archivos de Traducción
Crea messages/en.json:
JSON1{ 2 "common": { 3 "welcome": "Welcome to our app", 4 "loading": "Loading...", 5 "error": "Something went wrong" 6 }, 7 "home": { 8 "title": "Home", 9 "description": "This is the home page", 10 "cta": "Get Started" 11 }, 12 "navigation": { 13 "home": "Home", 14 "about": "About", 15 "contact": "Contact" 16 } 17}
Crea messages/es.json:
JSON1{ 2 "common": { 3 "welcome": "Bienvenido a nuestra aplicación", 4 "loading": "Cargando...", 5 "error": "Algo salió mal" 6 }, 7 "home": { 8 "title": "Inicio", 9 "description": "Esta es la página de inicio", 10 "cta": "Comenzar" 11 }, 12 "navigation": { 13 "home": "Inicio", 14 "about": "Acerca de", 15 "contact": "Contacto" 16 } 17}
Paso 8: Crear Root Layout
Crea app/[locale]/layout.tsx:
TypeScript1import { NextIntlClientProvider } from 'next-intl'; 2import { getMessages } from 'next-intl/server'; 3import { notFound } from 'next/navigation'; 4import { locales } from '@/i18n/config'; 5 6export function generateStaticParams() { 7 return locales.map((locale) => ({ locale })); 8} 9 10export default async function LocaleLayout({ 11 children, 12 params: { locale }, 13}: { 14 children: React.ReactNode; 15 params: { locale: string }; 16}) { 17 // Validar locale 18 if (!locales.includes(locale as any)) { 19 notFound(); 20 } 21 22 // Obtener mensajes para el locale actual 23 const messages = await getMessages(); 24 25 return ( 26 <html lang={locale}> 27 <body> 28 <NextIntlClientProvider messages={messages}> 29 {children} 30 </NextIntlClientProvider> 31 </body> 32 </html> 33 ); 34}
Usando Traducciones
Server Components (Recomendado)
En Server Components, usa getTranslations():
TypeScript1import { getTranslations } from 'next-intl/server'; 2 3export default async function HomePage() { 4 const t = await getTranslations('home'); 5 6 return ( 7 <main> 8 <h1>{t('title')}</h1> 9 <p>{t('description')}</p> 10 <button>{t('cta')}</button> 11 </main> 12 ); 13}
Este es el enfoque recomendado porque:
- Las traducciones se renderizan en el servidor
- Cero JavaScript enviado al cliente para estas cadenas
- Mejor rendimiento y SEO
Client Components
Para componentes interactivos, usa useTranslations():
TypeScript1'use client'; 2 3import { useTranslations } from 'next-intl'; 4 5export function AddToCartButton() { 6 const t = useTranslations('product'); 7 8 const handleClick = () => { 9 // Lógica del lado del cliente 10 }; 11 12 return ( 13 <button onClick={handleClick}> 14 {t('addToCart')} 15 </button> 16 ); 17}
Accediendo a Múltiples Namespaces
TypeScript1import { getTranslations } from 'next-intl/server'; 2 3export default async function Page() { 4 const t = await getTranslations('home'); 5 const tCommon = await getTranslations('common'); 6 7 return ( 8 <div> 9 <h1>{t('title')}</h1> 10 <p>{tCommon('welcome')}</p> 11 </div> 12 ); 13}
Variables e Interpolación
Variables Básicas
JSON1{ 2 "greeting": "Hola, {name}!", 3 "items": "Tienes {count} artículos en tu carrito" 4}
TypeScriptt('greeting', { name: 'Juan' }) // "Hola, Juan!" t('items', { count: 5 }) // "Tienes 5 artículos en tu carrito"
Pluralización (Formato ICU)
next-intl usa formato de mensaje ICU para pluralización adecuada:
JSON{ "cartItems": "{count, plural, =0 {Tu carrito está vacío} one {# artículo en el carrito} other {# artículos en el carrito}}" }
TypeScriptt('cartItems', { count: 0 }) // "Tu carrito está vacío" t('cartItems', { count: 1 }) // "1 artículo en el carrito" t('cartItems', { count: 5 }) // "5 artículos en el carrito"
Select (Género, Estado, etc.)
JSON{ "userStatus": "{status, select, online {El usuario está en línea} offline {El usuario está desconectado} away {El usuario está ausente} other {Estado desconocido}}" }
TypeScriptt('userStatus', { status: 'online' }) // "El usuario está en línea"
Texto Rico (HTML/Componentes)
JSON{ "terms": "Al registrarte, aceptas nuestros <terms>Términos de Servicio</terms> y <privacy>Política de Privacidad</privacy>." }
TypeScript1import { useTranslations } from 'next-intl'; 2import Link from 'next/link'; 3 4function SignupForm() { 5 const t = useTranslations('auth'); 6 7 return ( 8 <p> 9 {t.rich('terms', { 10 terms: (chunks) => <Link href="/terms">{chunks}</Link>, 11 privacy: (chunks) => <Link href="/privacy">{chunks}</Link>, 12 })} 13 </p> 14 ); 15}
Formateo de Número, Fecha y Moneda
Formateo de Números
TypeScript1import { useFormatter } from 'next-intl'; 2 3function PriceDisplay({ price }: { price: number }) { 4 const format = useFormatter(); 5 6 return ( 7 <span> 8 {format.number(price, { style: 'currency', currency: 'USD' })} 9 </span> 10 ); 11} 12 13// en-US: "$1,234.56" 14// de-DE: "1.234,56 $" 15// ja-JP: "$1,234.56"
Formateo de Fechas
TypeScript1import { useFormatter } from 'next-intl'; 2 3function DateDisplay({ date }: { date: Date }) { 4 const format = useFormatter(); 5 6 return ( 7 <> 8 <p>{format.dateTime(date, { dateStyle: 'full' })}</p> 9 <p>{format.dateTime(date, { timeStyle: 'short' })}</p> 10 <p>{format.relativeTime(date)}</p> 11 </> 12 ); 13} 14 15// en-US: "Friday, January 17, 2026" 16// de-DE: "Freitag, 17. Januar 2026"
Tiempo Relativo
TypeScript1const format = useFormatter(); 2 3format.relativeTime(new Date('2026-01-10')) // "hace 7 días" 4format.relativeTime(new Date('2026-01-20')) // "en 3 días"
Formateo de Listas
TypeScript1const format = useFormatter(); 2 3format.list(['Manzana', 'Banana', 'Naranja'], { type: 'conjunction' }) 4// en: "Apple, Banana, and Orange" 5// es: "Manzana, Banana y Naranja"
Enrutamiento y Navegación
Enlaces Conscientes del Locale
Usa next-intl/link para prefijo de locale automático:
TypeScript1import Link from 'next-intl/link'; 2 3function Navigation() { 4 return ( 5 <nav> 6 <Link href="/">Inicio</Link> 7 <Link href="/about">Acerca de</Link> 8 <Link href="/contact">Contacto</Link> 9 </nav> 10 ); 11} 12// Se renderiza automáticamente como /en/about, /es/about basado en el locale actual
useRouter Consciente del Locale
TypeScript1'use client'; 2 3import { useRouter } from 'next-intl/client'; 4 5function SearchForm() { 6 const router = useRouter(); 7 8 const handleSearch = (query: string) => { 9 router.push(`/search?q=${query}`); 10 // Incluye automáticamente el prefijo de locale 11 }; 12 13 return (/* form */); 14}
Selector de Idioma
TypeScript1'use client'; 2 3import { useLocale } from 'next-intl'; 4import { useRouter, usePathname } from 'next-intl/client'; 5import { locales, localeNames } from '@/i18n/config'; 6 7export function LanguageSwitcher() { 8 const locale = useLocale(); 9 const router = useRouter(); 10 const pathname = usePathname(); 11 12 const handleChange = (newLocale: string) => { 13 router.replace(pathname, { locale: newLocale }); 14 }; 15 16 return ( 17 <select value={locale} onChange={(e) => handleChange(e.target.value)}> 18 {locales.map((loc) => ( 19 <option key={loc} value={loc}> 20 {localeNames[loc]} 21 </option> 22 ))} 23 </select> 24 ); 25}
Gestión de Traducciones a Escala
A medida que tu app crece, gestionar archivos JSON manualmente se vuelve problemático:
- Traducciones faltantes son difíciles de rastrear
- Conflictos de fusión cuando múltiples desarrolladores editan archivos de traducción
- Sin contexto para traductores
- Flujo de trabajo lento con exportación/importación manual
Integración con IntlPull
IntlPull proporciona integración perfecta con next-intl:
Terminal1# Inicializar IntlPull en tu proyecto 2npx @intlpullhq/cli init 3 4# Subir tus traducciones existentes 5npx @intlpullhq/cli upload 6 7# Obtener traducciones de IA para nuevos idiomas 8npx @intlpullhq/cli translate --languages es,de,fr,ja 9 10# Bajar traducciones de vuelta a tu proyecto 11npx @intlpullhq/cli download 12 13# Modo watch para sincronización en tiempo real durante desarrollo 14npx @intlpullhq/cli watch
Beneficios:
- Traducción impulsada por IA con GPT-4, Claude, DeepL
- Editor visual con contexto de captura de pantalla
- Colaboración en equipo con flujos de revisión
- Sincronización automática con tu base de código
- Memoria de traducción para consistencia
Comienza gratis con IntlPull →
Patrones Comunes
Estados de Carga con Suspense
TypeScript1import { Suspense } from 'react'; 2 3export default function Page() { 4 return ( 5 <Suspense fallback={<Skeleton />}> 6 <TranslatedContent /> 7 </Suspense> 8 ); 9}
Límites de Error (Error Boundaries)
TypeScript1import { useTranslations } from 'next-intl'; 2 3function ErrorBoundary({ error }: { error: Error }) { 4 const t = useTranslations('errors'); 5 6 return ( 7 <div> 8 <h2>{t('title')}</h2> 9 <p>{t('message')}</p> 10 </div> 11 ); 12}
Rutas Dinámicas con Locales
TypeScript1// app/[locale]/blog/[slug]/page.tsx 2import { getTranslations } from 'next-intl/server'; 3 4export default async function BlogPost({ 5 params: { locale, slug }, 6}: { 7 params: { locale: string; slug: string }; 8}) { 9 const t = await getTranslations('blog'); 10 11 // Obtener contenido localizado 12 const post = await getPost(slug, locale); 13 14 return ( 15 <article>
