Autor:
    Creación:2026-09-13Última actualización:2026-09-27

    i18next VS @intlayer/i18next: Misma API, Distinto Bundle

    i18next VS Intlayer

    @intlayer/i18next, @intlayer/react-i18next y @intlayer/next-i18next son adaptadores de compatibilidad. Exponen la API de i18next que tu código ya utiliza (useTranslation, t(), <Trans>, i18n.changeLanguage(), getFixedT, serverSideTranslations...) y la sirven a partir de diccionarios compilados por Intlayer. Los componentes no cambian. El runtime debajo de ellos sí.

    Este artículo mide ese reemplazo en la misma aplicación Next.js, construida una vez con next-i18next y otra con @intlayer/next-i18next. Las cifras proceden de Benchmark Bloom. Para comparar i18next e Intlayer como librerías independientes, consulta i18next vs Intlayer. Este análisis se enfoca en lo que transforma el adaptador cuando mantienes tu código tal cual.

    tl;dr: En la misma aplicación Next.js, sustituir next-i18next por @intlayer/next-i18next redujo el JavaScript por página de 218.5 KB a 150.7 KB gzip (configuración inicial) y superó a la configuración de next-i18next totalmente optimizada (163.4 KB) por 12.7 KB. El componente promedio pasó de 78.5 KB a 9.7 KB, la fuga de cadenas hacia otras páginas bajó de ~90% a 0%, la hidratación se redujo de 15.6 ms a 11.3 ms, y el runtime de 19.7 KB a 9.4 KB. No se editó ningún componente; solo se modificó un archivo de provider. Los plugins de i18next (backends, detectores de idioma) se aceptan pero no hacen nada: no queda nada que cargar o detectar en tiempo de ejecución.

    Qué es @intlayer/i18next

    i18next es un runtime. i18n.init({ resources }) o un plugin backend carga locales/{lng}/{ns}.json en una instancia global; useTranslation("about") suscribe el componente a ella; t("title") busca la clave en el momento del renderizado. Los namespaces, la carga diferida (lazy loading), las listas de namespaces por página y la seguridad de tipos son responsabilidad tuya a la hora de configurar y mantener.

    Los adaptadores conservan la API y reemplazan la instancia:

    1. Alias de importación. createNextI18nPlugin() de @intlayer/next-i18next/plugin (o withI18next) envuelve withIntlayer y agrega alias de Webpack / Turbopack para que next-i18next, react-i18next e i18next resuelvan hacia sus equivalentes en @intlayer/*. En Vite, reactI18nextVitePlugin() de @intlayer/react-i18next/plugin hace lo mismo. No es necesario renombrar ninguna importación.
    2. JSON como fuente de verdad. El plugin syncJSON lee tus archivos existentes locales/{lng}/{ns}.json con format: "i18next" (de modo que {{name}}, anidamiento $t(), _one / _other y sufijos de contexto se procesen adecuadamente) y reescribe las traducciones cuando el CLI o el CMS las actualizan.
    3. Vinculación en el punto de llamada. El paso de optimización de Intlayer reescribe useTranslation("about") en una llamada que recibe directamente el diccionario about, en el idioma activo. El componente deja de consultar el store global.
    components/About.tsx
    // Tu código, sin cambios
    import { useTranslation } from "react-i18next";
    
    const About = () => {
      const { t } = useTranslation("about");
      return <h1>{t("title")}</h1>;
    };
    
    Lo que emite el compilador (simplificado)
    import _dicHash_about from "../.intlayer/dictionaries/about.mjs";
    import { useDictionary as useTranslation } from "@intlayer/react-i18next";
    
    const About = () => {
      const { t } = useTranslation(_dicHash_about);
      return <h1>{t("title")}</h1>;
    };
    

    Esa reescritura es la responsable de la drástica reducción en el tamaño de los componentes y en la fuga de contenido por página que se detalla a continuación.

    Qué conservan, ignoran y no reemplazan los adaptadores

    API de i18nextCon @intlayer/*
    useTranslation("ns"), useTranslation("ns", { keyPrefix })✅ Se conserva. Vinculado al diccionario ns en tiempo de compilación; claves tipadas contra tu contenido
    t("key", { name }), {{interpolation}}, anidamiento $t(key)✅ Se conserva
    Plurales key_one / key_other, contexto key_male, returnObjects✅ Se conserva. Plurales evaluados con Intl.PluralRules
    <Trans> con components, etiquetas numeradas <1>...</1>, values✅ Se conserva
    withTranslation, Translation, I18nContext✅ Se conserva
    i18n.changeLanguage(), i18n.language, i18n.dir(), on("languageChanged")✅ Se conserva. changeLanguage controla el idioma de Intlayer
    getFixedT(lng, ns, keyPrefix), i18n.exists(), hasLoadedNamespace()✅ Se conserva
    i18n.use(Backend).use(LanguageDetector).init({...})⚠️ use() llama al init del plugin y finaliza; backends y detectores no tienen nada que cargar o detectar
    init({ resources }), addResourceBundle()⚠️ resources se ignora con una advertencia en desarrollo; elimina los imports JSON para obtener los ahorros de bundle
    I18nextProvider i18n={i18n}⚠️ Renderiza un IntlayerProvider; la prop i18n se ignora. En App Router, pasa el idioma (ver abajo)
    serverSideTranslations(locale, ["common"]) (next-i18next)⚠️ Devuelve la estructura esperada y no carga nada. Seguro de mantener, seguro de eliminar
    appWithTranslation(App) (next-i18next)✅ Se conserva
    next-i18next.config.js⚠️ No se lee. Los idiomas provienen de intlayer.config.ts
    useTranslation() sin namespace✅ Funciona contra el diccionario global translation del archivo completo (splitKeys: false)

    El benchmark

    Qué se midió

    La suite Benchmark Bloom construye la misma aplicación con cada configuración: 10 páginas (inicio, nosotros, blog, empleo, contacto, preguntas frecuentes, precios, productos, ajustes, equipo), 10 idiomas (en, fr, es, de, it, pt, zh, ja, ko, ru), componentes idénticos y contenido idéntico. Las páginas se miden en en y fr.

    next-i18next se evaluó bajo cuatro estrategias de carga, desde el JSON de cada idioma importado en resources (static) hasta un namespace por ruta, cargado bajo demanda mediante un backend (scoped-dynamic). El adaptador se probó sobre los mismos componentes que la configuración básica, modificando únicamente next.config.ts, intlayer.config.ts y el archivo de provider. No cuenta con variante manual "scoped": el compilador asigna el alcance del contenido por componente.

    Para cada build, la suite registra:

    • Tamaño de la lib: tamaño gzip de un componente vacío que solo importa la librería de i18n.
    • JS por página: promedio de JavaScript gzip descargado por página en todas las rutas e idiomas.
    • % de fuga de idioma: porcentaje de cadenas traducidas en el JS descargado que pertenecen a un idioma que el usuario no está viendo.
    • % de fuga de página: porcentaje de cadenas traducidas en el JS descargado que pertenecen a una página en la que el usuario no está.
    • Promedio de componente: tamaño gzip promedio de cada componente compilado en aislamiento.
    • Reactividad E2E: tiempo real medido entre la selección de un nuevo idioma y la actualización de html[lang] en el DOM (Playwright, 5 iteraciones).
    • Hidratación: duración de la fase de hidratación de React.
    Los valores siguientes proceden de la ejecución del 12-09-2026 con next-i18next 16.3.0 (react-i18next 17.0.13, i18next 26.4.2) y @intlayer/next-i18next 9.5.1. La aplicación de prueba es intencionadamente compacta (unas decenas de cadenas por idioma), por lo que los porcentajes de fuga describen un patrón: aumentan conforme crece tu contenido mientras el costo del runtime permanece fijo.

    Resultados en Next.js

    Selecciona las métricas y las bibliotecas que te interesen:

    Métrica

    Carga JSON dinámica

    Carga traducciones en tiempo de ejecución

    JSON con alcance (namespacing)

    Espacios de nombres de traduction por página

    ¿Qué es esta métrica?

    El tamaño total comprimido en gzip del paquete de la biblioteca de internacionalización. Solo incluye el proveedor y la lógica de recuperación de contenido después del tree-shaking y la minificación.

    ¿Por qué es importante?

    Un tamaño de biblioteca más pequeño reduce la carga útil inicial de JavaScript, lo que acelera el tiempo de descarga y ejecución en el cliente.

    Ver como

    ConfiguraciónEstrategiaTamaño lib (gz)JS pág prom (gz)Fuga idiomaFuga págComp prom (gz)Reactividad E2EHidratación
    base (sin i18n)-0.0 KB141.0 KB0.0%0.0%0.9 KB13.4 ms11.8 ms
    next-i18nextstatic19.7 KB218.5 KB0.0%89.8%78.5 KB16.4 ms15.6 ms
    next-i18nextdynamic19.7 KB169.5 KB50.0%89.8%26.1 KB15.4 ms27.7 ms
    next-i18nextscoped-static19.7 KB220.1 KB0.0%89.8%78.9 KB16.4 ms14.7 ms
    next-i18nextscoped-dynamic19.7 KB163.4 KB0.0%0.0%27.1 KB15.9 ms15.1 ms
    @intlayer/next-i18nextstatic9.4 KB150.7 KB0.0%0.0%9.7 KB10.7 ms11.3 ms
    @intlayer/next-i18nextdynamic9.4 KB150.7 KB0.0%0.0%9.7 KB11.9 ms10.6 ms
    next-intlayer (nativo)static5.5 KB141.3 KB0.0%0.0%8.5 KB15.5 ms16.9 ms
    next-intlayer (nativo)dynamic5.5 KB141.3 KB0.0%0.0%6.9 KB15.3 ms15.9 ms

    Cómo interpretarlo

    • 68 KB menos por página frente a la configuración inicial. resources: { en, fr, ... } envía cada idioma y cada namespace en cada página: 218.5 KB. La compilación con adaptador para los mismos componentes queda en 150.7 KB. Además supera a la mejor configuración de next-i18next (163.4 KB, un namespace por ruta, cargado bajo demanda) por 12.7 KB, porque el runtime de i18next por sí solo pesa 19.7 KB contra 9.4 KB.
    • La fuga cae al 0% sin modificar ningún componente. Cada configuración de next-i18next, excepto la totalmente aislada, envía ~90% de cadenas de otras páginas. La fila dynamic resulta más perjudicial de lo que parece: no elimina la fuga de página e introduce un 50% de fuga de idioma, dado que el backend por idioma continúa trayendo todo el namespace translation. El adaptador alcanza 0% / 0% directamente desde el código original.
    • Componentes: 8 veces más pequeños. Un componente con useTranslation() compilado en aislamiento promedia 78.5 KB con resources incrustado y 26-27 KB con backend, debido a que t queda atado al store global. Con el adaptador promedia 9.7 KB.
    • Hidratación y cambio de idioma más rápidos. La hidratación pasa de 15.6 ms a 11.3 ms (y de 27.7 ms en la configuración dynamic, donde la petición del backend bloquea la ruta crítica). El cambio de idioma pasa de 15-16 ms a 11-12 ms.
    • El adaptador no es el runtime nativo. next-intlayer registra 141.3 KB, apenas +0.3 KB sobre la app base. El adaptador carga con la superficie de la API de i18next (sintaxis de interpolación, sufijos de plural y contexto, análisis de etiquetas <Trans>) sobre el núcleo de Intlayer: 9.4 KB y +9.4 KB por página respecto al nativo. Es un puente de transición, no el destino final.
    Tabla completa, cada biblioteca y cada estrategia, en el informe de benchmark de Next.js.

    Resultados en TanStack Start (react-i18next)

    Para Vite y TanStack Start, el benchmark compara react-i18next estándar con intlayer:

    BibliotecaEstrategiaTamaño lib (gz)JS pág prom (gz)Fuga idiomaFuga págComp prom (gz)Reactividad E2EHidratación
    base (sin i18n)-0.0 KB111.0 KB0.0%0.0%0.7 KB8.1 ms21.6 ms
    react-i18nextdynamic18.4 KB136.4 KB23.1%89.8%24.8 KB123.1 ms32.9 ms
    intlayerdynamic5.0 KB118.6 KB0.0%0.0%6.3 KB3.6 ms14.1 ms
    Tabla completa en el informe de benchmark de TanStack Start.
    El adaptador react-i18next en Vite / TanStack Start no formó parte de esta prueba. La referencia base de react-i18next en TanStack Start se encuentra en i18next vs Intlayer: 127-184 KB por página y 123-185 ms en el cambio de idioma cuando el backend se carga de forma diferida.

    Por qué varían las cifras

    The Intlayer compiler extracts content from components

    Nada en components/ ha cambiado, por lo que las ganancias se deben al destino al que se vincula useTranslation.

    Con i18next, la vinculación se realiza con la instancia global. Todo lo cargado en ella (todos los idiomas en static, el namespace entero del idioma activo en dynamic) resulta accesible desde cualquier componente que invoque useTranslation(). El empaquetador no puede dividir por debajo de lo que la instancia retiene, y el runtime no puede prever qué claves solicitará cada componente.

    bash
    .
    ├── next-i18next.config.js
    ├── public/locales
    │   ├── en/translation.json           # cadenas de cada página
    │   └── fr/translation.json
    ├── i18n/i18n.ts                      # i18n.use(initReactI18next).init({ resources })
    └── components
        ├── AppProviders.tsx              # <I18nextProvider i18n={i18n}>
        └── About.tsx                     # useTranslation(); t("about.title")
    

    Todo lo que contiene la instancia se envía a cada página, y el desperdicio crece en dos ejes, páginas e idiomas:

    Theoretical content leakage by architecture

    Con @intlayer/next-i18next, la vinculación se establece directamente con el diccionario. syncJSON transforma cada archivo de namespace en un diccionario; el paso de optimización proporciona al componente el diccionario requerido como una importación que el empaquetador puede rastrear y dividir por página y por idioma.

    bash
    .
    ├── intlayer.config.ts                # syncJSON({ format: "i18next", source: ... })
    ├── public/locales
    │   ├── en/translation.json           # sin cambios, sigue siendo la fuente de verdad
    │   └── fr/translation.json
    ├── .intlayer/                        # generado: un diccionario por namespace, por idioma
    └── components
        ├── AppProviders.tsx              # <IntlayerClientProvider locale={locale}>
        └── About.tsx                     # useTranslation(); t("about.title")  ← sin cambios
    

    i18n/i18n.ts y su importación de resources se convierten en código muerto. De ahí provienen los 68 KB de ahorro.

    Migración en tres pasos

    1. Instalación

      bash
      npx intlayer init --interactive
      

      El comando detecta i18next / react-i18next / next-i18next, instala intlayer, el paquete correspondiente al framework (next-intlayer o react-intlayer), el adaptador @intlayer/* adecuado y @intlayer/sync-json-plugin, además de preconfigurar intlayer.config.ts. Mantén instalados los paquetes originales: actúan como dependencias par y suministran los tipos.

    2. Apunta Intlayer a tus archivos de idioma

      intlayer.config.ts
      import { Locales, type IntlayerConfig } from "intlayer";
      import { syncJSON } from "@intlayer/sync-json-plugin";
      
      const config: IntlayerConfig = {
        internationalization: {
          locales: [Locales.ENGLISH, Locales.FRENCH, Locales.SPANISH],
          defaultLocale: Locales.ENGLISH,
        },
        dictionary: {
          importMode: "dynamic",
          format: "i18next",
        },
        plugins: [
          syncJSON({
            // dialecto i18next: {{name}}, $t(key), key_one / key_other, key_male
            format: "i18next",
            // Un archivo por namespace: `useTranslation("about")` → about.json
            source: ({ locale, key }) => `./public/locales/${locale}/${key}.json`,
            location: "public/locales",
          }),
        ],
      };
      
      export default config;
      

      Si dispones de un único archivo translation.json por idioma (el namespace predeterminado de i18next), define splitKeys: false para que el archivo completo permanezca como un solo diccionario y las llamadas simples a useTranslation() sigan resolviéndose.

    3. Agrega el plugin

      next.config.ts
      import type { NextConfig } from "next";
      import { withI18next } from "@intlayer/next-i18next/plugin";
      
      const nextConfig: NextConfig = {};
      
      export default withI18next(nextConfig);
      

      En App Router, los componentes cliente obtienen su idioma mediante el segmento [locale]. Dado que el I18nextProvider del adaptador no recibe idioma, reemplázalo una única vez en tu archivo de provider:

      components/AppProviders.tsx
      "use client";
      
      import { IntlayerClientProvider } from "next-intlayer";
      import type { LocalesValues } from "intlayer";
      
      export const AppProviders = ({
        locale,
        children,
      }: {
        locale: LocalesValues;
        children: React.ReactNode;
      }) => (
        <IntlayerClientProvider locale={locale}>{children}</IntlayerClientProvider>
      );
      

      Todos los componentes inferiores seguirán invocando useTranslation().

      vite.config.ts
      import { defineConfig } from "vite";
      import react from "@vitejs/plugin-react";
      import { reactI18nextVitePlugin } from "@intlayer/react-i18next/plugin";
      
      export default defineConfig({
        plugins: [react(), reactI18nextVitePlugin()],
      });
      

      reactI18nextVitePlugin() envuelve vite-intlayer y crea los alias de react-i18next e i18next. Para un proyecto sin React, i18nextVitePlugin() de @intlayer/i18next/plugin crea el alias de i18next en solitario.

    Qué puedes eliminar a continuación

    Archivo / patrónMotivo
    resources: { en, fr, ... } y las importaciones JSONIgnorados por el adaptador. Aquí residían los 68 KB
    i18next-http-backend, i18next-resources-to-backendNada que consultar en tiempo de ejecución
    i18next-browser-languagedetectorLa detección de idioma la gestiona el enrutamiento de Intlayer (prefijo URL, cookie, cabecera)
    serverSideTranslations() en getStaticPropsDevuelve una estructura vacía; inocuo, pero redundante
    next-i18next.config.jsNo se lee. Los idiomas residen en intlayer.config.ts
    Listas ns: [...] por páginaEl compilador determina los namespaces por componente

    Qué ganas más allá de los bytes

    • Claves tipadas. useTranslation("about") se tipa contra el diccionario compilado about; t("does.not.exist") genera un error de TypeScript en lugar de devolver la clave como texto.
    • npx intlayer test bloquea la CI ante cualquier clave ausente en cualquier idioma. npx intlayer fill traduce las claves faltantes con tu propia clave de proveedor (OpenAI, Anthropic, Mistral, Gemini...) y las escribe de nuevo en locales/{lng}/{ns}.json.
    • Editor Visual y CMS operan sobre el mismo JSON, permitiendo a los traductores editar mediante interfaz gráfica mientras los archivos se actualizan.
    • Transición progresiva a .content.ts. Cualquier componente puede migrar de useTranslation("about") a useIntlayer("about") con un archivo de contenido dedicado. Los archivos JSON y .content.ts conviven sin conflicto.

    Límites que debes conocer antes de empezar

    i18n.use(HttpBackend) llama al init del plugin y nada más. Si tu aplicación dependía de obtener traducciones de un CMS en tiempo de ejecución, ese flujo desaparece; usa el CMS de Intlayer o los comandos intlayer pull / push en su lugar. La detección de idioma pasa a ser la configuración de enrutamiento de Intlayer (prefijo de URL, cookie, cabecera).

    A diferencia de otros adaptadores, @intlayer/i18next no utiliza resources en línea como fallback. Cada clave debe existir en los diccionarios sincronizados, lo que verifica intlayer test.

    Un solo archivo, mostrado arriba. Pages Router con appWithTranslation no requiere nada.

    localePath, fallbackLng, reloadOnPrerender y similares no tienen equivalente; los idiomas y el fallback provienen de intlayer.config.ts.

    9.4 KB de runtime y +9.4 KB por página respecto a next-intlayer. Una vez que todos los componentes hayan pasado a useIntlayer, elimínalo.

    Comparación de funcionalidades

    Más allá de los bytes, lo que ofrece cada opción:

    Funcionalidadi18next / react-i18next / next-i18nextAdaptadores @intlayer/*Intlayer nativo
    Tus llamadas a t(), useTranslation, <Trans>✅✅ Sin cambios❌ Migradas a useIntlayer
    Tamaño del runtime (gzip, Next.js)19.7 KB9.4 KB5.5 KB
    Fuga de otras páginas sin namespaces manuales~90%0%0%
    Claves tipadas⚠️ Declaración manual✅ Desde los diccionarios compilados✅ Autogeneradas
    Backends y plugins en runtime✅ Ecosistema completo de plugins❌ Inertes❌ No aplica, usa el CMS
    Contenido junto a los componentes❌ JSON centralizado⚠️ JSON, .content.ts puede coexistir✅ .content.ts junto a cada componente
    Traducciones faltantes en CI⚠️ No integrado✅ npx intlayer test✅ npx intlayer test
    Traducción con IA❌ No✅ npx intlayer fill✅ npx intlayer fill
    Editor visual / CMS❌ Mediante plataformas externas✅ Sobre el mismo JSON✅ Sí
    Ecosistema / comunidad✅ Muy grande⚠️ Más pequeño, en rápido crecimiento⚠️ Más pequeño, en rápido crecimiento
    Los tamaños de runtime provienen de la ejecución en Next.js descrita arriba.

    ¿Cuándo elegir cada opción?

    Tu aplicación depende de backends en tiempo de ejecución (traducciones servidas por un CMS al momento de la petición), del ecosistema de plugins o de un entorno no-React que los adaptadores no cubren.

    Estás en react-i18next / next-i18next y quieres los 68 KB, componentes 8 veces más pequeños, 0% de fuga, claves tipadas y comprobaciones de CI sin reescribir código. Este es el punto de entrada para una base de código i18next existente.

    Para nuevos proyectos, o una vez que el adaptador haya cumplido su función. Tiene el runtime más ligero (5.5 KB, +0.3 KB por página) y permite Server Components síncronos y archivos .content.ts por componente. Comienza con Intlayer con Next.js o con Vite y React.

    Preguntas frecuentes

    De resources: { en, fr, ... }. La configuración básica de next-i18next importa el JSON de cada idioma en init(), por lo que cada página carga cada namespace en cada idioma: 218.5 KB por página. El adaptador nunca empaqueta ese bloque; entrega a cada componente solo el diccionario que nombra, en el idioma activo.

    Sí, con components, etiquetas numeradas <1>...</1> y values. También funcionan {{interpolation}}, anidación $t(key), plurales key_one / key_other (evaluados con Intl.PluralRules), sufijos de contexto y returnObjects.

    Configura splitKeys: false en el plugin syncJSON. Todo el archivo se mantiene como un único diccionario y un useTranslation() básico continuará resolviendo sobre él.

    No, es el puente. El adaptador conserva la API de i18next y cuesta 9.4 KB de runtime; next-intlayer nativo cuesta 5.5 KB y añade Server Components síncronos y archivos .content.ts colocados junto al código. Puedes migrar componente por componente, ya que los diccionarios JSON y .content.ts coexisten.

    Sí. locales/{lng}/{ns}.json sigue siendo la fuente de verdad: syncJSON lo lee con el dialecto de i18next y escribe las traducciones de vuelta cuando la CLI o el CMS los actualiza.

    Comparativas relacionadas

    Misma serie de adaptadores:

    Las bibliotecas comparadas directamente:

    Documentación de referencia:

    Compat adapters:

    Migration guides:

    Para entender de dónde vienen estas bibliotecas, lee la historia del i18n en JavaScript.

    Conclusión

    i18next es el runtime más pesado de este benchmark, y los adaptadores eliminan la mayor parte de su carga sin pedirte que abandones su API. En la misma aplicación Next.js, esto se traduce en 68 KB menos por página que la configuración inicial, 12.7 KB menos que la alternativa más optimizada a mano, componentes 8 veces más pequeños, 0% de fuga y 4 ms de hidratación, a cambio de un archivo de configuración, una línea de plugin y un cambio menor en el provider. Los backends y detectores pasan a ser inocuos, resources se descarta en lugar de combinarse, y el runtime nativo next-intlayer se mantiene aún 9 KB más ligero.

    Todos los datos brutos, las aplicaciones de prueba y los scripts están disponibles en el repositorio de Benchmark Bloom. Puedes comprobarlo tú mismo.

    Consulta el documento ¿Por qué Intlayer? para más información.

    Comentarios

    Aún no hay comentarios. Sé el primero en compartir tus pensamientos.

    Artículos relacionados

    Últimos artículos