IntlPull
Tutorial
25 min read

Tutorial next-intl: Guía completa de Next.js i18n (vs next-i18next) 2026

Domina next-intl para Next.js App Router. Tutorial completo que cubre configuración, enrutamiento, Server Components, formateo, TypeScript y mejores prácticas de producción.

IntlPull Team
IntlPull Team
Jan 17, 2026
On this page
Summary

Domina next-intl para Next.js App Router. Tutorial completo que cubre configuración, enrutamiento, Server Components, formateo, TypeScript y mejores prácticas de producción.

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ísticanext-intlreact-i18nextreact-intl
Construido para Next.jsNo (general React)No (general React)
Server ComponentsNativoNecesita wrapperNecesita wrapper
Tamaño del bundle~2KB~8KB~12KB
Soporte App RouterDe primera claseParcialParcial
TypeScriptExcelenteBuenoBueno
Integración de enrutamientoIntegradoManualManual

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

Terminal
1npm 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:

TypeScript
1export 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:

TypeScript
1import { 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

JavaScript
1const 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:

TypeScript
1import 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:

JSON
1{
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:

JSON
1{
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:

TypeScript
1import { 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():

TypeScript
1import { 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():

TypeScript
1'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

TypeScript
1import { 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

JSON
1{
2  "greeting": "Hola, {name}!",
3  "items": "Tienes {count} artículos en tu carrito"
4}
TypeScript
t('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}}"
}
TypeScript
t('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}}"
}
TypeScript
t('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>."
}
TypeScript
1import { 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

TypeScript
1import { 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

TypeScript
1import { 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

TypeScript
1const 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

TypeScript
1const 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:

TypeScript
1import 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

TypeScript
1'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

TypeScript
1'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:

Terminal
1# 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

TypeScript
1import { 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)

TypeScript
1import { 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

TypeScript
1// 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>
Tags
next-intl
nextjs
i18n
internacionalización
app-router
server-components
react
2026
IntlPull Team
IntlPull Team
Engineering

Building tools to help teams ship products globally. Follow us for more insights on localization and i18n.