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

    i18next VS @intlayer/i18next | Même API, Bundle Différent

    @intlayer/i18next, @intlayer/react-i18next et @intlayer/next-i18next sont des adaptateurs de compatibilité. Ils exposent l'API i18next que votre code utilise déjà (useTranslation, t(), <Trans>, i18n.changeLanguage(), getFixedT, serverSideTranslations...) et la distribuent à partir de dictionnaires compilés par Intlayer. Les composants ne changent pas. Le runtime sous-jacent, lui, change.

    Cet article mesure ce remplacement sur la même application Next.js, construite une fois avec next-i18next et une fois avec @intlayer/next-i18next. Les chiffres proviennent de Benchmark Bloom. Pour comparer i18next et Intlayer en tant que bibliothèques distinctes, consultez i18next vs Intlayer. Celui-ci se concentre sur ce que l'adaptateur transforme lorsque vous conservez votre code tel quel.

    tl;dr : Sur la même application Next.js, remplacer next-i18next par @intlayer/next-i18next fait passer le JavaScript par page de 218.5 Ko à 150.7 Ko gzip (configuration naïve) et surpasse la configuration next-i18next entièrement optimisée (163.4 Ko) de 12.7 Ko. Le composant moyen passe de 78.5 Ko à 9.7 Ko, la fuite de chaînes vers d'autres pages passe de ~90% à 0%, l'hydratation de 15.6 ms à 11.3 ms, et le runtime de 19.7 Ko à 9.4 Ko. Aucun composant n'a été modifié, un seul fichier de provider l'a été. Les plugins i18next (backends, détecteurs de langue) sont acceptés mais ne font rien : il n'y a plus rien à charger ni à détecter au runtime.

    Ce qu'est @intlayer/i18next

    i18next est un runtime. i18n.init({ resources }) ou un plugin backend charge locales/{lng}/{ns}.json dans une instance globale ; useTranslation("about") y abonne le composant ; t("title") recherche la clé au moment du rendu. Les namespaces, le lazy loading, les listes de namespaces par page et la sécurité de typage sont entièrement à votre charge en matière de configuration et de maintenance.

    Les adaptateurs conservent l'API et remplacent l'instance :

    1. Alias d'importation. createNextI18nPlugin() depuis @intlayer/next-i18next/plugin (ou withI18next) enveloppe withIntlayer et ajoute des alias Webpack / Turbopack pour que next-i18next, react-i18next et i18next renvoient vers leurs équivalents @intlayer/*. Sur Vite, reactI18nextVitePlugin() depuis @intlayer/react-i18next/plugin fait de même. Aucun import n'a besoin d'être renommé.
    2. JSON comme source de vérité. Le plugin syncJSON lit vos fichiers existants locales/{lng}/{ns}.json avec format: "i18next" (ainsi {{name}}, l'imbrication $t(), les suffixes _one / _other et de contexte sont correctement analysés) et réécrit les traductions lorsque le CLI ou le CMS les met à jour.
    3. Liaison sur le site d'appel. La passe d'optimisation d'Intlayer réécrit useTranslation("about") en un appel qui reçoit directement le dictionnaire about, dans la locale active. Le composant cesse d'interroger le store global.
    components/About.tsx
    // Votre code, inchangé
    import { useTranslation } from "react-i18next";
    
    const About = () => {
      const { t } = useTranslation("about");
      return <h1>{t("title")}</h1>;
    };
    
    Ce que le compilateur émet (simplifié)
    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>;
    };
    

    Cette réécriture est précisément ce qui fait basculer les colonnes de taille des composants et de fuite de page ci-dessous.

    Ce que les adaptateurs conservent, ignorent et ne remplacent pas

    API i18nextAvec @intlayer/*
    useTranslation("ns"), useTranslation("ns", { keyPrefix })✅ Conservé. Lié au dictionnaire ns au moment du build ; clés typées selon votre contenu
    t("key", { name }), {{interpolation}}, imbrication $t(key)✅ Conservé
    Pluriels key_one / key_other, contexte key_male, returnObjects✅ Conservé. Pluriels évalués avec Intl.PluralRules
    <Trans> avec components, balises numérotées <1>...</1>, values✅ Conservé
    withTranslation, Translation, I18nContext✅ Conservé
    i18n.changeLanguage(), i18n.language, i18n.dir(), on("languageChanged")✅ Conservé. changeLanguage pilote la locale d'Intlayer
    getFixedT(lng, ns, keyPrefix), i18n.exists(), hasLoadedNamespace()✅ Conservé
    i18n.use(Backend).use(LanguageDetector).init({...})⚠️ use() appelle le init du plugin et s'arrête là ; les backends et détecteurs n'ont rien à charger ni à détecter
    init({ resources }), addResourceBundle()⚠️ resources est ignoré avec un avertissement en dev ; supprimez les imports JSON pour obtenir les gains de bundle
    I18nextProvider i18n={i18n}⚠️ Rend un IntlayerProvider ; la prop i18n est ignorée. Sur App Router, transmettez la locale (voir ci-dessous)
    serverSideTranslations(locale, ["common"]) (next-i18next)⚠️ Renvoie la structure attendue et ne charge rien. Inoffensif à garder, sûr à supprimer
    appWithTranslation(App) (next-i18next)✅ Conservé
    next-i18next.config.js⚠️ Non lu. Les locales proviennent de intlayer.config.ts
    useTranslation() sans namespace spécifié✅ Fonctionne avec le dictionnaire global translation du fichier complet (splitKeys: false)

    Le benchmark

    Ce qui a été mesuré

    La suite Benchmark Bloom construit la même application avec chaque configuration : 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 identiques et un contenu identique. Les pages sont mesurées en en et fr.

    next-i18next a été testé avec quatre stratégies de chargement, depuis le JSON de chaque locale importé dans resources (static) jusqu'à un namespace par route, chargé paresseusement via un backend (scoped-dynamic). L'adaptateur a été construit sur les mêmes composants que la configuration naïve, avec uniquement next.config.ts, intlayer.config.ts et le fichier de provider modifiés. Il ne propose aucune variante "scopée" manuelle : le compilateur scope le contenu par composant.

    Pour chaque build, la suite enregistre :

    • Taille de la lib : taille gzip d'un composant vide qui n'importe que la bibliothèque i18n.
    • JS par page : moyenne du JavaScript gzip téléchargé par page sur toutes les pages et locales.
    • % de fuite de locale : part des chaînes traduites dans le JS téléchargé qui appartiennent à une locale que l'utilisateur ne consulte pas.
    • % de fuite de page : part des chaînes traduites dans le JS téléchargé qui appartiennent à une page sur laquelle l'utilisateur ne se trouve pas.
    • Moyenne composant : taille moyenne gzip de chaque composant compilé isolément.
    • Réactivité E2E : temps réel mesuré entre le choix d'une nouvelle locale et la mise à jour de html[lang] dans le DOM (Playwright, 5 itérations).
    • Hydratation : durée de la phase d'hydratation de React.
    Les chiffres ci-dessous proviennent de l'exécution du 12/09/2026 avec next-i18next 16.3.0 (react-i18next 17.0.13, i18next 26.4.2) et @intlayer/next-i18next 9.5.1. L'application de test est volontairement compacte (quelques dizaines de chaînes par locale), les pourcentages de fuite décrivent donc une tendance : ils croissent avec votre contenu tandis que le coût du runtime reste fixe.

    Résultats sur Next.js

    ConfigurationStratégieTaille lib (gz)JS page moy (gz)Fuite localeFuite pageComposant moy (gz)Réactivité E2EHydratation
    base (sans i18n)-0.0 Ko141.0 Ko0.0%0.0%0.9 Ko13.4 ms11.8 ms
    next-i18nextstatic19.7 Ko218.5 Ko0.0%89.8%78.5 Ko16.4 ms15.6 ms
    next-i18nextdynamic19.7 Ko169.5 Ko50.0%89.8%26.1 Ko15.4 ms27.7 ms
    next-i18nextscoped-static19.7 Ko220.1 Ko0.0%89.8%78.9 Ko16.4 ms14.7 ms
    next-i18nextscoped-dynamic19.7 Ko163.4 Ko0.0%0.0%27.1 Ko15.9 ms15.1 ms
    @intlayer/next-i18nextstatic9.4 Ko150.7 Ko0.0%0.0%9.7 Ko10.7 ms11.3 ms
    @intlayer/next-i18nextdynamic9.4 Ko150.7 Ko0.0%0.0%9.7 Ko11.9 ms10.6 ms
    next-intlayer (natif)static5.5 Ko141.3 Ko0.0%0.0%8.5 Ko15.5 ms16.9 ms
    next-intlayer (natif)dynamic5.5 Ko141.3 Ko0.0%0.0%6.9 Ko15.3 ms15.9 ms

    Comment l'interpréter

    • 68 Ko de moins par page par rapport à la configuration naïve. resources: { en, fr, ... } expédie chaque locale et chaque namespace sur chaque page : 218.5 Ko. Le build avec adaptateur pour les mêmes composants tombe à 150.7 Ko. Il surpasse également la meilleure configuration de next-i18next (163.4 Ko, un namespace par route, chargé à la demande) de 12.7 Ko, car le runtime i18next pèse à lui seul 19.7 Ko contre 9.4 Ko.
    • La fuite passe à 0% sans toucher au moindre composant. Chaque configuration de next-i18next sauf la version entièrement découpée expédie ~90% de chaînes d'autres pages. La ligne dynamic est plus pénalisante qu'il n'y paraît : elle n'élimine pas la fuite de page et introduit 50% de fuite de locale, car le backend par locale rapatrie toujours l'intégralité du namespace translation. L'adaptateur atteint 0% / 0% directement sur le code d'origine.
    • Composants : 8x plus légers. Un composant utilisant useTranslation() compilé isolément pèse en moyenne 78.5 Ko avec resources inliné et 26-27 Ko avec un backend, car t reste lié au store global. Avec l'adaptateur, il n'affiche plus que 9.7 Ko.
    • Hydratation et basculement plus rapides. L'hydratation passe de 15.6 ms à 11.3 ms (et de 27.7 ms dans la configuration dynamic, où l'appel backend se situe sur le chemin critique). Le basculement de locale passe de 15-16 ms à 11-12 ms.
    • L'adaptateur n'est pas le runtime natif. next-intlayer atteint 141.3 Ko, soit +0.3 Ko par rapport à l'application de base. L'adaptateur supporte la surface d'API de i18next (dialecte d'interpolation, résolution des pluriels et contextes, analyse des balises <Trans>) en surcouche du cœur d'Intlayer : 9.4 Ko et +9.4 Ko par page par rapport au natif. Il constitue une passerelle, non une fin en soi.
    L'adaptateur react-i18next sur Vite / TanStack Start n'a pas été inclus dans ce cycle de test. La référence pour react-i18next sur TanStack Start se trouve dans i18next vs Intlayer : 127-184 Ko par page et 123-185 ms de temps de basculement de locale quand le backend est différé.

    Pourquoi ces écarts de métriques

    Rien n'a changé dans le dossier components/ : les gains proviennent donc exclusivement de la cible à laquelle useTranslation est relié.

    Avec i18next, la liaison s'établit avec l'instance globale. Tout ce qui y a été chargé (toutes les locales en static, l'ensemble du namespace de la locale active en dynamic) est accessible depuis chaque composant appelant useTranslation(). Le bundler ne peut pas découper plus finement que ce que l'instance retient, et le runtime ne peut pas anticiper les clés demandées au rendu.

    bash
    .
    ├── next-i18next.config.js
    ├── public/locales
       ├── en/translation.json           # toutes les chaînes de chaque page
       └── fr/translation.json
    ├── i18n/i18n.ts                      # i18n.use(initReactI18next).init({ resources })
    └── components
        ├── AppProviders.tsx              # <I18nextProvider i18n={i18n}>
        └── About.tsx                     # useTranslation(); t("about.title")
    

    Avec @intlayer/next-i18next, la liaison s'opère directement avec le dictionnaire. syncJSON convertit chaque fichier de namespace en dictionnaire ; la passe d'optimisation transmet au composant le dictionnaire correspondant sous forme d'un import que le bundler peut tracer et séparer par page et par locale.

    bash
    .
    ├── intlayer.config.ts                # syncJSON({ format: "i18next", source: ... })
    ├── public/locales
       ├── en/translation.json           # inchangé, toujours source de vérité
       └── fr/translation.json
    ├── .intlayer/                        # généré : un dictionnaire par namespace, par locale
    └── components
        ├── AppProviders.tsx              # <IntlayerClientProvider locale={locale}>
        └── About.tsx                     # useTranslation(); t("about.title")  ← inchangé
    

    Le fichier i18n/i18n.ts et son import de resources deviennent du code mort. C'est là que résident les 68 Ko d'économie.

    Migration en trois étapes

    1. Installation

      bash
      npx intlayer init --interactive
      

      La commande détecte i18next / react-i18next / next-i18next, installe intlayer, le package de framework (next-intlayer ou react-intlayer), l'adaptateur @intlayer/* correspondant ainsi que @intlayer/sync-json-plugin, et pré-remplit intlayer.config.ts. Laissez les packages d'origine installés : ils servent de peer dependencies et fournissent les définitions de types.

    2. Pointer Intlayer vers vos fichiers de locales

      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({
            // dialecte i18next : {{name}}, $t(key), key_one / key_other, key_male
            format: "i18next",
            // Un fichier par namespace : `useTranslation("about")` → about.json
            source: ({ locale, key }) => `./public/locales/${locale}/${key}.json`,
            location: "public/locales",
          }),
        ],
      };
      
      export default config;
      

      Si vous disposez d'un unique fichier translation.json par locale (le namespace par défaut d'i18next), définissez splitKeys: false afin que le fichier complet reste un seul dictionnaire et qu'un appel simple à useTranslation() continue de fonctionner.

    3. Ajouter le plugin

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

      Sur l'App Router, les composants clients obtiennent leur locale via le segment [locale]. L'I18nextProvider de l'adaptateur ne prenant aucune locale en paramètre, remplacez-le une seule fois dans votre fichier 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>
      );
      

      Tous les composants enfants continuent d'appeler useTranslation() sans modification.

      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() enveloppe vite-intlayer et crée les alias pour react-i18next et i18next. Pour un projet sans React, i18nextVitePlugin() depuis @intlayer/i18next/plugin crée l'alias pour i18next seul.

    Ce que vous pouvez supprimer par la suite

    Fichier / modèleRaison
    resources: { en, fr, ... } et les imports JSONIgnorés par l'adaptateur. C'est ici que se trouvaient les 68 Ko
    i18next-http-backend, i18next-resources-to-backendPlus rien à récupérer au runtime
    i18next-browser-languagedetectorLa détection de locale dépend du routage d'Intlayer (préfixe URL, cookie, en-tête)
    serverSideTranslations() dans getStaticPropsRenvoie une structure vide ; inoffensif, mais inutile
    next-i18next.config.jsNon lu. Les locales sont déclarées dans intlayer.config.ts
    Listes ns: [...] par pageLe compilateur sélectionne les namespaces par composant

    Ce que vous gagnez au-delà des octets

    • Clés typées. useTranslation("about") est typé d'après le dictionnaire compilé about ; t("does.not.exist") déclenche une erreur TypeScript au lieu de renvoyer la chaîne de clé.
    • npx intlayer test bloque la CI en cas de clé manquante dans n'importe quelle langue. npx intlayer fill traduit automatiquement les clés manquantes avec votre propre clé d'API (OpenAI, Anthropic, Mistral, Gemini...) et les réécrit dans locales/{lng}/{ns}.json.
    • Éditeur visuel et CMS opèrent sur le même JSON, permettant aux équipes éditoriales de modifier le contenu via une interface pendant que les fichiers se mettent à jour.
    • Transition progressive vers .content.ts. Chaque composant peut basculer indépendamment de useTranslation("about") à useIntlayer("about") avec un fichier de contenu dédié. Fichiers JSON et .content.ts coexistent naturellement.

    Limites à connaître avant de démarrer

    • Backends et détecteurs sont inertes. i18n.use(HttpBackend) exécute le init du plugin et rien de plus. Si votre application dépendait de traductions rapatriées depuis un CMS au moment de la requête, ce flux n'existe plus ; utilisez le CMS d'Intlayer ou les commandes intlayer pull / push.
    • resources est ignoré, non fusionné. Contrairement à certains adaptateurs, @intlayer/i18next n'utilise pas resources inliné comme solution de repli. Chaque clé doit exister dans les dictionnaires synchronisés, ce que valide intlayer test.
    • L'App Router nécessite la retouche du provider. Un seul fichier, présenté ci-dessus. Le Pages Router avec appWithTranslation ne nécessite aucun ajustement.
    • next-i18next.config.js n'est pas lu. localePath, fallbackLng, reloadOnPrerender et équivalents n'ont pas d'effet direct ; les locales et le repli sont gérés dans intlayer.config.ts.
    • L'adaptateur a un coût résiduel. 9.4 Ko de runtime et +9.4 Ko par page par rapport à next-intlayer. Lorsque tous vos composants seront passés à useIntlayer, retirez-le.

    Quand choisir quelle solution ?

    • Restez sur i18next si votre application nécessite impérativement des backends au runtime (traductions servies par un CMS à la volée), l'écosystème spécifique de plugins, ou une cible hors React non couverte par les adaptateurs.
    • Optez pour @intlayer/* si vous utilisez react-i18next / next-i18next et souhaitez récupérer 68 Ko, obtenir des composants 8x plus légers, 0% de fuite, des clés typées et des contrôles en CI sans devoir tout réécrire. C'est le point d'entrée idéal pour une codebase i18next existante.
    • Passez au natif (next-intlayer / react-intlayer) pour les nouveaux projets ou une fois l'adaptateur assimilé. C'est la formule la plus légère (5.5 Ko, +0.3 Ko par page) qui débloque les Server Components synchrones et les fichiers de contenu .content.ts par composant.

    Comparatifs associés

    Conclusion

    i18next constitue le runtime le plus lourd de ce benchmark, et les adaptateurs en retirent la majeure partie sans vous obliger à quitter son API. Sur la même application Next.js, cela représente 68 Ko de moins par page par rapport à la configuration naïve, 12.7 Ko de moins que la version la plus optimisée à la main, des composants 8x plus légers, 0% de fuite et 4 ms d'hydratation, le tout en échange d'un fichier de configuration, d'une ligne de plugin et d'un ajustement de provider. Les backends et détecteurs deviennent sans effet, resources est ignoré plutôt que fusionné, et le runtime natif next-intlayer conserve 9 Ko d'avance supplémentaire en légèreté.

    L'ensemble des données brutes, des applications de test et des scripts est disponible dans le dépôt Benchmark Bloom. Vous pouvez le reproduire vous-même.

    Consultez le document Pourquoi Intlayer ? pour en savoir plus.

    Commentaires

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

    Articles similaires

    Derniers articles