InicioEntorno de pruebasExhibiciónAppDocBlog
    • Englishinglés
      EN
    • русскийruso
      RU
    • 日本語japonés
      JA
    • françaisfrancés
      FR
    • 한국어coreano
      KO
    • 中文chino
      ZH
    • españolespañol
      ES
    • Deutschalemán
      DE
    • العربيةárabe
      AR
    • italianoitaliano
      IT
    • British Englishinglés británico
      EN-GB
    • portuguêsportugués
      PT
    • हिन्दीhindi
      HI
    • Türkçeturco
      TR
    • polskipolaco
      PL
    • Indonesiaindonesio
      ID
    • Tiếng Việtvietnamita
      VI
    • українськаucraniano
      UK
    /
    Alt+←
    ¿Qué es la internacionalización (i18n)?
    SEO y i18n
    Guía
    • i18n usando next-i18next
    • i18n usando next-intl
    Utiliza Intlayer en tu solución
    • Automatizar next-i18next
    • Automatizar react-i18next
    • Automatizar next-intl
    • Automatizar react-intl
    • Automatizar vue-i18n
    Comparaciones
    • next-i18next vs next-intl vs Intlayer
    • react-i18next vs react-intl vs Intlayer
    Documentación
    1. Blog
    2. Nextjs internationalization using next intl
    Creación:2025-11-01Última actualización:2025-11-01
    Ver la plantilla de aplicación en GitHub

    Esta página tiene una plantilla de aplicación disponible.

    Referencia esta doc a tu asistente AI favorito
    ChatGPT
    Claude
    DeepSeek
    Google AI mode
    Gemini
    Perplexity
    Mistral
    Grok

    Haz tu pregunta y obtén un resumen del documento referenciando esta página y el proveedor AI de tu elección

    Historial de versiones

    1. Versión inicial
      v7.0.01/11/2025

    El contenido de esta página ha sido traducido con una IA.

    Ver la última versión del contenido original en inglés
    Editar esta documentación

    Si tienes una idea para mejorar esta documentación, no dudes en contribuir enviando una pull request en GitHub.

    Enlace de GitHub a la documentación
    Copiar

    Copiar el Markdown del documento a la portapapeles

    Cómo internacionalizar tu aplicación Next.js usando next-intl en 2026

    Tabla de Contenidos

    ¿Qué es next-intl?

    next-intl es una biblioteca popular de internacionalización (i18n) diseñada específicamente para Next.js App Router. Proporciona una forma fluida de construir aplicaciones Next.js multilingües con excelente soporte para TypeScript y optimizaciones integradas.

    Si lo prefieres, también puedes consultar la guía de next-i18next, o usar directamente Intlayer.
    Consulta la comparación en next-i18next vs next-intl vs Intlayer.

    Prácticas que debes seguir

    Antes de sumergirnos en la implementación, aquí hay algunas prácticas que debes seguir:

    • Configura los atributos HTML lang y dir
      En tu layout, calcula dir usando getLocaleDirection(locale) y establece <html lang={locale} dir={dir}> para una accesibilidad y SEO adecuados.
    • Divide los mensajes por namespace
      Organiza los archivos JSON por locale y namespace (por ejemplo, common.json, about.json) para cargar solo lo que necesitas.
    • Minimiza la carga en el cliente
      En las páginas, envía solo los namespaces requeridos a NextIntlClientProvider (por ejemplo, pick(messages, ['common', 'about'])).
    • Prefiere páginas estáticas
      Usa páginas estáticas tanto como sea posible para un mejor rendimiento y SEO.
    • I18n en componentes del servidor
      Los componentes del servidor, como las páginas o todos los componentes que no están marcados como client, son estáticos y pueden pre-renderizarse en tiempo de compilación. Por lo tanto, tendremos que pasar las funciones de traducción a ellos como props.
    • Configura los tipos de TypeScript
      Para tus locales, para asegurar la seguridad de tipos en toda tu aplicación.
    • Proxy para redirección
      Usa un proxy para manejar la detección de locale y el enrutamiento, y redirigir al usuario a la URL con el prefijo de locale adecuado.
    • Internacionalización de tus metadatos, sitemap, robots.txt
      Internacionaliza tus metadatos, sitemap, robots.txt usando la función generateMetadata proporcionada por Next.js para asegurar un mejor descubrimiento por los motores de búsqueda en todos los locales.
    • Localiza los enlaces
      Localiza los enlaces usando el componente Link para redirigir al usuario a la URL con el prefijo de locale adecuado. Es importante asegurar el descubrimiento de tus páginas en todos los locales.
    • Automatiza pruebas y traducciones
      Automatizar pruebas y traducciones ayuda a ahorrar tiempo en el mantenimiento de tu aplicación multilingüe.
    Consulta nuestra documentación que lista todo lo que necesitas saber sobre internacionalización y SEO: Internationalization (i18n) with next-intl.

    Guía paso a paso para configurar next-intl en una aplicación Next.js

    ide.intlayer.org

    Consulta la Plantilla de Aplicación en GitHub.

    Aquí está la estructura del proyecto que crearemos:

    bash
    Copiar código

    Copiar el código al portapapeles

    .├── global.ts├── locales│   ├── en│   │  ├── common.json│   │  └── about.json│   ├── fr│   │  ├── common.json│   │  └── about.json│   └── es│      ├── common.json│      └── about.json└── src # Src es opcional    ├── proxy.ts    ├── app    │   ├── i18n.ts    │   └── [locale]    │       ├── layout.tsx    │       ├── (home) # / (Grupo de rutas para no saturar todas las páginas con recursos de home)    │       │   ├── layout.tsx    │       │   └── page.tsx    │       └── about # /about    │           ├── layout.tsx    │           └── page.tsx    └── components        ├── ClientComponent.tsx        └── ServerComponent.tsx

    Paso 1: Instalar Dependencias

    Instala los paquetes necesarios usando npm:

    bash
    Copiar código

    Copiar el código al portapapeles

    npm install next-intl
    • next-intl: La biblioteca principal de internacionalización para Next.js App Router que proporciona hooks, funciones del servidor y proveedores del cliente para gestionar las traducciones.

    Paso 2: Configura tu Proyecto

    Crea un archivo de configuración que defina los locales soportados y configure la solicitud de next-intl. Este archivo sirve como la única fuente de verdad para tu configuración i18n y asegura la seguridad de tipos en toda tu aplicación.

    Centralizar la configuración de locales previene inconsistencias y facilita agregar o eliminar locales en el futuro. La función getRequestConfig se ejecuta en cada solicitud y carga solo las traducciones necesarias para cada página, permitiendo la división de código y reduciendo el tamaño del paquete.

    src/i18n.ts
    Copiar código

    Copiar el código al portapapeles

    import { notFound } from "next/navigation";import createMiddleware from "next-intl/middleware";import { createNavigation } from "next-intl/navigation";// Define los locales soportados con seguridad de tiposexport const locales = ["en", "fr", "es"] as const;export type Locale = (typeof locales)[number];export const defaultLocale: Locale = "en";export function isRTL(locale: Locale | (string & {})) {  // Devuelve true si el locale es un idioma que se escribe de derecha a izquierda  return /^(ar|fa|he|iw|ur|ps|sd|ug|yi|ckb|ku)(-|$)/i.test(locale);}// Carga mensajes dinámicamente por locale para habilitar code-splitting// Promise.all carga los namespaces en paralelo para mejor rendimientoasync function loadMessages(locale: Locale) {  // Carga solo los namespaces que tu layout/páginas necesitan  const [common, home, about] = await Promise.all([    import(`../locales/${locale}/common.json`).then((m) => m.default),    import(`../locales/${locale}/home.json`).then((m) => m.default),    import(`../locales/${locale}/about.json`).then((m) => m.default),    // ... Los futuros archivos JSON deberían añadirse aquí  ]);  return { common, home, about } as const;}// Ayudante para generar URLs localizadas (por ejemplo, /about vs /fr/about)export function localizedPath(locale: string, path: string) {  return locale === defaultLocale ? path : `/${locale}${path}`;}// getRequestConfig se ejecuta en cada solicitud y proporciona mensajes a los componentes del servidor// Aquí es donde next-intl se conecta con el renderizado del lado servidor de Next.jsexport default async function getRequestConfig({  requestLocale,}: {  requestLocale: Promise<string | undefined>;}) {  const requested: Locale = ((await requestLocale) as Locale) ?? defaultLocale;  if (!locales.includes(requested)) notFound();  return {    locale: requested,    messages: await loadMessages(requested),  };}export function getCookie(locale: Locale) {  return [    `NEXT_LOCALE=${locale}`,    "Path=/",    `Max-Age=${60 * 60 * 24 * 365}`, // 1 año    "SameSite=Lax",  ].join("; ");}const routingOptions = {  locales,  defaultLocale,  localePrefix: "as-needed", // Cambiar ruta /en/... a /...  // Opcional: rutas localizadas  // pathnames: {  //   '/': '/',  //   '/about': {en: '/about', fr: '/a-propos', es: '/acerca-de'},  //   '/blog/[slug]': '/blog/[slug]'  // }  //  localeDetection: true, // evitar redirecciones de "/" a "/en" desde cookie} as const;export const { Link, redirect, usePathname, useRouter, getPathname } =  createNavigation(routingOptions);export const proxy = createMiddleware(routingOptions);

    Paso 3: Definir Rutas Dinámicas por Locales

    Configura el enrutamiento dinámico para las locales creando un directorio [locale] en tu carpeta de la aplicación. Esto permite que Next.js maneje el enrutamiento basado en la localización donde cada locale se convierte en un segmento de la URL (por ejemplo, /en/about, /fr/about).

    Usar rutas dinámicas permite que Next.js genere páginas estáticas para todas las locales en tiempo de compilación, mejorando el rendimiento y el SEO. El componente layout establece los atributos HTML lang y dir basados en la locale, lo cual es crucial para la accesibilidad y la comprensión por parte de los motores de búsqueda.

    src/app/[locale]/layout.tsx
    Copiar código

    Copiar el código al portapapeles

    import type { ReactNode } from "react";import { locales } from "@/i18n";import { getLocaleDirection, setRequestLocale } from "next-intl/server";// Pre-generar páginas estáticas para todas las locales en tiempo de compilación (SSG)// Esto mejora el rendimiento y el SEOexport function generateStaticParams() {  return locales.map((locale) => ({ locale }));}export default function LocaleLayout({  children,  params,}: {  children: ReactNode;  params: Promise<{ locale: string }>;}) {  // En Next.js App Router, params es una Promise (se puede await)  // Esto permite que los segmentos de ruta dinámicos se resuelvan de forma asíncrona  const { locale } = await params;  // Crítico: setRequestLocale indica a next-intl qué locale usar para esta solicitud  // Sin esto, getTranslations() no sabrá qué locale usar en los componentes del servidor  setRequestLocale(locale);  // Obtener la dirección del texto (LTR/RTL) para el renderizado correcto del HTML  const dir = getLocaleDirection(locale);  return (    <html lang={locale} dir={dir}>      <body>{children}</body>    </html>  );}
    src/app/[locale]/about/page.tsx
    Copiar código

    Copiar el código al portapapeles

    import { getTranslations, getMessages, getFormatter } from "next-intl/server";import { NextIntlClientProvider } from "next-intl";import pick from "lodash/pick";import ServerComponent from "@/components/ServerComponent";import ClientComponent from "@/components/ClientComponent";export default async function AboutPage({  params,}: {  params: Promise<{ locale: string }>;}) {  const { locale } = await params;  // Los mensajes se cargan del lado del servidor. Solo se envía al cliente lo necesario.  // Esto minimiza el paquete de JavaScript enviado al navegador  const messages = await getMessages();  const clientMessages = pick(messages, ["common", "about"]);  // Traducciones/formateo estrictamente del lado del servidor  // Estos se ejecutan en el servidor y pueden pasarse como props a los componentes  const tAbout = await getTranslations("about");  const tCounter = await getTranslations("about.counter");  const format = await getFormatter();  const initialFormattedCount = format.number(0);  return (    // NextIntlClientProvider hace que las traducciones estén disponibles para los componentes cliente    // Solo pasa los namespaces que tus componentes cliente realmente usan    <NextIntlClientProvider locale={locale} messages={clientMessages}>      <main>        <h1>{tAbout("title")}</h1>        <ClientComponent />        <ServerComponent          formattedCount={initialFormattedCount}          label={tCounter("label")}          increment={tCounter("increment")}        />      </main>    </NextIntlClientProvider>  );}

    Paso 4: Crea tus archivos de traducción

    Crea archivos JSON para cada locale y namespace. Esta estructura te permite organizar las traducciones de manera lógica y cargar solo lo que necesitas para cada página.

    Organizar las traducciones por namespace (por ejemplo, common.json, about.json) permite hacer code splitting y reduce el tamaño del bundle. Solo cargas las traducciones necesarias para cada página, mejorando el rendimiento.

    locales/en/common.json
    Copiar código

    Copiar el código al portapapeles

    {  "welcome": "Welcome",  "greeting": "Hello, world!"}
    locales/fr/common.json
    Copiar código

    Copiar el código al portapapeles

    {  "welcome": "Bienvenue",  "greeting": "Bonjour le monde!"}
    locales/en/about.json
    Copiar código

    Copiar el código al portapapeles

    {  "title": "About",  "description": "About page description",  "counter": {    "label": "Counter",    "increment": "Increment"  }}
    locales/fr/about.json
    Copiar código

    Copiar el código al portapapeles

    {  "title": "À propos",  "description": "Description de la page À propos",  "counter": {    "label": "Compteur",    "increment": "Incrémenter"  }}

    Paso 5: Utiliza las Traducciones en Tus Páginas

    Crea un componente de página que cargue las traducciones en el servidor y las pase tanto a los componentes del servidor como del cliente. Esto asegura que las traducciones se carguen antes de renderizar y previene el parpadeo de contenido.

    La carga de traducciones del lado del servidor mejora el SEO y previene el FOUC (Flash of Untranslated Content). Al usar pick para enviar solo los namespaces requeridos al proveedor del cliente, minimizamos el paquete JavaScript enviado al navegador.

    src/app/[locale]/about/page.tsx
    Copiar código

    Copiar el código al portapapeles

    import { getTranslations, getMessages, getFormatter } from "next-intl/server";import { NextIntlClientProvider } from "next-intl";import pick from "lodash/pick";import ServerComponent from "@/components/ServerComponent";import ClientComponent from "@/components/ClientComponent";export default async function AboutPage({  params,}: {  params: Promise<{ locale: string }>;}) {  const { locale } = await params;  // Los mensajes se cargan del lado del servidor. Solo se envía al cliente lo que se necesita.  // Esto minimiza el paquete de JavaScript enviado al navegador  const messages = await getMessages();  const clientMessages = pick(messages, ["common", "about"]);  // Traducciones/formateo estrictamente del lado del servidor  // Estos se ejecutan en el servidor y pueden pasarse como props a los componentes  const tAbout = await getTranslations("about");  const tCounter = await getTranslations("about.counter");  const format = await getFormatter();  const initialFormattedCount = format.number(0);  return (    // NextIntlClientProvider hace que las traducciones estén disponibles para los componentes cliente    // Solo pasa los namespaces que tus componentes cliente realmente usan    <NextIntlClientProvider locale={locale} messages={clientMessages}>      <main>        <h1>{tAbout("title")}</h1>        <ClientComponent />        <ServerComponent          formattedCount={initialFormattedCount}          label={tCounter("label")}          increment={tCounter("increment")}        />      </main>    </NextIntlClientProvider>  );}

    Paso 6: Usar Traducciones en Componentes Cliente

    Los componentes cliente pueden usar los hooks useTranslations y useFormatter para acceder a las traducciones y funciones de formato. Estos hooks leen del contexto NextIntlClientProvider.

    Los componentes cliente necesitan hooks de React para acceder a las traducciones. Los hooks useTranslations y useFormatter se integran perfectamente con next-intl y proporcionan actualizaciones reactivas cuando cambia la configuración regional.

    No olvides agregar los namespaces requeridos a los mensajes cliente de la página (solo incluye los namespaces que tus componentes cliente realmente necesitan).
    src/components/ClientComponent.tsx
    Copiar código

    Copiar el código al portapapeles

    "use client";import React, { useState } from "react";import { useTranslations, useFormatter } from "next-intl";const ClientComponent = () => {  // Alcance directamente al objeto anidado  // useTranslations/useFormatter son hooks que leen del contexto NextIntlClientProvider  // Solo funcionan si el componente está envuelto en NextIntlClientProvider  const t = useTranslations("about.counter");  const format = useFormatter();  const [count, setCount] = useState(0);  return (    <div>      <p>{format.number(count)}</p>      <button        aria-label={t("label")}        onClick={() => setCount((count) => count + 1)}      >        {t("increment")}      </button>    </div>  );};

    Paso 7: Usar traducciones en componentes del servidor

    Los componentes del servidor no pueden usar hooks de React, por lo que reciben las traducciones y los formateadores a través de props desde sus componentes padres. Este enfoque mantiene los componentes del servidor síncronos y permite que se aniden dentro de componentes cliente.

    Los componentes del servidor que podrían estar anidados bajo límites de cliente necesitan ser síncronos. Al pasar cadenas traducidas y valores formateados como props, evitamos operaciones asíncronas y aseguramos un renderizado adecuado. Pre-calcula las traducciones y el formato en el componente padre de la página.

    src/components/ServerComponent.tsx
    Copiar código

    Copiar el código al portapapeles

    // Los componentes del servidor anidados dentro de componentes cliente deben ser síncronos// React no puede serializar funciones asíncronas a través del límite servidor/cliente// Solución: pre-calcular traducciones/formateos en el padre y pasarlos como propstype ServerComponentProps = {  formattedCount: string;  label: string;  increment: string;};const ServerComponent = ({  formattedCount,  label,  increment,}: ServerComponentProps) => {  return (    <div>      <p>{formattedCount}</p>      <button aria-label={label}>{increment}</button>    </div>  );};
    En tu página/layout, usa getTranslations y getFormatter de next-intl/server para precomputar traducciones y formatos, luego pásalos como props a los componentes del servidor.

    (Opcional) Paso 8: Cambiar el idioma de tu contenido

    Para cambiar el idioma de tu contenido con next-intl, renderiza enlaces conscientes del locale que apunten al mismo pathname mientras cambias el locale. El provider reescribe las URLs automáticamente, así que solo tienes que apuntar a la ruta actual.

    src/components/LocaleSwitcher.tsx
    Copiar código

    Copiar el código al portapapeles

    "use client";import Link from "next/link";import { usePathname } from "next/navigation";import { useLocale } from "next-intl";import { defaultLocale, getCookie, type Locale, locales } from "@/i18n";const getLocaleLabel = (locale: Locale): string => {  try {    const displayNames = new Intl.DisplayNames([locale], { type: "language" });    return displayNames.of(locale) ?? locale.toUpperCase();  } catch {    return locale.toUpperCase();  }};const localeFlags: Record<Locale, string> = {  en: "🇬🇧",  fr: "🇫🇷",  es: "🇪🇸",};export default function LocaleSwitcher() {  const activeLocale = useLocale();  const pathname = usePathname();  // Eliminar el prefijo de la localidad del pathname para obtener la ruta base  const getBasePath = (path: string) => {    for (const locale of locales) {      if (path.startsWith(`/${locale}`)) {        return path.slice(locale.length + 1) || "/";      }    }    return path;  };  const basePath = getBasePath(pathname);  return (    <nav aria-label="Selector de idioma">      <div>        {(locales as readonly Locale[]).map((locale) => {          const isActive = locale === activeLocale;          // Construir el href según si es el locale por defecto          const href =            locale === defaultLocale ? basePath : `/${locale}${basePath}`;          return (            <Link              key={locale}              href={href}              aria-current={isActive ? "page" : undefined}              onClick={() => {                document.cookie = getCookie(locale);              }}            >              <span>{localeFlags[locale]}</span>              <span>{getLocaleLabel(locale)}</span>              <span>{locale.toUpperCase()}</span>            </Link>          );        })}      </div>    </nav>  );}

    (Opcional) Paso 9: Usar el componente Link localizado

    next-intl proporciona un subpaquete next-intl/navigation que contiene un componente Link localizado que aplica automáticamente la locale activa. Ya lo hemos extraído para ti en el archivo @/i18n, por lo que puedes usarlo así:

    src/components/MyComponent.tsx
    Copiar código

    Copiar el código al portapapeles

    import { Link } from "@/i18n";return <Link href="/about">t("about.title")</Link>;

    (Opcional) Paso 10: Acceder a la locale activa dentro de Server Actions

    Las Server Actions pueden leer la locale actual usando next-intl/server. Esto es útil para enviar correos electrónicos localizados o almacenar preferencias de idioma junto con los datos enviados.

    src/app/actions/get-current-locale.ts
    Copiar código

    Copiar el código al portapapeles

    "use server";import { getLocale } from "next-intl/server";export async function getCurrentLocale() {  return getLocale();}export async function handleContactForm(formData: FormData) {  const locale = await getCurrentLocale();  // Usa la locale para seleccionar plantillas, etiquetas de analíticas, etc.  console.log(`Formulario de contacto recibido desde la locale ${locale}`);}
    getLocale lee la locale establecida por el proxy de next-intl, por lo que funciona en cualquier parte del servidor: Route Handlers, Server Actions y funciones edge.

    (Opcional) Paso 11: Internacionaliza tus Metadatos

    Traducir contenido es importante, pero el objetivo principal de la internacionalización es hacer que tu sitio web sea más visible para el mundo. I18n es una palanca increíble para mejorar la visibilidad de tu sitio web mediante un SEO adecuado.

    Los metadatos correctamente internacionalizados ayudan a los motores de búsqueda a entender qué idiomas están disponibles en tus páginas. Esto incluye configurar las etiquetas meta hreflang, traducir títulos y descripciones, y asegurar que las URLs canónicas estén correctamente establecidas para cada locale.

    src/app/[locale]/about/layout.tsx
    Copiar código

    Copiar el código al portapapeles

    import type { Metadata } from "next";import { locales, defaultLocale, localizedPath } from "@/i18n";import { getTranslations } from "next-intl/server";// generateMetadata se ejecuta para cada locale, generando metadatos amigables para SEO// Esto ayuda a los motores de búsqueda a entender las versiones en idiomas alternativosexport async function generateMetadata({  params,}: {  params: { locale: string };}): Promise<Metadata> {  const { locale } = params;  const t = await getTranslations({ locale, namespace: "about" });  const url = "/about";  const languages = Object.fromEntries(    locales.map((locale) => [locale, localizedPath(locale, url)])  );  return {    title: t("title"),    description: t("description"),    alternates: {      canonical: localizedPath(locale, url),      languages: { ...languages, "x-default": url },    },  };}// ... Resto del código de la página

    (Opcional) Paso 12: Internacionaliza tu Sitemap

    Genera un sitemap que incluya todas las versiones locales de tus páginas. Esto ayuda a los motores de búsqueda a descubrir e indexar todas las versiones en diferentes idiomas de tu contenido.

    Un sitemap correctamente internacionalizado asegura que los motores de búsqueda puedan encontrar e indexar todas las versiones en diferentes idiomas de tus páginas. Esto mejora la visibilidad en los resultados de búsqueda internacionales.

    src/app/sitemap.ts
    Copiar código

    Copiar el código al portapapeles

    import type { MetadataRoute } from "next";import { defaultLocale, locales } from "@/i18n";const origin = "https://example.com";const formatterLocalizedPath = (locale: string, path: string) =>  locale === defaultLocale ? `${origin}${path}` : `${origin}/${locale}${path}`;/** * Obtiene un mapa de todos los locales y sus rutas localizadas * * Ejemplo de salida: * { *   "en": "https://example.com", *   "fr": "https://example.com/fr", *   "es": "https://example.com/es", *   "x-default": "https://example.com" * } */const getLocalizedMap = (path: string) =>  Object.fromEntries([    ...locales.map((locale) => [locale, formatterLocalizedPath(locale, path)]),    ["x-default", formatterLocalizedPath(defaultLocale, path)],  ]);// Generar sitemap con todas las variantes de locales para un mejor SEO// El campo alternates informa a los motores de búsqueda sobre las versiones de idiomaexport default function sitemap(): MetadataRoute.Sitemap {  return [    {      url: formatterLocalizedPath(defaultLocale, "/"),      lastModified: new Date(),      changeFrequency: "monthly",      priority: 1.0,      alternates: { languages: getLocalizedMap("/") },    },    {      url: formatterLocalizedPath(defaultLocale, "/about"),      lastModified: new Date(),      changeFrequency: "monthly",      priority: 0.7,      alternates: { languages: getLocalizedMap("/about") },    },  ];}

    (Opcional) Paso 13: Internacionaliza tu archivo robots.txt

    Crea un archivo robots.txt que maneje correctamente todas las versiones de locales de tus rutas protegidas. Esto asegura que los motores de búsqueda no indexen páginas de administración o panel en ningún idioma.

    Configurar correctamente robots.txt para todos los locales evita que los motores de búsqueda indexen páginas sensibles cuando tus rutas son diferentes para cada localización.

    src/app/robots.ts
    Copiar código

    Copiar el código al portapapeles

    import type { MetadataRoute } from "next";import { locales, defaultLocale } from "@/i18n";const origin = "https://example.com";// Genera rutas para todos los locales (por ejemplo, /admin, /fr/admin, /es/admin)const withAllLocales = (path: string) => [  path,  ...locales    .filter((locale) => locale !== defaultLocale)    .map((locale) => "/" + locale + path),];export default function robots(): MetadataRoute.Robots {  const disallow = [    ...withAllLocales("/dashboard"),    ...withAllLocales("/admin"),  ];  return {    rules: { userAgent: "*", allow: ["/"], disallow },    host: origin,    sitemap: origin + "/sitemap.xml",  };}

    (Opcional) Paso 14: Configurar Proxy para el Enrutamiento de Locales

    Crea un proxy para detectar automáticamente la locale preferida del usuario y redirigirlo a la URL con el prefijo de locale correspondiente. next-intl proporciona una función proxy conveniente que maneja esto automáticamente.

    El proxy asegura que los usuarios sean redirigidos automáticamente a su idioma preferido cuando visitan tu sitio. También guarda la preferencia del usuario para futuras visitas, mejorando la experiencia del usuario.

    src/proxy.ts
    Copiar código

    Copiar el código al portapapeles

    import { proxy } from "@/i18n";// Middleware que se ejecuta antes de las rutas, manejando la detección y el enrutamiento de la locale// localeDetection: true usa el encabezado Accept-Language para detectar automáticamente la localeexport default proxy;export const config = {  // Omitir API, internals de Next y assets estáticos  // Regex: coincide con todas las rutas excepto las que comienzan con api, _next, o que contienen un punto (archivos)  matcher: ["/((?!api|_next|.*\\..*).*)"],};

    (Opcional) Paso 15: Configurar los tipos de TypeScript para la locale

    Configurar TypeScript te ayudará a obtener autocompletado y seguridad de tipos para tus claves.

    Para ello, puedes crear un archivo global.ts en la raíz de tu proyecto y añadir el siguiente código:

    global.ts
    Copiar código

    Copiar el código al portapapeles

    import type { locales } from "@/i18n";type Messages = {  common: typeof import("./locales/en/common.json");  home: typeof import("./locales/en/home.json");  about: typeof import("./locales/en/about.json");  // ... Los futuros archivos JSON también deben añadirse aquí};declare module "next-intl" {  interface AppConfig {    Locale: (typeof locales)[number];    Messages: Messages;  }}

    Este código usará la Ampliación de Módulos (Module Augmentation) para añadir los locales y mensajes al tipo AppConfig de next-intl.

    (Opcional) Paso 15: Automatiza tus traducciones usando Intlayer

    Intlayer es una biblioteca gratuita y de código abierto diseñada para asistir en el proceso de localización en tu aplicación. Mientras que next-intl se encarga de la carga y gestión de las traducciones, Intlayer ayuda a automatizar el flujo de trabajo de traducción.

    Gestionar las traducciones manualmente puede ser una tarea que consume mucho tiempo y propensa a errores. Intlayer automatiza las pruebas, generación y gestión de traducciones, ahorrándote tiempo y asegurando consistencia en toda tu aplicación.

    Intlayer te permitirá:

    • Declarar tu contenido donde quieras en tu base de código Intlayer permite declarar tu contenido donde quieras en tu base de código usando archivos .content.{ts|js|json}. Esto permitirá una mejor organización de tu contenido, asegurando una mejor legibilidad y mantenibilidad de tu base de código.

    • Probar traducciones faltantes Intlayer proporciona funciones de prueba que pueden integrarse en tu pipeline de CI/CD o en tus pruebas unitarias. Aprende más sobre cómo probar tus traducciones.

    • Automatiza tus traducciones, Intlayer ofrece una CLI y una extensión para VSCode para automatizar tus traducciones. Puede integrarse en tu pipeline de CI/CD. Aprende más sobre automatizar tus traducciones. Puedes usar tu propia clave API y el proveedor de IA de tu elección. También proporciona traducciones conscientes del contexto, consulta rellenar contenido.

    • Conectar contenido externo Intlayer te permite conectar tu contenido a un sistema de gestión de contenido externo (CMS). Para obtenerlo de manera optimizada e insertarlo en tus recursos JSON. Aprende más sobre obtener contenido externo.

    • Editor visual Intlayer ofrece un editor visual gratuito para editar tu contenido usando un editor visual. Aprende más sobre edición visual de tus traducciones.

    Y más. Para descubrir todas las funcionalidades que ofrece Intlayer, por favor consulta la documentación sobre el interés de Intlayer.

    i18n usando next-i18next
    Automatizar next-i18next
    Alt+→

    En esta página

      Las conversaciones son anónimas y se revisan regularmente para abordar problemas comunes. No dudes en compartir ideas de funcionalidades, comentarios sobre la documentación o cualquier cosa relacionada con Intlayer, usamos esta información para definir nuestra hoja de ruta y mejorar el producto.

      .├── global.ts├── locales│   ├── en│   │  ├── common.json│   │  └── about.json│   ├── fr│   │  ├── common.json│   │  └── about.json│   └── es│      ├── common.json│      └── about.json└── src # Src es opcional    ├── proxy.ts    ├── app    │   ├── i18n.ts    │   └── [locale]    │       ├── layout.tsx    │       ├── (home) # / (Grupo de rutas para no saturar todas las páginas con recursos de home)    │       │   ├── layout.tsx    │       │   └── page.tsx    │       └── about # /about    │           ├── layout.tsx    │           └── page.tsx    └── components        ├── ClientComponent.tsx        └── ServerComponent.tsx
      npm install next-intl
      import { notFound } from "next/navigation";import createMiddleware from "next-intl/middleware";import { createNavigation } from "next-intl/navigation";// Define los locales soportados con seguridad de tiposexport const locales = ["en", "fr", "es"] as const;export type Locale = (typeof locales)[number];export const defaultLocale: Locale = "en";export function isRTL(locale: Locale | (string & {})) {  // Devuelve true si el locale es un idioma que se escribe de derecha a izquierda  return /^(ar|fa|he|iw|ur|ps|sd|ug|yi|ckb|ku)(-|$)/i.test(locale);}// Carga mensajes dinámicamente por locale para habilitar code-splitting// Promise.all carga los namespaces en paralelo para mejor rendimientoasync function loadMessages(locale: Locale) {  // Carga solo los namespaces que tu layout/páginas necesitan  const [common, home, about] = await Promise.all([    import(`../locales/${locale}/common.json`).then((m) => m.default),    import(`../locales/${locale}/home.json`).then((m) => m.default),    import(`../locales/${locale}/about.json`).then((m) => m.default),    // ... Los futuros archivos JSON deberían añadirse aquí  ]);  return { common, home, about } as const;}// Ayudante para generar URLs localizadas (por ejemplo, /about vs /fr/about)export function localizedPath(locale: string, path: string) {  return locale === defaultLocale ? path : `/${locale}${path}`;}// getRequestConfig se ejecuta en cada solicitud y proporciona mensajes a los componentes del servidor// Aquí es donde next-intl se conecta con el renderizado del lado servidor de Next.jsexport default async function getRequestConfig({  requestLocale,}: {  requestLocale: Promise<string | undefined>;}) {  const requested: Locale = ((await requestLocale) as Locale) ?? defaultLocale;  if (!locales.includes(requested)) notFound();  return {    locale: requested,    messages: await loadMessages(requested),  };}export function getCookie(locale: Locale) {  return [    `NEXT_LOCALE=${locale}`,    "Path=/",    `Max-Age=${60 * 60 * 24 * 365}`, // 1 año    "SameSite=Lax",  ].join("; ");}const routingOptions = {  locales,  defaultLocale,  localePrefix: "as-needed", // Cambiar ruta /en/... a /...  // Opcional: rutas localizadas  // pathnames: {  //   '/': '/',  //   '/about': {en: '/about', fr: '/a-propos', es: '/acerca-de'},  //   '/blog/[slug]': '/blog/[slug]'  // }  //  localeDetection: true, // evitar redirecciones de "/" a "/en" desde cookie} as const;export const { Link, redirect, usePathname, useRouter, getPathname } =  createNavigation(routingOptions);export const proxy = createMiddleware(routingOptions);
      import type { ReactNode } from "react";import { locales } from "@/i18n";import { getLocaleDirection, setRequestLocale } from "next-intl/server";// Pre-generar páginas estáticas para todas las locales en tiempo de compilación (SSG)// Esto mejora el rendimiento y el SEOexport function generateStaticParams() {  return locales.map((locale) => ({ locale }));}export default function LocaleLayout({  children,  params,}: {  children: ReactNode;  params: Promise<{ locale: string }>;}) {  // En Next.js App Router, params es una Promise (se puede await)  // Esto permite que los segmentos de ruta dinámicos se resuelvan de forma asíncrona  const { locale } = await params;  // Crítico: setRequestLocale indica a next-intl qué locale usar para esta solicitud  // Sin esto, getTranslations() no sabrá qué locale usar en los componentes del servidor  setRequestLocale(locale);  // Obtener la dirección del texto (LTR/RTL) para el renderizado correcto del HTML  const dir = getLocaleDirection(locale);  return (    <html lang={locale} dir={dir}>      <body>{children}</body>    </html>  );}
      import { getTranslations, getMessages, getFormatter } from "next-intl/server";import { NextIntlClientProvider } from "next-intl";import pick from "lodash/pick";import ServerComponent from "@/components/ServerComponent";import ClientComponent from "@/components/ClientComponent";export default async function AboutPage({  params,}: {  params: Promise<{ locale: string }>;}) {  const { locale } = await params;  // Los mensajes se cargan del lado del servidor. Solo se envía al cliente lo necesario.  // Esto minimiza el paquete de JavaScript enviado al navegador  const messages = await getMessages();  const clientMessages = pick(messages, ["common", "about"]);  // Traducciones/formateo estrictamente del lado del servidor  // Estos se ejecutan en el servidor y pueden pasarse como props a los componentes  const tAbout = await getTranslations("about");  const tCounter = await getTranslations("about.counter");  const format = await getFormatter();  const initialFormattedCount = format.number(0);  return (    // NextIntlClientProvider hace que las traducciones estén disponibles para los componentes cliente    // Solo pasa los namespaces que tus componentes cliente realmente usan    <NextIntlClientProvider locale={locale} messages={clientMessages}>      <main>        <h1>{tAbout("title")}</h1>        <ClientComponent />        <ServerComponent          formattedCount={initialFormattedCount}          label={tCounter("label")}          increment={tCounter("increment")}        />      </main>    </NextIntlClientProvider>  );}
      {  "welcome": "Welcome",  "greeting": "Hello, world!"}
      {  "welcome": "Bienvenue",  "greeting": "Bonjour le monde!"}
      {  "title": "About",  "description": "About page description",  "counter": {    "label": "Counter",    "increment": "Increment"  }}
      {  "title": "À propos",  "description": "Description de la page À propos",  "counter": {    "label": "Compteur",    "increment": "Incrémenter"  }}
      import { getTranslations, getMessages, getFormatter } from "next-intl/server";import { NextIntlClientProvider } from "next-intl";import pick from "lodash/pick";import ServerComponent from "@/components/ServerComponent";import ClientComponent from "@/components/ClientComponent";export default async function AboutPage({  params,}: {  params: Promise<{ locale: string }>;}) {  const { locale } = await params;  // Los mensajes se cargan del lado del servidor. Solo se envía al cliente lo que se necesita.  // Esto minimiza el paquete de JavaScript enviado al navegador  const messages = await getMessages();  const clientMessages = pick(messages, ["common", "about"]);  // Traducciones/formateo estrictamente del lado del servidor  // Estos se ejecutan en el servidor y pueden pasarse como props a los componentes  const tAbout = await getTranslations("about");  const tCounter = await getTranslations("about.counter");  const format = await getFormatter();  const initialFormattedCount = format.number(0);  return (    // NextIntlClientProvider hace que las traducciones estén disponibles para los componentes cliente    // Solo pasa los namespaces que tus componentes cliente realmente usan    <NextIntlClientProvider locale={locale} messages={clientMessages}>      <main>        <h1>{tAbout("title")}</h1>        <ClientComponent />        <ServerComponent          formattedCount={initialFormattedCount}          label={tCounter("label")}          increment={tCounter("increment")}        />      </main>    </NextIntlClientProvider>  );}
      "use client";import React, { useState } from "react";import { useTranslations, useFormatter } from "next-intl";const ClientComponent = () => {  // Alcance directamente al objeto anidado  // useTranslations/useFormatter son hooks que leen del contexto NextIntlClientProvider  // Solo funcionan si el componente está envuelto en NextIntlClientProvider  const t = useTranslations("about.counter");  const format = useFormatter();  const [count, setCount] = useState(0);  return (    <div>      <p>{format.number(count)}</p>      <button        aria-label={t("label")}        onClick={() => setCount((count) => count + 1)}      >        {t("increment")}      </button>    </div>  );};
      // Los componentes del servidor anidados dentro de componentes cliente deben ser síncronos// React no puede serializar funciones asíncronas a través del límite servidor/cliente// Solución: pre-calcular traducciones/formateos en el padre y pasarlos como propstype ServerComponentProps = {  formattedCount: string;  label: string;  increment: string;};const ServerComponent = ({  formattedCount,  label,  increment,}: ServerComponentProps) => {  return (    <div>      <p>{formattedCount}</p>      <button aria-label={label}>{increment}</button>    </div>  );};
      "use client";import Link from "next/link";import { usePathname } from "next/navigation";import { useLocale } from "next-intl";import { defaultLocale, getCookie, type Locale, locales } from "@/i18n";const getLocaleLabel = (locale: Locale): string => {  try {    const displayNames = new Intl.DisplayNames([locale], { type: "language" });    return displayNames.of(locale) ?? locale.toUpperCase();  } catch {    return locale.toUpperCase();  }};const localeFlags: Record<Locale, string> = {  en: "🇬🇧",  fr: "🇫🇷",  es: "🇪🇸",};export default function LocaleSwitcher() {  const activeLocale = useLocale();  const pathname = usePathname();  // Eliminar el prefijo de la localidad del pathname para obtener la ruta base  const getBasePath = (path: string) => {    for (const locale of locales) {      if (path.startsWith(`/${locale}`)) {        return path.slice(locale.length + 1) || "/";      }    }    return path;  };  const basePath = getBasePath(pathname);  return (    <nav aria-label="Selector de idioma">      <div>        {(locales as readonly Locale[]).map((locale) => {          const isActive = locale === activeLocale;          // Construir el href según si es el locale por defecto          const href =            locale === defaultLocale ? basePath : `/${locale}${basePath}`;          return (            <Link              key={locale}              href={href}              aria-current={isActive ? "page" : undefined}              onClick={() => {                document.cookie = getCookie(locale);              }}            >              <span>{localeFlags[locale]}</span>              <span>{getLocaleLabel(locale)}</span>              <span>{locale.toUpperCase()}</span>            </Link>          );        })}      </div>    </nav>  );}
      import { Link } from "@/i18n";return <Link href="/about">t("about.title")</Link>;
      "use server";import { getLocale } from "next-intl/server";export async function getCurrentLocale() {  return getLocale();}export async function handleContactForm(formData: FormData) {  const locale = await getCurrentLocale();  // Usa la locale para seleccionar plantillas, etiquetas de analíticas, etc.  console.log(`Formulario de contacto recibido desde la locale ${locale}`);}
      import type { Metadata } from "next";import { locales, defaultLocale, localizedPath } from "@/i18n";import { getTranslations } from "next-intl/server";// generateMetadata se ejecuta para cada locale, generando metadatos amigables para SEO// Esto ayuda a los motores de búsqueda a entender las versiones en idiomas alternativosexport async function generateMetadata({  params,}: {  params: { locale: string };}): Promise<Metadata> {  const { locale } = params;  const t = await getTranslations({ locale, namespace: "about" });  const url = "/about";  const languages = Object.fromEntries(    locales.map((locale) => [locale, localizedPath(locale, url)])  );  return {    title: t("title"),    description: t("description"),    alternates: {      canonical: localizedPath(locale, url),      languages: { ...languages, "x-default": url },    },  };}// ... Resto del código de la página
      import type { MetadataRoute } from "next";import { defaultLocale, locales } from "@/i18n";const origin = "https://example.com";const formatterLocalizedPath = (locale: string, path: string) =>  locale === defaultLocale ? `${origin}${path}` : `${origin}/${locale}${path}`;/** * Obtiene un mapa de todos los locales y sus rutas localizadas * * Ejemplo de salida: * { *   "en": "https://example.com", *   "fr": "https://example.com/fr", *   "es": "https://example.com/es", *   "x-default": "https://example.com" * } */const getLocalizedMap = (path: string) =>  Object.fromEntries([    ...locales.map((locale) => [locale, formatterLocalizedPath(locale, path)]),    ["x-default", formatterLocalizedPath(defaultLocale, path)],  ]);// Generar sitemap con todas las variantes de locales para un mejor SEO// El campo alternates informa a los motores de búsqueda sobre las versiones de idiomaexport default function sitemap(): MetadataRoute.Sitemap {  return [    {      url: formatterLocalizedPath(defaultLocale, "/"),      lastModified: new Date(),      changeFrequency: "monthly",      priority: 1.0,      alternates: { languages: getLocalizedMap("/") },    },    {      url: formatterLocalizedPath(defaultLocale, "/about"),      lastModified: new Date(),      changeFrequency: "monthly",      priority: 0.7,      alternates: { languages: getLocalizedMap("/about") },    },  ];}
      import type { MetadataRoute } from "next";import { locales, defaultLocale } from "@/i18n";const origin = "https://example.com";// Genera rutas para todos los locales (por ejemplo, /admin, /fr/admin, /es/admin)const withAllLocales = (path: string) => [  path,  ...locales    .filter((locale) => locale !== defaultLocale)    .map((locale) => "/" + locale + path),];export default function robots(): MetadataRoute.Robots {  const disallow = [    ...withAllLocales("/dashboard"),    ...withAllLocales("/admin"),  ];  return {    rules: { userAgent: "*", allow: ["/"], disallow },    host: origin,    sitemap: origin + "/sitemap.xml",  };}
      import { proxy } from "@/i18n";// Middleware que se ejecuta antes de las rutas, manejando la detección y el enrutamiento de la locale// localeDetection: true usa el encabezado Accept-Language para detectar automáticamente la localeexport default proxy;export const config = {  // Omitir API, internals de Next y assets estáticos  // Regex: coincide con todas las rutas excepto las que comienzan con api, _next, o que contienen un punto (archivos)  matcher: ["/((?!api|_next|.*\\..*).*)"],};
      import type { locales } from "@/i18n";type Messages = {  common: typeof import("./locales/en/common.json");  home: typeof import("./locales/en/home.json");  about: typeof import("./locales/en/about.json");  // ... Los futuros archivos JSON también deben añadirse aquí};declare module "next-intl" {  interface AppConfig {    Locale: (typeof locales)[number];    Messages: Messages;  }}