Next.js

Configuración i18n de next-intl en Next.js 16 con proxy.ts

Configura next-intl Next.js 16 con proxy.ts, async params, rutas locales estáticas y metadatos App Router i18n para páginas SaaS paso a paso.

By Mohamed DjoudirMay 29, 202614 min read
Compartir:
Configuración i18n de next-intl en Next.js 16 con proxy.ts
#next-intl-nextjs-16#nextjs-i18n#app-router#proxy-ts#setrequestlocale#locale-routing

Tu landing page SaaS está lista, pero la versión en alemán devuelve 404, el selector de idioma deja a los usuarios en la URL equivocada y un tutorial antiguo todavía te dice que crees middleware.ts. Las piezas móviles son pequeñas, pero deben encajar: proxy.ts, params asíncronos, enrutamiento por locale, generación estática y metadata. Esta guía muestra una configuración de next-intl con Next.js 16 segura para producción.

  • src/proxy.ts: Next.js 16 usa proxy.ts para la negociación de locale en tiempo de request; la convención de archivo middleware fue renombrada a Proxy y está obsoleta en Next.js 16.
  • params asíncronos: En app/[locale]/layout.tsx, tipa params como Promise<{locale: string}>, haz await, valida el locale y luego renderiza el provider.
  • Rutas estáticas por locale: Usa generateStaticParams() para devolver los locales soportados y llama a setRequestLocale(locale) antes de usar APIs de next-intl en un layout o página estática.
  • Navegación segura para SEO: Usa los helpers de routing de next-intl para que los links localizados, redirects, pathnames y URLs de idiomas alternativos se mantengan consistentes.
  • Payloads de cliente: Para apps más grandes, mantén la mayor parte del texto de la página en Server Components y pasa solo los namespaces de mensajes que necesitan los Client Components.

¿Qué es next-intl Next.js 16?

next-intl Next.js 16 es el patrón actual de integración con App Router para crear apps localizadas de Next.js con next-intl y src/proxy.ts.

El objetivo no es solo buscar traducciones. Una configuración de producción necesita URLs conscientes del locale, negociación de requests, params de ruta validados, renderizado estático cuando sea posible, metadata localizada y enlaces de idioma que los motores de búsqueda puedan entender.

La mayor fuente de bugs es copiar un árbol de archivos de la época de Next.js 15 en una app con Next.js 16. Los conceptos son familiares, pero la superficie de integración cambió lo suficiente como para que pequeños desajustes puedan provocar errores como Unable to find next-intl locale o hacer que rutas estáticas caigan en renderizado dinámico.

Por qué esta configuración de i18n con App Router difiere de guías antiguas

El App Router no te da por sí solo un sistema completo de traducción. Next.js soporta internacionalización mediante primitivas de routing, segmentos de ruta dinámicos, Proxy y static params. next-intl añade carga de mensajes, negociación de locale, helpers de routing tipados, APIs de traducción para server/client y enlaces alternativos orientados a SEO.

Para i18n en Next.js 16, el cambio más visible es el nombre del archivo de interceptación de requests. Usa src/proxy.ts en una configuración nueva. Las publicaciones antiguas todavía pueden mostrar middleware.ts, que era la convención de archivo anterior y ahora está obsoleta en Next.js 16.

Área Patrón Next.js 16 + next-intl Desajuste común en guías antiguas
Negociación de requests src/proxy.ts con createMiddleware(routing) middleware.ts en el árbol de archivos
Rutas por locale app/[locale]/... Páginas fuera del segmento de locale
Params de ruta params: Promise<{locale: string}> Tipado síncrono de params
i18n estático generateStaticParams() más setRequestLocale(locale) Solo generateStaticParams()
Navegación Helpers de createNavigation(routing) Concatenación manual de strings

Si ya tienes una configuración con Next.js 15, compárala con este artículo en lugar de cambiar nombres de archivos a ciegas. La guía anterior de Aniq UI sobre cómo añadir next-intl a Next.js 15 sigue siendo útil para los conceptos, pero este artículo está pensado primero para Next.js 16.

Instala next-intl y añade el plugin de Next.js

Empieza con un proyecto actual de App Router. Next.js 16 requiere Node >=20.9.0, y las apps actuales de React usan React 19. Instala next-intl normalmente:

npm install next-intl

Añade el plugin de next-intl a next.config.ts:

import createNextIntlPlugin from 'next-intl/plugin';
import type {NextConfig} from 'next';

const nextConfig: NextConfig = {};

const withNextIntl = createNextIntlPlugin();

export default withNextIntl(nextConfig);

Una estructura pequeña de archivos para producción mantiene la configuración fácil de entender:

src/
  app/
    [locale]/
      layout.tsx
      page.tsx
  i18n/
    request.ts
    routing.ts
    navigation.ts
  proxy.ts
messages/
  en.json
  de.json

Crea primero los archivos de mensajes para que la configuración de request tenga algo real que cargar:

{
  "HomePage": {
    "title": "Ship your product in more languages"
  },
  "Metadata": {
    "title": "Localized SaaS landing page",
    "description": "A localized landing page built with Next.js and next-intl."
  }
}

Mantén los namespaces predecibles. Un patrón común es usar un namespace por cada ruta o feature, más un namespace Metadata para títulos y descripciones.

Configura routing basado en locale con src/proxy.ts

El routing basado en locale empieza con una configuración compartida de routing. Pon todos los locales soportados en un solo archivo y reutiliza esa configuración en el resto de la app.

// src/i18n/routing.ts
import {defineRouting} from 'next-intl/routing';

export const routing = defineRouting({
  locales: ['en', 'de'],
  defaultLocale: 'en'
});

Luego, crea el archivo Proxy. Aquí es donde next-intl puede negociar el locale, redirigir o reescribir requests, y proporcionar comportamiento de routing basado en el locale activo.

// src/proxy.ts
import createMiddleware from 'next-intl/middleware';
import {routing} from './i18n/routing';

export default createMiddleware(routing);

export const config = {
  matcher: '/((?!api|trpc|_next|_vercel|.*\\..*).*)'
};

El matcher excluye rutas de API, internos de Next.js, internos de Vercel y requests a archivos con extensiones. Eso evita que el locale middleware se ejecute sobre assets como imágenes, fuentes y source maps.

Ahora añade la configuración de request. Aquí es donde se lee el locale y se cargan los mensajes para el request activo.

// src/i18n/request.ts
import {getRequestConfig} from 'next-intl/server';
import {hasLocale} from 'next-intl';
import {routing} from './routing';

export default getRequestConfig(async ({requestLocale}) => {
  const requested = await requestLocale;
  const locale = hasLocale(routing.locales, requested)
    ? requested
    : routing.defaultLocale;

  return {
    locale,
    messages: (await import(`../../messages/${locale}.json`)).default
  };
});

Este fallback es importante. Proxy normalmente debería proporcionar un locale válido, pero la configuración de request aún debe manejar de forma segura un valor inválido o ausente. La documentación de next-intl también recomienda reemplazar valores inválidos de [locale] por un locale válido en la configuración de request y llamar a notFound() en el root layout cuando corresponda.

Crea el layout [locale] con params asíncronos

En el App Router, tu segmento de ruta localizado debe envolver las páginas que necesitan traducciones. Para la mayoría de sitios de marketing SaaS, eso significa mover las páginas públicas bajo app/[locale].

El detalle importante de Next.js 16 es la forma de params: la prop es una Promise, así que haz await sobre la prop antes de usar locale.

// src/app/[locale]/layout.tsx
import {NextIntlClientProvider, hasLocale} from 'next-intl';
import {setRequestLocale} from 'next-intl/server';
import {notFound} from 'next/navigation';
import {routing} from '../../i18n/routing';

type Props = {
  children: React.ReactNode;
  params: Promise<{locale: string}>;
};

export function generateStaticParams() {
  return routing.locales.map((locale) => ({locale}));
}

export default async function LocaleLayout({children, params}: Props) {
  const {locale} = await params;

  if (!hasLocale(routing.locales, locale)) {
    notFound();
  }

  // Enables static rendering for next-intl APIs in this locale scope.
  setRequestLocale(locale);

  return (
    <html lang={locale}>
      <body>
        <NextIntlClientProvider>{children}</NextIntlClientProvider>
      </body>
    </html>
  );
}

Hay tres trabajos separados en este layout:

  • generateStaticParams() le dice a Next.js qué params de ruta por locale pueden generarse estáticamente.
  • hasLocale() valida el param de ruta antes de que la página se renderice.
  • setRequestLocale(locale) hace que el locale esté disponible para las APIs de next-intl durante el renderizado estático.

No reemplaces la validación de locale por una comprobación flexible de string. La configuración de routing es la fuente de verdad, y hasLocale() mantiene la comprobación en runtime alineada con esa configuración.

Usa traducciones en Server y Client Components

next-intl soporta tanto Server Components como Client Components, pero la superficie de API depende de dónde se ejecute el componente.

Para componentes compartidos o de servidor que no son async, useTranslations es válido:

// src/app/[locale]/page.tsx
import {useTranslations} from 'next-intl';

export default function HomePage() {
  const t = useTranslations('HomePage');

  return (
    <main className='mx-auto max-w-3xl px-6 py-24'>
      <h1 className='text-4xl font-semibold tracking-tight'>
        {t('title')}
      </h1>
    </main>
  );
}

Para Server Components async, usa getTranslations desde next-intl/server:

import {getTranslations} from 'next-intl/server';

export default async function PricingPage() {
  const t = await getTranslations('PricingPage');

  return <h1>{t('title')}</h1>;
}

Usa Client Components solo cuando necesites estado del lado del cliente, APIs del navegador o hooks interactivos. El provider en el layout hace que los mensajes estén disponibles para los Client Components debajo de él, pero eso no significa que cada componente traducido deba convertirse en un Client Component. next-intl documenta getTranslations, getMessages, getLocale y APIs awaitable relacionadas para Server Components async, mientras que hooks como useTranslations funcionan en componentes compartidos regulares según dónde se ejecuten.

Una regla práctica para landing pages es sencilla: mantén las secciones con mucho texto como Server Components y aísla las partes interactivas como menús, tabs y selectores de idioma.

Habilita renderizado estático con i18n en generateStaticParams

El renderizado estático suele ser una buena opción para páginas de marketing, documentación, páginas de precios y páginas SaaS públicas donde el contenido cambia mediante despliegues o revalidación, no por request.

Para i18n con generateStaticParams, devolver locales es necesario pero no suficiente cuando se usan APIs de next-intl. Llama a setRequestLocale(locale) en cada layout o página que deba renderizarse estáticamente y use APIs de next-intl. La llamada debe ocurrir antes de useTranslations, getMessages u otras APIs de next-intl que dependen del locale activo.

// src/app/[locale]/features/page.tsx
import {getTranslations, setRequestLocale} from 'next-intl/server';
import {routing} from '../../../i18n/routing';

type Props = {
  params: Promise<{locale: string}>;
};

export function generateStaticParams() {
  return routing.locales.map((locale) => ({locale}));
}

export default async function FeaturesPage({params}: Props) {
  const {locale} = await params;

  setRequestLocale(locale);

  const t = await getTranslations('FeaturesPage');

  return <h1>{t('title')}</h1>;
}

Para renderizado estático, llama a setRequestLocale(locale) en cada layout o página relevante antes de usar APIs de next-intl. Esto evita caer en renderizado dinámico cuando next-intl necesita el locale activo.

Aquí también aparecen muchos bugs de actualización. Los desarrolladores añaden generateStaticParams() y aun así ven comportamiento dinámico porque el locale no se estableció explícitamente para el scope de next-intl.

Añade metadata localizada, navegación y comprobaciones de hreflang

La metadata localizada debe usar el mismo param de locale que la página. En funciones de metadata async, usa getTranslations y pasa el locale explícitamente.

// src/app/[locale]/page.tsx
import type {Metadata} from 'next';
import {getTranslations} from 'next-intl/server';
import {useTranslations} from 'next-intl';

type Props = {
  params: Promise<{locale: string}>;
};

export async function generateMetadata({params}: Props): Promise<Metadata> {
  const {locale} = await params;
  const t = await getTranslations({locale, namespace: 'Metadata'});

  return {
    title: t('title'),
    description: t('description'),
    alternates: {
      languages: {
        en: '/en',
        de: '/de'
      }
    }
  };
}

export default function HomePage() {
  const t = useTranslations('HomePage');
  return <h1>{t('title')}</h1>;
}

Para navegación, evita construir manualmente strings como /${locale}/pricing. Exporta una vez los helpers de navegación de next-intl:

// src/i18n/navigation.ts
import {createNavigation} from 'next-intl/navigation';
import {routing} from './routing';

export const {Link, redirect, usePathname, useRouter, getPathname} =
  createNavigation(routing);

Luego usa el componente Link localizado:

import {Link} from '../../i18n/navigation';

export function Header() {
  return (
    <nav className='flex gap-4'>
      <Link href='/'>Home</Link>
      <Link href='/pricing'>Pricing</Link>
      <Link href='/pricing' locale='de'>Deutsch</Link>
    </nav>
  );
}

Antes de lanzar, revisa tres básicos de SEO:

  • Cada página localizada debe tener una URL clara, como /en/pricing o /de/pricing.
  • El valor de <html lang> debe coincidir con el locale activo.
  • Los enlaces de idiomas alternativos deben apuntar a contenido equivalente, no solo a la homepage localizada.

Si estás combinando esto con una landing page de alta conversión, los mismos principios aplican a la estructura y el rendimiento. La publicación de Aniq UI sobre optimización de rendimiento en Next.js es un complemento útil cuando las páginas localizadas empiezan a crecer.

Escalar next-intl para apps más grandes

La configuración anterior está bien para blogs y landing pages. Pero a medida que tu app crece, un único messages/en.json puede convertirse en un cuello de botella para la experiencia de desarrollo, y si pasas el objeto completo de mensajes a Client Components, también aumenta el payload serializado de cliente que se envía al navegador. Tu homepage no debería serializar mensajes de AboutPage o CheckoutPage si esa ruta nunca los usa.

Dividir tus mensajes en archivos por namespace ayuda a la organización, pero por sí solo no corrige la fuga: si vuelves a unir todos los archivos en request.ts, el catálogo completo todavía se carga del lado del servidor. La solución real es usar mensajes selectivos en el provider y mantener el texto de la página en Server Components.

En next-intl v4, NextIntlClientProvider hereda la configuración de request.ts por defecto, así que si tu configuración de request carga el catálogo completo, el provider puede exponer todo el objeto de mensajes a Client Components a menos que optes por salir de ese comportamiento. Establece messages={null} en la raíz. Los Server Components siguen funcionando, y no se pasan mensajes automáticamente al cliente:

// app/[locale]/layout.tsx
// Keep hasLocale() validation and setRequestLocale() as shown earlier

<NextIntlClientProvider messages={null}>
  {children}
</NextIntlClientProvider>

Luego envuelve solo las islas de cliente que necesitan traducciones, pasando solo sus namespaces:

import pick from 'lodash/pick';
import {NextIntlClientProvider} from 'next-intl';
import {getMessages} from 'next-intl/server';

export default async function HomePage() {
  const messages = await getMessages();

  return (
    <>
      <Hero /> {/* Server Component, no client message payload */}

      <NextIntlClientProvider
        messages={pick(messages, ['Common', 'PricingCalculator'])}
      >
        <PricingCalculator />
      </NextIntlClientProvider>
    </>
  );
}

El provider debe incluir todos los namespaces que usa el Client Component. Los providers anidados no hacen deep-merge de mensajes, así que si PricingCalculator también llama a useTranslations('Common'), selecciona tanto Common como PricingCalculator. La documentación de next-intl describe las props del provider como atómicas y señala que messages debe fusionarse manualmente cuando sea necesario.

Una advertencia: cuando seleccionas un subconjunto, TypeScript puede marcar la prop messages porque el tipo aumentado AppConfig['Messages'] espera la forma completa. Es una aspereza conocida de v4, no un error en tu configuración.

Mantén la UI compartida en namespaces pequeños como Common, Navigation y Footer, y mantén el contenido específico de una ruta cerca de su ruta. No necesitas esto para un sitio pequeño, pero para dashboards SaaS y apps multilingües grandes, mantiene los payloads de cliente intencionales y los archivos de traducción mantenibles.

Errores comunes

  • Usar middleware.ts en una app nueva con Next.js 16 → la capa de request puede no coincidir con la documentación actual → crea src/proxy.ts y exporta createMiddleware(routing).
  • Tipar params como {locale: string} → el layout ya no coincide con la forma actual de props del App Router → típalo como Promise<{locale: string}> y haz await.
  • Omitir hasLocale() → segmentos de locale inválidos pueden renderizar contenido inesperado → valida contra routing.locales y llama a notFound().
  • Añadir solo generateStaticParams() → next-intl aún puede necesitar que el locale se establezca en el scope de renderizado → llama a setRequestLocale(locale) antes de las APIs de traducción.
  • Construir URLs de idioma a mano → los links pueden romperse cuando cambien las reglas de routing → usa los helpers de createNavigation(routing).
  • Pasar todos los mensajes al provider de cliente raíz en una app grande → los Client Components pueden recibir namespaces que nunca usan → establece messages={null} en la raíz y pasa namespaces seleccionados a las islas de cliente.

FAQ

¿El App Router de Next.js tiene soporte i18n integrado?

El App Router de Next.js proporciona bloques de construcción para i18n, no un framework completo de traducción. Soporta patrones como negociación basada en Proxy, segmentos dinámicos [locale] y generateStaticParams() para rutas por locale. Librerías como next-intl añaden carga de mensajes, APIs de traducción, navegación localizada y manejo de idiomas alternativos.

¿Cómo habilito renderizado estático con i18n en el App Router?

Habilita rutas i18n estáticas devolviendo cada locale desde generateStaticParams() y estableciendo el locale activo antes de que se ejecuten las APIs de traducción. Con next-intl, eso normalmente significa llamar a setRequestLocale(locale) en el layout o página [locale] relevante antes de useTranslations, getMessages o APIs similares.

¿Cómo funcionan las etiquetas hreflang con next-intl?

next-intl puede ayudar a producir enlaces de idiomas alternativos mediante su routing middleware y su configuración de navegación localizada. Para SEO, cada URL alternativa debe representar el mismo contenido en otro idioma, como /en/pricing y /de/pricing. La página también debe establecer el valor correcto de <html lang>.

¿Cómo soluciono “Unable to find next-intl locale because the middleware didn’t run on this request”?

Este error normalmente significa que la negociación de request de next-intl no se ejecutó para la ruta que se está renderizando. En Next.js 16, confirma que src/proxy.ts existe, exporta createMiddleware(routing) y tiene un matcher que incluye la página localizada. También confirma que la página vive bajo app/[locale] cuando usas routing basado en locale.

¿Por qué Next.js 16 renombró middleware.ts a proxy.ts?

Next.js 16 usa la convención de archivo proxy.ts para la capa de interceptación de requests que muchas guías antiguas llamaban middleware.ts. La documentación indica que Middleware fue renombrado a Proxy para reflejar mejor su propósito, y la funcionalidad sigue siendo la misma. En apps nuevas, coloca el export de next-intl middleware en src/proxy.ts y evita mezclar ambos nombres en un mismo tutorial o árbol de archivos.

¿Cómo configuro next-intl Next.js 16 para una landing page SaaS?

Configura routing por locale con defineRouting, crea src/proxy.ts con createMiddleware(routing), envuelve las páginas en app/[locale]/layout.tsx y carga mensajes mediante i18n/request.ts. Para una landing page SaaS, añade también metadata localizada, valida locales y usa los helpers de navegación de next-intl para links de idioma.

¿Debería dividir los mensajes de next-intl por ruta?

Para sitios pequeños, un único archivo de mensajes por locale está bien. Para apps más grandes, divide los mensajes por ruta o feature para mejorar la experiencia de desarrollo, pero no fusiones el catálogo completo y lo pases a cada Client Component. Mantén la mayor parte del contenido traducido en Server Components, establece messages={null} en el provider raíz y pasa solo namespaces seleccionados a las islas de cliente que los necesiten.

Conclusión

Una configuración multilingüe fiable con App Router se reduce a unas pocas decisiones precisas: usa src/proxy.ts, haz await y valida los params de locale, mantén centralizada la configuración de routing y combina generateStaticParams() con setRequestLocale() cuando el renderizado estático importe. Una vez que esas piezas están en su lugar, la metadata y los enlaces de idioma se vuelven mucho más fáciles de razonar.

Para apps SaaS más grandes, el siguiente paso es la disciplina de payload: mantén el copy de la página en el servidor, opta por no pasar mensajes de cliente automáticamente en la raíz y pasa solo los namespaces que necesitan tus islas interactivas. Si estás localizando una landing page SaaS o una UI de dashboard, empezar desde una plantilla de Aniq UI puede ahorrarte tiempo en el layout alrededor mientras te enfocas en la capa de i18n.

¿Encontró útil este artículo?

Compartir:
world map

Comunidad Global de Usuarios

Únete a miles de desarrolladores en todo el mundo que confían en Aniq-UI para sus proyectos. Nuestras plantillas se utilizan en todo el mundo para crear experiencias web impresionantes.

Contáctanos

Need custom work or reskin? Get in touch with us

Aniq-uiAniq-uiAniq-ui