Autore:
    Creazione:2026-09-13Ultimo aggiornamento:2026-09-13

    i18next VS Intlayer | Benchmark di internazionalizzazione (i18n) per React e Next.js

    i18next è il framework di i18n più diffuso nell'ecosistema JavaScript. Attraverso react-i18next e next-i18next, alimenta un'ampia quota di applicazioni React e Next.js. Intlayer è un'alternativa basata su compilatore e con ambito per componente.

    Questo articolo li mette a confronto attraverso misurazioni concrete anziché elenchi di funzionalità. I dati provengono da Benchmark Bloom, una suite open source che compila la stessa applicazione con ciascuna libreria e registra ciò che il browser scarica effettivamente.

    tl;dr: i18next è il runtime più pesante del benchmark: +77 KB gzip per pagina su Next.js nella configurazione standard (naive), +22 KB dopo l'ottimizzazione completa con namespace e lazy loading. Intlayer aggiunge appena +0.3 KB. Ogni configurazione di i18next, tranne quella completamente isolata (scoped), include ~90% di stringhe di pagine non visitate; Intlayer ne include lo 0% per impostazione predefinita. Il cambio di lingua con backend caricato in modalità lazy ha richiesto 123-185 ms con react-i18next contro 3-4 ms con Intlayer. L'adattatore @intlayer/next-i18next conserva l'API di i18next e si attesta a 150.7 KB per pagina rispetto ai 218.5 KB dell'originale.

    In sintesi

    • i18next / react-i18next / next-i18next - Maturo, ricco di plugin e indipendente dal framework. Namespace, rilevatori di lingua, backend, ICU tramite plugin, <Trans> per contenuti avanzati. I contenuti sono centralizzati in locales/{lng}/{ns}.json. Potente, ma ogni ottimizzazione (divisione in namespace, caricamento per pagina, sicurezza dei tipi) richiede configurazioni manuali a carico dello sviluppatore.
    • Intlayer - Modello incentrato sui componenti. I dizionari .content.ts risiedono accanto al componente a cui sono destinati, un compilatore in fase di build applica tree-shaking e lazy loading per componente e per lingua, tipi TypeScript rigorosi vengono generati automaticamente dai contenuti e le traduzioni mancanti bloccano la compilazione. Include middleware, helper SEO, un editor visuale / CMS e traduzione assistita da IA.
    LibreriaStelle GitHubCommit totaliUltimo commitPrima versioneVersione NPMDownload NPM
    aymericzip/intlayerGitHub Repo starsGitHub commit activityLast CommitAprile 2024npmnpm downloads
    i18next/i18nextGitHub Repo starsGitHub commit activityLast CommitGennaio 2012npmnpm downloads
    i18next/react-i18nextGitHub Repo starsGitHub commit activityLast CommitDicembre 2015npmnpm downloads
    i18next/next-i18nextGitHub Repo starsGitHub commit activityLast CommitNovembre 2018npmnpm downloads
    I badge si aggiornano automaticamente. Le metriche variano nel tempo.

    Confronto delle caratteristiche

    CaratteristicaIntlayer (react-intlayer / next-intlayer)i18next (react-i18next / next-i18next)
    Traduzioni vicine ai componenti✅ Sì, .content.ts posizionato accanto a ogni componente❌ No, cartella centralizzata locales/{lng}/{ns}.json
    Integrazione con TypeScript✅ Tipi rigorosi generati in automatico dai contenuti⚠️ Base; richiede estensione manuale di CustomTypeOptions e tipizzazione
    Rilevamento traduzioni mancanti✅ Errore TypeScript + errore/avviso in fase di compilazione⚠️ Fallback a runtime (saveMissing, restituzione della chiave)
    Contenuti avanzati (JSX / Markdown)✅ Supporto nativo⚠️ <Trans> con segnaposto indicizzati
    Supporto ICU⚠️ In lavorazione⚠️ Tramite plugin (i18next-icu)
    Pluralizzazione✅ Modelli basati su enumerazioni✅ Suffissi _one / _other (Intl.PluralRules)
    Formattazione (date, numeri, valute)useNumber, useDate, ... (Intl integrato)⚠️ Formattatori di interpolazione o chiamate manuali a Intl.*
    Routing localizzato e middleware✅ Proxy/middleware integrato, getMultilingualUrls⚠️ Non nativo; richiede middleware personalizzato o di terze parti
    Helper SEO (hreflang, sitemap, robots)✅ Helper integrati❌ Manuale
    Componenti server sincroniuseIntlayer da next-intlayer/server utilizzabile in ogni server component⚠️ getFixedT a livello di pagina, poi t passato come prop
    Tree-shaking (carica solo contenuti usati)✅ Per componente, per lingua, automatizzato dal compilatore⚠️ Manuale: namespace + lista ns per pagina + backend
    Lazy loadingimportMode: 'dynamic' (una riga di configurazione)✅ Tramite plugin backend (i18next-resources-to-backend, i18next-http-backend)
    Eliminazione contenuti inutilizzati✅ I dizionari orfani vengono scartati in fase di compilazione❌ Non integrato
    Test traduzioni mancanti (CLI / CI)npx intlayer content test⚠️ i18next-parser / strumenti di terze parti
    Traduzione assistita da IA✅ Integrata, utilizza le tue chiavi API❌ No (Locize è un servizio a pagamento separato)
    Editor Visuale / CMS✅ Editor Visuale gratuito + CMS opzionale❌ No (Locize / piattaforme esterne)
    Server MCP e Agent Skills✅ Sì❌ No
    Ecosistema e community⚠️ Più recente ma in rapida espansione✅ Il più esteso e consolidato

    Il benchmark

    Cosa è stato misurato

    La suite Benchmark Bloom costruisce la medesima applicazione con ciascuna libreria: 10 pagine (home, about, blog, careers, contact, FAQ, pricing, products, settings, team), 10 lingue (en, fr, es, de, it, pt, zh, ja, ko, ru), componenti e contenuti identici. Le pagine sono misurate in en e fr. Ogni libreria viene testata con un massimo di quattro strategie di caricamento:

    StrategiaDescrizioneCasi d'uso tipici
    staticTutte le lingue e pagine raggruppate (resources incorporate in init())Prototipi veloci, codice generato da IA
    dynamicSolo la lingua attiva viene caricata via backend, ma tutti i namespace contemporaneamenteLa maggior parte dei progetti
    scoped-staticUn namespace per percorso, tutti inclusi in anticipoRaro
    scoped-dynamicUn namespace per percorso + lazy loading tramite backend. Solo pagina e lingua correntiApplicazioni con vincoli di performance stringenti

    Intlayer non necessita di una variante "scoped": il compilatore suddivide i contenuti per componente in modo del tutto automatico, per cui le righe static e dynamic sono già ottimizzate.

    Per ciascuna build, la suite registra:

    • Lib size: dimensione gzip di un componente vuoto che importa esclusivamente la libreria i18n.
    • Page JS: JavaScript gzip scaricato per pagina, mediato su tutte le pagine e lingue.
    • Locale leak %: percentuale di stringhe tradotte nel JS scaricato appartenenti a una lingua che l'utente non sta visualizzando.
    • Page leak %: percentuale di stringhe tradotte nel JS scaricato appartenenti a pagine in cui l'utente non si trova.
    • Component avg: dimensione media gzip di ciascun componente compilato isolatamente.
    • E2E reactivity: intervallo reale tra la selezione di una nuova lingua e l'aggiornamento di html[lang] nel DOM (Playwright, 5 iterazioni).
    • Hydration: durata della fase di idratazione di React.
    I dati seguenti si riferiscono al test del 2026-09-12 con next-i18next 16.3.0, react-i18next 17.0.13 e intlayer 9.5.1. L'applicazione di test è volutamente contenuta (poche decine di stringhe per lingua), pertanto le percentuali di dispersione mostrano un modello: crescono insieme ai contenuti mentre il costo del runtime rimane costante.

    Risultati su Next.js (next-i18next)

    LibreriaStrategiaLib size (gz)Page JS avg (gz)Locale leakPage leakComponent avg (gz)Reattività E2EHydration
    base (senza 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
    next-intlayerstatic5.5 KB141.3 KB0.0%0.0%8.5 KB15.5 ms16.9 ms
    next-intlayerdynamic5.5 KB141.3 KB0.0%0.0%6.9 KB15.3 ms15.9 ms
    @intlayer/next-i18next (compat)static9.4 KB150.7 KB0.0%0.0%9.7 KB10.7 ms11.3 ms
    @intlayer/next-i18next (compat)dynamic9.4 KB150.7 KB0.0%0.0%9.7 KB11.9 ms10.6 ms

    Come interpretare i dati

    • Costo del runtime. Il core di i18next abbinato a react-i18next è il runtime più consistente: 19.7 KB gzip per un componente vuoto, rispetto ai 5.5 KB di next-intlayer.
    • La configurazione base è onerosa. Incorporare resources in init() produce 218.5 KB per pagina, +77.5 KB rispetto all'applicazione priva di i18n. Ogni pagina include tutti i namespace.
    • Ottimizzare richiede lavoro. Passare a un backend (dynamic) risparmia 49 KB ma disperde ancora il 90% delle stringhe di altre pagine e, in questo scenario, la metà delle stringhe appartiene alla lingua errata. Aggiungere la divisione per namespace su ogni route (scoped-dynamic) raggiunge lo 0% di dispersione a 163.4 KB, rimanendo comunque +22.4 KB per pagina sopra Intlayer (141.3 KB), che non ha richiesto alcuna configurazione speciale.
    • Dimensioni dei componenti. Un componente con useTranslation() compila tra i 26 e i 79 KB; lo stesso componente con useIntlayer() si attesta a 6.9 KB.
    • L'idratazione sale a 27.7 ms nella configurazione dynamic: l'istanza i18next si inizializza e risolve il proprio backend sul client prima che React possa idratare la pagina.

    Risultati su TanStack Start (react-i18next)

    Stessa applicazione di prova su TanStack Start con react-i18next puro, isolando la comparazione dalle specificità di Next.js.

    LibreriaStrategiaLib size (gz)Page JS avg (gz)Locale leakPage leakComponent avg (gz)Reattività E2EHydration
    base (no i18n)-0.0 KB111.0 KB0.0%0.0%0.7 KB8.1 ms21.6 ms
    react-i18nextstatic18.4 KB180.3 KB50.0%89.8%24.3 KB12.9 ms85.1 ms
    react-i18nextdynamic18.4 KB136.4 KB23.1%89.8%24.8 KB123.1 ms32.9 ms
    react-i18nextscoped-static18.4 KB184.2 KB50.7%89.8%25.3 KB185.1 ms25.2 ms
    react-i18nextscoped-dynamic18.4 KB127.2 KB0.0%0.0%26.7 KB17.6 ms11.3 ms
    intlayerstatic5.0 KB125.8 KB50.0%0.0%8.1 KB3.2 ms11.5 ms
    intlayerdynamic5.0 KB118.6 KB0.0%0.0%6.3 KB3.6 ms14.1 ms

    Come interpretare i dati

    • L'app react-i18next di base trasmette +69 KB per pagina rispetto all'applicazione senza i18n, e l'idratazione impiega 85 ms (4 volte il valore di base) perché l'albero completo delle risorse viene elaborato e registrato sul client prima del rendering iniziale.
    • Il cambio di lingua rende evidente la latenza del lazy loading. Quando le risorse vengono caricate al bisogno, il cambio lingua comporta una chiamata di rete prima dell'aggiornamento di html[lang]: 123 ms in dynamic, 185 ms in scoped-static. Intlayer aggiorna il DOM in 3-4 ms in entrambe le modalità: la modifica è istantanea e non dipende da richieste di rete.
    • L'assetto interamente ottimizzato scoped-dynamic tocca lo 0% di dispersione a 127.2 KB, rimanendo comunque +8.6 KB sopra la riga dynamic di Intlayer, e ha richiesto una mappatura route-namespace, un backend di risorse e confini Suspense dedicati.
    • La riga static di Intlayer ha già 0% di dispersione di pagina poiché vengono inclusi solo i dizionari importati dai componenti effettivi della pagina. Abilitando importMode: 'dynamic' si azzera anche la dispersione linguistica.
    • Dimensioni componenti: 24-27 KB con react-i18next contro 6-8 KB con Intlayer. useTranslation() vincola ogni componente all'istanza globale di i18next.

    Origine del divario: Istanza globale vs dizionari compilati

    i18next è nato nel 2012 come runtime: un'istanza globale mantiene un archivio di risorse, i plugin la espandono e t() ricerca le chiavi a runtime. Tale architettura garantisce grande versatilità (qualsiasi framework, backend e formato), ma introduce inevitabilmente overhead:

    bash
    .
    ├── i18n.ts                      # createInstance().use(...).use(...).init({...})
    └── src
        ├── locales
       ├── en
       ├── common.json
       ├── home.json
       └── about.json
       └── fr
           ├── common.json
           ├── home.json
           └── about.json
        ├── components
       └── Counter.tsx          # useTranslation("about") + t("counter.label")
        └── app
            └── [locale]
                └── about
                    └── page.tsx     # deve sapere che necessita di ["common", "about"]
    

    L'istanza non può anticipare le chiavi richieste da un componente. Ottimizzare significa che tu devi suddividere i file in namespace, tu devi elencare i namespace richiesti da ogni pagina e tu devi mantenere allineata la lista quando i componenti vengono spostati. Come riportato nelle note del benchmark: "mantenere la sicurezza dei tipi e sapere esattamente quale namespace includere su quale pagina è un incubo".

    Intlayer elimina l'istanza globale. I contenuti sono dichiarati accanto al componente e il compilatore risolve il grafico delle dipendenze durante il build:

    bash
    .
    ├── intlayer.config.ts
    └── src
        ├── components
       └── Counter
           ├── index.tsx        # useIntlayer("counter")
           └── index.content.ts
        └── app
            └── [locale]
                └── about
                    ├── page.tsx
                    └── page.content.ts
    

    @intlayer/swc / @intlayer/babel analizza quale componente importa quale dizionario, impacchetta esclusivamente quelli utili per la lingua corrente e scarta il resto. Il pattern "scoped-dynamic" diventa il risultato naturale del build anziché un onere manuale.

    Per ottenere i parametri della riga dynamic, imposta dictionary.importMode: 'dynamic' in intlayer.config.ts. Consulta la guida all'ottimizzazione del bundle.

    Esperienza di sviluppo

    Configurazione

    next-i18next (App Router)

    src/app/i18n/server.ts
    import { createInstance } from "i18next";
    import { initReactI18next } from "react-i18next/initReactI18next";
    import resourcesToBackend from "i18next-resources-to-backend";
    import { defaultLocale } from "@/i18n.config";
    
    const backend = resourcesToBackend(
      (locale: string, namespace: string) =>
        import(`../../locales/${locale}/${namespace}.json`)
    );
    
    export const initI18next = async (
      locale: string,
      namespaces: string[] = ["common"]
    ) => {
      const i18n = createInstance();
      await i18n
        .use(initReactI18next)
        .use(backend)
        .init({
          lng: locale,
          fallbackLng: defaultLocale,
          ns: namespaces,
          defaultNS: "common",
          interpolation: { escapeValue: false },
          react: { useSuspense: false },
        });
      return i18n;
    };
    

    A questo si aggiunge un I18nProvider client che reinizializza l'istanza con le medesime opzioni, generateStaticParams e un elenco namespaces per ciascuna pagina.

    Intlayer

    intlayer.config.ts
    import { type IntlayerConfig, Locales } from "intlayer";
    
    const config: IntlayerConfig = {
      internationalization: {
        locales: [Locales.ENGLISH, Locales.FRENCH],
        defaultLocale: Locales.ENGLISH,
      },
    };
    
    export default config;
    
    src/app/[locale]/layout.tsx
    import { getHTMLTextDir } from "intlayer";
    import { IntlayerClientProvider, type NextLayoutIntlayer } from "next-intlayer";
    
    const LocaleLayout: NextLayoutIntlayer = async ({ children, params }) => {
      const { locale } = await params;
    
      return (
        <html lang={locale} dir={getHTMLTextDir(locale)}>
          <body>
            <IntlayerClientProvider locale={locale}>
              {children}
            </IntlayerClientProvider>
          </body>
        </html>
      );
    };
    
    export default LocaleLayout;
    

    Componente client

    react-i18next

    src/locales/en/about.json
    {
      "counter": {
        "label": "Counter",
        "increment": "Increment"
      }
    }
    
    src/components/Counter.tsx
    "use client";
    
    import { useState } from "react";
    import { useTranslation } from "react-i18next";
    
    export const Counter = () => {
      const { t, i18n } = useTranslation("about");
      const [count, setCount] = useState(0);
      const numberFormat = new Intl.NumberFormat(i18n.language);
    
      return (
        <div>
          <p>{numberFormat.format(count)}</p>
          <button
            aria-label={t("counter.label")}
            onClick={() => setCount((c) => c + 1)}
          >
            {t("counter.increment")}
          </button>
        </div>
      );
    };
    
    La pagina che renderizza questo componente deve caricare il namespace about, e t("counter.label") rimane una stringa semplice finché non si estende CustomTypeOptions.

    Intlayer

    src/components/Counter/index.content.ts
    import { t, type Dictionary } from "intlayer";
    
    const counterContent = {
      key: "counter",
      content: {
        label: t({ en: "Counter", fr: "Compteur" }),
        increment: t({ en: "Increment", fr: "Incrémenter" }),
      },
    } satisfies Dictionary;
    
    export default counterContent;
    
    src/components/Counter/index.tsx
    "use client";
    
    import { useState } from "react";
    import { useIntlayer } from "next-intlayer";
    import { useNumber } from "next-intlayer/format";
    
    export const Counter = () => {
      const { label, increment } = useIntlayer("counter");
      const number = useNumber();
      const [count, setCount] = useState(0);
    
      return (
        <div>
          <p>{number(count)}</p>
          <button aria-label={label} onClick={() => setCount((c) => c + 1)}>
            {increment}
          </button>
        </div>
      );
    };
    

    label e increment sono tipizzati rigorosamente: ogni errore di battitura genera un errore TypeScript e una traduzione mancante blocca la compilazione.

    Componente server sincrono

    next-i18next

    src/components/ServerCounter.tsx
    type ServerCounterProps = {
      t: (key: string) => string;
      locale: string;
      count: number;
    };
    
    export const ServerCounter = ({ t, locale, count }: ServerCounterProps) => (
      <div>
        <p>{new Intl.NumberFormat(locale).format(count)}</p>
        <button aria-label={t("counter.label")}>{t("counter.increment")}</button>
      </div>
    );
    

    La pagina invoca i18n.getFixedT(locale, "about") e trasmette t e locale verso il basso tramite props.

    Intlayer

    src/components/ServerCounter.tsx
    import { useIntlayer } from "next-intlayer/server";
    import { useNumber } from "next-intlayer/server/format";
    
    export const ServerCounter = ({ count }: { count: number }) => {
      const { label, increment } = useIntlayer("counter");
      const number = useNumber();
    
      return (
        <div>
          <p>{number(count)}</p>
          <button aria-label={label}>{increment}</button>
        </div>
      );
    };
    

    Conserva l'API di i18next, sfrutta le prestazioni di Intlayer

    Non serve riscrivere i componenti per beneficiare dei numeri del benchmark. @intlayer/i18next, @intlayer/react-i18next e @intlayer/next-i18next sono adattatori compatibili pronti all'uso: useTranslation, t(), <Trans>, {{interpolation}}, plurali _one / _other, suffissi di contesto e returnObjects continuano a funzionare, erogati tramite i dizionari compilati da Intlayer.

    next.config.ts
    import type { NextConfig } from "next";
    import { createNextI18nPlugin } from "@intlayer/next-i18next/plugin";
    
    const withIntlayer = createNextI18nPlugin();
    
    const nextConfig: NextConfig = {};
    
    export default withIntlayer(nextConfig);
    
    vite.config.ts
    import { defineConfig } from "vite";
    import { reactI18nextVitePlugin } from "@intlayer/react-i18next/plugin";
    
    export default defineConfig({
      plugins: [reactI18nextVitePlugin()],
    });
    

    Nel benchmark, la versione con adattatore della medesima app Next.js è passata da 218.5 KB a 150.7 KB per pagina, da 78.5 KB a 9.7 KB per componente, da ~90% di dispersione a 0%, e l'idratazione da 15.6 ms a 11.3 ms, senza alterare il codice dell'applicazione. I file esistenti locales/{lng}/{ns}.json possono rimanere la sorgente primaria dei dati tramite il plugin di sincronizzazione JSON.

    Consulta le guide di migrazione: i18next, react-i18next, next-i18next.

    Quando scegliere quale soluzione?

    • Scegli i18next se necessiti del suo ecosistema di plugin (rilevatori, backend, ICU, Locize), se gestisci la localizzazione anche fuori da React (servizi Node, vanilla JS, altri framework), se il tuo team ha già consolidata esperienza o se una piattaforma di traduzione richiede locales/{lng}/{ns}.json. Considera il tempo necessario per organizzare i namespace, configurare un backend e mantenere aggiornata la mappa delle route se le performance sono critiche.
    • Scegli Intlayer se desideri contenuti integrati a livello di componente, TypeScript rigoroso, errori in fase di build per chiavi mancanti, tree-shaking e lazy loading a costo zero, cambio lingua istantaneo, componenti server sincroni e strumenti editoriali integrati (editor visuale, CMS, traduzione con IA, server MCP). Ideale per codebase modulari e design system.
    • Scegli gli adattatori @intlayer/*-i18next se utilizzi già i18next e vuoi ottenere vantaggi immediati su bundle e reattività senza riscrivere i componenti.

    Confronti correlati

    Stelle su GitHub

    Le stelle su GitHub rispecchiano la popolarità, la fiducia della community e la rilevanza a lungo termine di un progetto. Pur non misurando direttamente la qualità tecnica, mostrano quanti sviluppatori apprezzano il progetto, ne seguono gli aggiornamenti e intendono adottarlo.

    Grafico dello storico delle stelle

    Conclusione

    i18next ha meritato il proprio ruolo di riferimento: funziona ovunque, dispone di plugin per ogni esigenza ed è supportato da oltre un decennio. Il benchmark dimostra tuttavia il costo di un'architettura focalizzata sul runtime. Il setup comunemente adottato aggiunge +70-77 KB gzip per pagina, disperde ~90% dei contenuti di altre pagine e richiede più di 100 ms per un cambio lingua con lazy loading. Raggiungere lo 0% di dispersione è possibile, ma impone un backend, un namespace per route e un mapping manuale continuo, rimanendo comunque +9-22 KB sopra Intlayer.

    Intlayer delega questo lavoro al compilatore. Dizionari per componente, lazy loading per lingua ed eliminazione dei contenuti orfani sono artefatti del build anziché convenzioni da gestire manualmente. Sulla stessa applicazione: +0.3 KB per pagina, 0% di dispersione, componenti da 3 a 10 volte più leggeri e cambio lingua in 3-4 ms.

    Tutti i dati grezzi, le applicazioni di prova e gli script sono consultabili nel repository Benchmark Bloom. Puoi verificarli direttamente.

    Per ulteriori dettagli consulta il documento 'Perché Intlayer?'.

    Commenti

    Ancora nessun commento. Sii il primo a condividere i tuoi pensieri.

    Articoli correlati

    Ultimi articoli