Auteur:
    Création:2026-09-13Dernière mise à jour:2026-09-13

    i18next VS Intlayer | Benchmark d'internationalisation (i18n) React & Next.js

    i18next est le framework d'i18n le plus utilisé de l'écosystème JavaScript. À travers react-i18next et next-i18next, il alimente une grande partie des applications React et Next.js. Intlayer est une alternative basée sur un compilateur, découpée par composant.

    Cet article les compare sur des mesures concrètes plutôt que sur des listes de fonctionnalités. Les chiffres proviennent de Benchmark Bloom, une suite open source qui compile la même application avec chaque bibliothèque et enregistre ce que le navigateur télécharge réellement.

    tl;dr: i18next est le runtime le plus lourd du benchmark: +77 KB gzip par page sur Next.js dans la configuration naïve, +22 KB après l'optimisation complète par namespaces + lazy loading. Intlayer n'ajoute que +0.3 KB. Chaque configuration d'i18next en dehors de celle entièrement compartimentée (scoped) envoie ~90% des chaînes des pages distantes; Intlayer n'envoie 0% par défaut. Changer de langue avec un backend chargé à la demande prend 123 à 185 ms avec react-i18next contre 3 à 4 ms avec Intlayer. L'adaptateur @intlayer/next-i18next conserve l'API d'i18next et atteint 150.7 KB par page contre 218.5 KB pour l'original.

    En résumé

    • i18next / react-i18next / next-i18next - Mature, riche en plugins, agnostique du framework. Namespaces, détecteurs de langue, backends, ICU via plugin, <Trans> pour le contenu enrichi. Le contenu est centralisé dans locales/{lng}/{ns}.json. Puissant, mais chaque optimisation (découpage des namespaces, chargement par page, sécurité des types) représente une configuration que vous devez maintenir.
    • Intlayer - Modèle centré sur les composants. Les dictionnaires .content.ts se trouvent à côté du composant qu'ils desservent, un compilateur au build effectue le tree-shaking et les charge à la demande par composant et par locale, des types TypeScript stricts sont générés à partir de votre contenu, et les traductions manquantes provoquent des erreurs au build. Fournit middleware, helpers SEO, un Éditeur Visuel / CMS et la traduction assistée par IA.
    BibliothèqueÉtoiles GitHubCommits totauxDernier commitPremière versionVersion NPMTéléchargements NPM
    aymericzip/intlayerGitHub Repo starsGitHub commit activityLast CommitAvril 2024npmnpm downloads
    i18next/i18nextGitHub Repo starsGitHub commit activityLast CommitJanvier 2012npmnpm downloads
    i18next/react-i18nextGitHub Repo starsGitHub commit activityLast CommitDécembre 2015npmnpm downloads
    i18next/next-i18nextGitHub Repo starsGitHub commit activityLast CommitNovembre 2018npmnpm downloads
    Les badges se mettent à jour automatiquement. Les données évoluent au fil du temps.

    Comparaison des fonctionnalités

    FonctionnalitéIntlayer (react-intlayer / next-intlayer)i18next (react-i18next / next-i18next)
    Traductions proches des composants✅ Oui, .content.ts colocalisé avec chaque composant❌ Non, centralisé dans locales/{lng}/{ns}.json
    Intégration TypeScript✅ Types stricts générés automatiquement à partir du contenu⚠️ Basique; clés strictes via extension CustomTypeOptions et typage ressources
    Détection des traductions manquantes✅ Erreur TypeScript + erreur/avertissement au build⚠️ Fallback au runtime (saveMissing, écho de clé)
    Contenu riche (JSX / Markdown / composants)✅ Support direct⚠️ <Trans> avec placeholders indexés
    Support ICU⚠️ En cours⚠️ Via plugin (i18next-icu)
    Pluralisation✅ Motifs basés sur les énumérations✅ Suffixes _one / _other (Intl.PluralRules)
    Formatage (dates, nombres, devises)useNumber, useDate, ... (Intl sous le capot)⚠️ Formateurs d'interpolation ou Intl.* manuel
    Routage localisé et middleware✅ Proxy/middleware intégré, getMultilingualUrls⚠️ Non natif; middleware personnalisé ou tiers
    Helpers SEO (hreflang, sitemap, robots)✅ Helpers intégrés❌ Manuel
    Composants serveur synchronesuseIntlayer de next-intlayer/server utilisable dans tout composant serveur⚠️ getFixedT sur la page, puis t transmis en props
    Tree-shaking (n'embarque que le contenu utile)✅ Par composant, par locale, automatisé par le compilateur⚠️ Manuel: namespaces + liste ns par page + backend
    Lazy loadingimportMode: 'dynamic' (une ligne de config)✅ Via plugins backend (i18next-resources-to-backend, i18next-http-backend)
    Purger le contenu inutilisé✅ Dictionnaires orphelins supprimés au build❌ Non intégré
    Test des traductions manquantes (CLI / CI)npx intlayer content test⚠️ i18next-parser / outil tiers
    Traduction assistée par IA✅ Intégrée, utilise vos propres clés d'API❌ Non (Locize est un service tiers payant)
    Éditeur Visuel / CMS✅ Éditeur Visuel gratuit + CMS optionnel❌ Non (Locize / plateformes externes)
    Serveur MCP et compétences d'agent (Skills)✅ Oui❌ Non
    Écosystème / communauté⚠️ Plus récent mais en forte croissance✅ Le plus étendu et le plus mature

    Le benchmark

    Ce qui a été mesuré

    La suite Benchmark Bloom construit la même application avec chaque bibliothèque: 10 pages (accueil, à propos, blog, carrières, contact, FAQ, tarifs, produits, paramètres, équipe), 10 locales (en, fr, es, de, it, pt, zh, ja, ko, ru), des composants et un contenu identiques. Les pages sont mesurées en en et en fr. Chaque bibliothèque est testée sous un maximum de quatre stratégies de chargement, de la configuration naïve à l'optimale:

    StratégieDescriptionQui fait cela
    staticToutes les locales et pages regroupées (resources inlinées dans init())Prototypes rapides, code généré par IA
    dynamicSeule la locale active est chargée via un backend, mais tous les namespaces à la foisLa majorité des projets
    scoped-staticUn namespace par route, tous chargés en amontRare
    scoped-dynamicUn namespace par route + chargement dynamique via backend. Seulement la page et locale activeApplications avec un budget de perf strict

    Intlayer n'a pas de variante "scoped": le compilateur isole le contenu par composant automatiquement, ses lignes static et dynamic sont donc déjà découpées.

    Pour chaque build, la suite enregistre:

    • Lib size: taille gzip d'un composant vide qui n'importe que la bibliothèque i18n. Le coût fixe du runtime.
    • Page JS: JavaScript gzip téléchargé par page, en moyenne sur l'ensemble des pages et locales.
    • Locale leak %: part des chaînes traduites dans le JS téléchargé appartenant à une locale que l'utilisateur ne consulte pas (évalué sur en et fr, donc 50% signifie "l'autre locale mesurée est entièrement présente"; avec 10 locales compilées, le gaspillage réel est bien supérieur).
    • Page leak %: part des chaînes traduites dans le JS téléchargé appartenant à une page sur laquelle l'utilisateur n'est pas.
    • Component avg: taille gzip moyenne de chaque composant compilé isolément. Indique le poids du runtime i18n injecté dans chaque composant.
    • E2E reactivity: temps réel entre la sélection d'une nouvelle locale et la mise à jour de html[lang] dans le DOM (Playwright, 5 itérations).
    • Hydration: durée de la phase d'hydratation de React.
    Les données ci-dessous proviennent de l'exécution du 2026-09-12 avec next-i18next 16.3.0, react-i18next 17.0.13 et intlayer 9.5.1. L'application de test est volontairement modeste (quelques dizaines de chaînes par locale), les pourcentages de fuite décrivent donc un comportement type: ils augmentent à mesure que votre contenu grandit tandis que le coût du runtime reste fixe.

    Résultats sur Next.js (next-i18next)

    BibliothèqueStratégieLib size (gz)Page JS avg (gz)Locale leakPage leakComponent avg (gz)Réactivité E2EHydratation
    base (sans 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

    Comment interpréter ces données

    • Coût du runtime. Le cœur d'i18next plus react-i18next représente le runtime le plus lourd: 19.7 KB gzip pour un composant vide, contre 5.5 KB pour next-intlayer.
    • La configuration naïve coûte cher. Inliner resources dans init() génère 218.5 KB par page, soit +77.5 KB par rapport à l'application de base. Chaque page embarque chaque namespace.
    • Optimiser demande beaucoup d'efforts. Passer à un backend (dynamic) fait gagner 49 KB mais laisse fuiter 90% des chaînes des pages distantes et, dans cette configuration, la moitié des chaînes appartiennent à la mauvaise langue. Ajouter un découpage par namespace sur chaque route (scoped-dynamic) permet enfin d'atteindre 0% de fuite à 163.4 KB, ce qui reste +22.4 KB par page de plus qu'Intlayer à 141.3 KB, qui n'a nécessité aucune configuration spécifique.
    • Taille des composants. Un composant utilisant useTranslation() pèse entre 26 et 79 KB selon la configuration; le même composant avec useIntlayer() ne pèse que 6.9 KB.
    • L'hydratation monte à 27.7 ms dans la configuration dynamic: l'instance i18next s'initialise et résout son backend côté client avant que React ne puisse hydrater la page.

    Résultats sur TanStack Start (react-i18next)

    La même application de test sur TanStack Start avec react-i18next pur, ce qui isole les dépendances propres à Next.js.

    BibliothèqueStratégieLib size (gz)Page JS avg (gz)Locale leakPage leakComponent avg (gz)Réactivité E2EHydratation
    base (pas 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

    Comment interpréter ces données

    • L'application react-i18next naïve envoie +69 KB par page de plus que l'application de base, et l'hydratation prend 85 ms (4 fois la base) parce que l'arbre complet des ressources est parsé et enregistré côté client avant le premier rendu.
    • Le changement de langue met en lumière la latence du lazy loading. Lorsque les ressources sont chargées à la demande via un backend, changer de langue nécessite un aller-retour réseau avant que html[lang] ne se mette à jour: 123 ms en dynamic, 185 ms en scoped-static. Intlayer met à jour le DOM en 3 à 4 ms dans les deux cas: le changement de langue est immédiat et ne dépend pas d'une requête réseau.
    • La configuration pleinement optimisée scoped-dynamic atteint 0% de fuite à 127.2 KB, restant +8.6 KB au-dessus de la ligne dynamic d'Intlayer, et a nécessité une correspondance routes-namespaces, un backend de ressources et une frontière Suspense par route pour y parvenir.
    • La ligne static d'Intlayer présente déjà 0% de fuite de page car seuls les dictionnaires importés par les composants de la page sont inclus dans le bundle. Activer importMode: 'dynamic' supprime également la fuite de locale.
    • Taille des composants: 24 à 27 KB par composant avec react-i18next contre 6 à 8 KB avec Intlayer. useTranslation() lie chaque composant à l'instance globale d'i18next.

    Pourquoi un tel écart? Instance globale vs dictionnaires compilés

    i18next a été pensé en 2012 comme un runtime: une instance globale conserve un dépôt de ressources, des plugins l'enrichissent, et t() résout les clés au rendu. C'est ce qui le rend si flexible (n'importe quel framework, backend ou format) et également ce qui le rend lourd:

    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     # doit savoir qu'elle requiert ["common", "about"]
    

    L'instance ne peut pas anticiper les clés qu'un composant demandera, elle conserve donc tous les namespaces que vous lui demandez de charger. Optimiser signifie que vous devez découper les catalogues en namespaces, vous devez lister les namespaces nécessaires à chaque page, et vous devez maintenir cette liste à jour quand les composants se déplacent. Comme le soulignent les notes du benchmark: "maintenir la sécurité des types et savoir exactement quel namespace inclure sur quelle page est un cauchemar".

    Intlayer élimine l'instance. Le contenu est déclaré à côté du composant, et le compilateur résout le graphe de dépendances au 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 identifie quel composant importe quel dictionnaire, n'embarque que ceux-ci, uniquement pour la locale active, et exclut ceux qui ne sont pas référencés. Le modèle "scoped-dynamic" devient le résultat direct du build plutôt qu'une discipline manuelle imposée à l'équipe.

    Pour obtenir les chiffres de la ligne dynamic, définissez dictionary.importMode: 'dynamic' dans intlayer.config.ts. Consultez la documentation sur l'optimisation de bundle.

    Expérience développeur

    Configuration

    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;
    };
    

    Il faut y ajouter un I18nProvider côté client qui recrée l'instance avec les mêmes options, un generateStaticParams, et une liste de namespaces sur chaque page.

    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;
    

    Composant 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 page affichant ce composant doit charger le namespace about, et t("counter.label") reste une simple chaîne de caractères non typée à moins d'étendre 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 et increment sont strictement typés; une faute de frappe déclenche une erreur TypeScript, et une valeur française manquante génère une erreur au build.

    Composant serveur synchrone

    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 page appelle i18n.getFixedT(locale, "about") et fait descendre t et locale en tant que 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>
      );
    };
    

    Gardez l'API d'i18next, profitez des performances d'Intlayer

    Il n'est pas nécessaire de réécrire vos composants pour bénéficier des résultats du benchmark ci-dessus. @intlayer/i18next, @intlayer/react-i18next et @intlayer/next-i18next sont des adaptateurs prêts à l'emploi: useTranslation, t(), <Trans>, {{interpolation}}, pluriels _one / _other, suffixes de contexte et returnObjects continuent de fonctionner, alimentés par les dictionnaires Intlayer compilés.

    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()],
    });
    

    Dans le benchmark, la version adaptée de la même application Next.js est passée de 218.5 KB à 150.7 KB par page, de 78.5 KB à 9.7 KB par composant, de ~90% de fuite de page à 0%, et d'une hydratation de 15.6 ms à 11.3 ms, sans modifier le code de l'application. Vos fichiers existants locales/{lng}/{ns}.json peuvent rester la source de vérité grâce au plugin de synchronisation JSON.

    Consultez les guides de migration: i18next, react-i18next, next-i18next.

    Quand choisir quelle solution?

    • Choisissez i18next si vous avez besoin de son écosystème de plugins (détecteurs, backends, ICU, Locize), si vous traduisez aussi en dehors de React (services Node, vanilla JS, autres frameworks), si votre équipe le maîtrise déjà, ou si une plateforme de traduction externe exige locales/{lng}/{ns}.json. Prévoyez le temps de découper vos catalogues en namespaces, de configurer un backend et de maintenir la table routes-namespaces si la performance compte.
    • Choisissez Intlayer si vous voulez du contenu découpé par composant, un typage TypeScript strict, des erreurs au build en cas de clé manquante, du tree-shaking et lazy loading sans effort, un changement de langue instantané, des composants serveur synchrones et des outils éditoriaux intégrés (Éditeur Visuel, CMS, traduction IA, serveur MCP). Particulièrement pertinent pour les codebases modulaires et les design systems.
    • Choisissez les adaptateurs @intlayer/*-i18next si vous utilisez déjà i18next et souhaitez obtenir les gains de bundle et de réactivité sans refactoriser vos composants.

    Comparaisons associées

    Étoiles GitHub

    Les étoiles GitHub reflètent la popularité d'un projet, la confiance de la communauté et sa pérennité. Bien qu'elles ne mesurent pas directement la qualité technique, elles montrent combien de développeurs trouvent le projet utile, suivent ses avancées et sont susceptibles de l'adopter.

    Graphique d'historique des étoiles

    Conclusion

    i18next a gagné sa place: il s'exécute partout, propose un plugin pour chaque besoin et bénéficie de plus d'une décennie de maintenance. Le benchmark met toutefois en lumière le coût d'une architecture centrée sur le runtime. La configuration déployée par la majorité des équipes ajoute +70 à 77 KB gzip par page, laisse fuiter ~90% du contenu des autres pages, et un changement de langue avec chargement dynamique prend plus de 100 ms. Atteindre 0% de fuite reste envisageable, mais impose un backend, un namespace par route et une table de correspondance maintenue manuellement, tout en restant +9 à 22 KB plus lourd qu'Intlayer.

    Intlayer délègue ce travail au compilateur. Dictionnaires par composant, lazy loading par locale et purge du contenu inutile deviennent des artéfacts de build plutôt que des conventions complexes. Sur la même application: +0.3 KB par page, 0% de fuite, des composants 3 à 10 fois plus légers et un basculement de langue en 3 à 4 ms.

    Toutes les données brutes, applications de test et scripts sont disponibles dans le dépôt Benchmark Bloom. Testez-le par vous-même.

    Consultez la documentation 'Pourquoi Intlayer?' pour en savoir plus.

    Commentaires

    Aucun commentaire pour le moment. Soyez le premier à partager vos pensées.

    Articles similaires

    Derniers articles