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

    next-intl VS @intlayer/next-intl | Même API, Bundle différent

    @intlayer/next-intl est un adaptateur de compatibilité : il expose l'API next-intl (useTranslations, getTranslations, useLocale, t.rich(), pluriels ICU, NextIntlClientProvider...) et la sert à partir de dictionnaires compilés par Intlayer. Le code de l'application ne change pas. Le bundle, lui, change.

    Cet article compare les deux sur la même application Next.js, construite une fois avec next-intl et une fois avec l'adaptateur. Les chiffres proviennent de Benchmark Bloom, une suite open-source qui enregistre ce que le navigateur télécharge réellement. Si vous voulez la comparaison next-intl vs Intlayer en tant que bibliothèques, consultez next-intl vs Intlayer. Celui-ci traite de ce que l'adaptateur change lorsque vous conservez vos composants tels qu'ils sont.

    tl;dr: Sur la même application Next.js, remplacer next-intl par @intlayer/next-intl a réduit le JavaScript par page de 153.6 KB à 147.5 KB en gzip, le composant moyen de 21.8 KB à 8.1 KB, la fuite de chaînes de pages étrangères de ~90% à 0%, et l'hydratation de 14.7 ms à 12.8 ms, sans modifier aucun composant. Sur TanStack Start, l'équivalent use-intl (@intlayer/use-intl) a réduit les composants de 76-87 KB à 9-11 KB et le changement de locale de 7-21 ms à 4-9 ms. L'adaptateur consomme 8.0 KB de runtime contre 14.7 KB pour next-intl et 5.5 KB pour next-intlayer natif. La navigation et les middleware sont réimplémentés sur la configuration de routage d'Intlayer; les pathnames localisés sont la seule fonctionnalité non reprise.

    Qu'est-ce que @intlayer/next-intl

    next-intl est un runtime : getRequestConfig charge un messages/{locale}.json par requête, NextIntlClientProvider l'envoie au client, et useTranslations("about") lit les clés de cet objet au moment du rendu. Chaque optimisation (namespaces, pick(messages, [...]) par page, lazy loading) est à votre charge.

    @intlayer/next-intl conserve la première et la dernière partie de cette chaîne et remplace celle du milieu. Vos composants appellent toujours useTranslations("about"); ce qu'ils reçoivent provient d'un dictionnaire Intlayer compilé au moment de la construction, limité à ce composant, dans la locale active uniquement.

    Trois mécanismes le rendent possible :

    1. Aliasing d'imports. createNextIntlPlugin() depuis @intlayer/next-intl/plugin encapsule withIntlayer et ajoute des alias Webpack / Turbopack pour que next-intl, next-intl/server, next-intl/navigation et next-intl/middleware se résolvent en @intlayer/next-intl. Aucun import dans votre codebase n'est renommé.
    2. JSON comme source de vérité. Le plugin syncJSON lit votre messages/{locale}.json existant, divise ses clés de niveau supérieur en un dictionnaire par namespace, et réécrit les traductions dans les mêmes fichiers lorsque la CLI ou le CMS les met à jour. Le workflow de vos traducteurs reste inchangé.
    3. Call-site binding. The Intlayer optimize pass (Babel or SWC) rewrites useTranslations("about") into a call that receives the about dictionary directly. The component no longer reaches a global message tree; it reaches its own content.
    app/[locale]/about/page.tsx
    // Votre code, inchangé
    import { useTranslations } from "next-intl";
    
    const AboutPage = () => {
      const t = useTranslations("about");
      return <h1>{t("title")}</h1>;
    };
    
    What the compiler emits (simplified)
    import _dicHash_about from "../.intlayer/dictionaries/about.mjs";
    import { useDictionary as useTranslations } from "@intlayer/next-intl";
    
    const AboutPage = () => {
      const t = useTranslations(_dicHash_about);
      return <h1>{t("title")}</h1>;
    };
    

    Cette réécriture explique pourquoi les colonnes taille des composants et fuite de page ci-dessous se déplacent : une page récupère uniquement les dictionnaires des composants qu'elle rend, et uniquement dans la locale servie.

    Ce que l'adaptateur conserve, ignore et ne remplace pas

    API next-intlAvec @intlayer/next-intl
    useTranslations("ns") / getTranslations("ns")✅ Conservé. Lié au dictionnaire ns au moment de la compilation. Les clés sont typées par rapport à votre contenu.
    getTranslations({ locale, namespace })✅ Conservé
    t("key", { name }), t.rich(), t.markup(), t.raw()✅ Conservé. Les pluriels ICU, select, selectordinal, #, {ts, date, long} s'exécutent via le résolveur ICU d'Intlayer
    useLocale() / getLocale() / setRequestLocale() / setLocale✅ Conservé
    useFormatter()✅ Conservé. dateTime, number, relativeTime, list, dateTimeRange font le pont vers Intl natif
    NextIntlClientProvider✅ Conservé. Les props messages, timeZone et now sont acceptés mais ignorés (un avertissement dev vous l'indique)
    getMessages()✅ Conservé pour la compatibilité; plus nécessaire
    getRequestConfig() in src/i18n.ts⚠️ Non nécessaire. Les dictionnaires sont compilés au moment de la construction; il n'y a pas de chargement de messages par requête
    defineRouting()✅ Conservé. Les champs omis (locales, defaultLocale, localePrefix) sont lus depuis intlayer.config.ts
    createNavigation(), Link, redirect, usePathname, useRouter✅ Conservé. Réimplémenté sur la config de routage d'Intlayer ; l'argument routing est accepté mais ignoré
    pathnames (noms de routes localisés)❌ Accepté pour le typage, non interpolé. Conservez les chemins simples ou déplacez ce mapping vers Intlayer's rewrite
    createMiddleware()✅ Conservé. Retourne le proxy d'Intlayer ; définit le cookie NEXT_LOCALE afin que useLocale() et votre sélecteur de langue continuent de fonctionner
    NEXT_LOCALE cookie✅ Lu par défaut (sauf si vous configurez routing.storage vous-même)
    useTranslations() nu sans namespace⚠️ Fonctionne, mais le site d'appel n'est pas lié : il se résout via le registre runtime. Passez un namespace pour obtenir les gains de bundle

    Le benchmark

    Ce qui a été mesuré

    La suite Benchmark Bloom construit la même application avec chaque configuration : 10 pages (home, about, blog, careers, contact, FAQ, pricing, products, settings, team), 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-intl a été construit avec quatre stratégies de chargement, de la configuration naïve (chargement entier de messages/{locale}.json) à la configuration optimale (un namespace par route + pick() par page). L'adaptateur a été construit sur les mêmes composants que la configuration naïve, avec seulement next.config.ts et intlayer.config.ts modifiés. Il n'a pas de variante "scoped" : le compilateur scopes le contenu par composant, donc ses lignes static et dynamic sont déjà scoped.

    Pour chaque build, la suite enregistre :

    • Lib size : taille gzip d'un composant vide qui importe uniquement la bibliothèque i18n. Le coût fixe du runtime.
    • Page JS : JavaScript gzip téléchargé par page, en moyenne sur toutes les pages et locales.
    • Fuite locale % : part des chaînes traduites trouvées dans le JS téléchargé appartenant à une locale que l'utilisateur ne consulte pas.
    • Fuite page % : part des chaînes traduites trouvées dans le JS téléchargé appartenant à une page sur laquelle l'utilisateur n'est pas.
    • Composant moy : taille gzip moyenne de chaque composant compilé isolément. Montre combien de runtime i18n et de catalogue un seul composant entraîne.
    • Réactivité E2E : temps écoulé entre la sélection 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 React.
    Les chiffres ci-dessous proviennent de l'exécution datée du 2026-09-12 avec next-intl / use-intl 4.14.2 et @intlayer/* 9.5.1. L'application de test est volontairement petite (quelques dizaines de chaînes par locale), donc les pourcentages de fuite décrivent un modèle : ils augmentent avec votre contenu tandis que le coût d'exécution reste fixe.

    Résultats sur Next.js

    SetupStrategyLib size (gz)Page JS avg (gz)Locale leakPage leakComponent avg (gz)E2E reactivityHydration
    base (no i18n)-0.0 KB141.0 KB0.0%0.0%0.9 KB13.4 ms11.8 ms
    next-intlstatic14.7 KB153.6 KB4.2%89.8%21.8 KB16.0 ms14.7 ms
    next-intldynamic14.7 KB153.6 KB9.7%89.9%21.8 KB15.6 ms14.8 ms
    next-intlscoped-static14.7 KB153.6 KB0.0%0.0%80.1 KB17.9 ms17.4 ms
    next-intlscoped-dynamic14.7 KB153.6 KB0.0%0.0%22.9 KB17.8 ms16.8 ms
    @intlayer/next-intlstatic8.0 KB147.5 KB0.0%0.0%8.1 KB14.5 ms12.8 ms
    @intlayer/next-intldynamic8.0 KB148.7 KB0.0%0.0%8.1 KB11.7 ms12.8 ms
    next-intlayer (native)static5.5 KB141.3 KB0.0%0.0%8.5 KB15.5 ms16.9 ms
    next-intlayer (native)dynamic5.5 KB141.3 KB0.0%0.0%6.9 KB15.3 ms15.9 ms

    Comment le lire

    • Mêmes composants, 6 KB de moins par page. L'adapter build de l'application naïve atterrit à 147.5 KB, sous chaque configuration next-intl y compris la plus optimisée (153.6 KB). Le runtime lui-même est la différence : 8.0 KB versus 14.7 KB, payés sur chaque page.
    • La fuite passe à 0% sans toucher à un composant. La configuration naïve de next-intl expédie ~90% des chaînes de pages étrangères sur chaque page. Atteindre 0% avec next-intl signifie les setups scoped-* : un namespace par route, et pick(messages, [...]) dans chaque page. L'adapter atteint 0% à partir du code naïf car la passe d'optimisation lie chaque useTranslations("ns") à son propre dictionnaire.
    • Les composants rétrécissent 2.7x. Un composant compilé en isolation fait en moyenne 21.8 KB avec next-intl (il atteint le provider et l'arborescence des messages) et 8.1 KB avec l'adapter. Dans le setup scoped-static de next-intl, ce nombre monte à 80 KB, car le fichier namespace de chaque route devient accessible à partir de la page qui le sélectionne.
    • L'hydratation est 2 ms plus rapide (12.8 vs 14.7 ms) : il n'y a pas d'objet de message à désérialiser de la charge utile RSC avant que React puisse hydrater.
    • L'adaptateur n'est pas le runtime natif. next-intlayer s'élève à 141.3 KB, +0.3 KB par rapport à l'app de base, avec un runtime de 5.5 KB. L'adaptateur transporte la surface API de next-intl (useFormatter, t.rich, le résolveur ICU) au-dessus du noyau d'Intlayer, d'où 8.0 KB et +6 KB par page. C'est le pont, pas la destination.

    Résultats sur TanStack Start (use-intl)

    use-intl est le noyau framework-agnostique de next-intl. Son adaptateur, @intlayer/use-intl, suit le même design avec un plugin Vite (@intlayer/use-intl/plugin).

    ConfigurationStratégieTaille lib (gz)JS page moy (gz)Fuite localeFuite pageComposant moy (gz)Réactivité E2EHydratation
    base (pas i18n)-0.0 KB111.0 KB0.0%0.0%0.7 KB8.1 ms21.6 ms
    use-intlstatic14.1 KB179.8 KB50.0%89.8%76.0 KB6.7 ms15.3 ms
    use-intldynamic14.1 KB119.4 KB0.0%89.8%75.9 KB7.0 ms15.4 ms
    use-intlscoped-static14.1 KB128.7 KB0.0%0.0%87.1 KB20.9 ms24.8 ms
    use-intlscoped-dynamic14.1 KB128.7 KB0.0%0.0%87.1 KB13.3 ms25.9 ms
    @intlayer/use-intlstatic7.3 KB135.8 KB49.7%0.0%10.9 KB4.2 ms10.5 ms
    @intlayer/use-intldynamic7.3 KB129.7 KB0.0%0.0%9.3 KB8.7 ms16.1 ms
    intlayer (native)static5.0 KB125.8 KB50.0%0.0%8.1 KB3.2 ms11.5 ms
    intlayer (native)dynamic5.0 KB118.6 KB0.0%0.0%6.3 KB3.6 ms14.1 ms

    Comment le lire

    • Les octets par page sont équivalents à l'use-intl optimisé. @intlayer/use-intl en mode dynamic (129.7 KB) est dans les 1 KB de use-intl's scoped-dynamic (128.7 KB), et 10 KB au-dessus du dynamic simple de use-intl (119.4 KB). Cette ligne dynamic simple fuit toujours 90 % des chaînes de pages étrangères ; le nombre d'octets est faible car le contenu de l'application de test est petit. Le 0% de l'adapter reste plat à mesure que le contenu augmente.
    • Les composants sont 7-9x plus petits. Les composants use-intl font en moyenne 76-87 KB dans chaque stratégie, car useTranslations est lié à l'objet de messages complet du provider. L'adaptateur fait en moyenne 9-11 KB.
    • Le changement de locale est plus rapide. Les configurations use-intl optimisées prennent 13-21 ms pour mettre à jour html[lang]; l'adaptateur prend 4-9 ms. Moins de composants se re-rendent, et rien n'est re-sélectionné dans un arbre de messages.
    • static conserve chaque locale. La ligne static de l'adaptateur affiche une fuite de locale de 49.7%, la même que celle d'Intlayer natif en mode static : toutes les locales sont bundlées, seuls les dictionnaires de la page le sont. Une ligne de config (importMode: 'dynamic') la supprime.

    Pourquoi les nombres changent

    Rien dans le composant n'a changé, donc les gains proviennent entièrement de ce à quoi useTranslations est lié.

    Avec next-intl, la liaison est le fournisseur. NextIntlClientProvider reçoit l'objet messages complet pour la locale ; chaque useTranslations("about") le lit depuis celui-ci. Le bundler voit un composant important un hook qui lit un contexte, et ne peut pas savoir que seule la branche about est utilisée. Les routes ci-dessous partagent toutes le même objet message, donc la colonne page-leak affiche ~90% jusqu'à ce que vous divisiez le fichier vous-même.

    bash
    .
    ├── messages
       ├── en.json                       # tous les namespaces, toutes les pages
       └── fr.json
    └── src
        ├── i18n.ts                       # getRequestConfig({ messages: await import(...) })
        ├── middleware.ts                 # createMiddleware(routing)
        └── app/[locale]
            ├── layout.tsx                # <NextIntlClientProvider messages={messages}>
            └── about/page.tsx            # useTranslations("about")
    

    Avec @intlayer/next-intl, la liaison est le dictionnaire. syncJSON transforme messages/en.json en un dictionnaire par clé de niveau supérieur ; le compilateur résout quel composant appelle useTranslations("about") et lui transmet about directement, dans la locale active, en tant qu'import que le bundler peut tracer et diviser.

    bash
    .
    ├── intlayer.config.ts                # syncJSON({ source: ({ locale }) => `./messages/${locale}.json` })
    ├── messages
       ├── en.json                       # inchangé, toujours la source de vérité
       └── fr.json
    ├── .intlayer/                        # généré : un dictionnaire par namespace, par locale
    └── src
        ├── middleware.ts                 # createMiddleware() retourne maintenant le proxy d'Intlayer
        └── app/[locale]
            ├── layout.tsx                # <NextIntlClientProvider> (pas de prop messages)
            └── about/page.tsx            # useTranslations("about")  ← inchangé
    

    src/i18n.ts et la prop messages disparaissent. Tout le reste est identique.

    Migration en trois étapes

    1. Installer

      bash
      npx intlayer init --interactive
      

      La commande détecte next-intl et installe intlayer, next-intlayer, @intlayer/next-intl et @intlayer/sync-json-plugin. Gardez next-intl installé : c'est une dépendance pair de l'adaptateur et il fournit les types.

    2. Pointez Intlayer vers vos messages

      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: {
          // "static" regroupe toutes les locales ; "dynamic" charge celle active à la demande
          importMode: "dynamic",
        },
        plugins: [
          syncJSON({
            // Placeholders ICU : {name}, {count, plural, one {# item} other {# items}}
            format: "icu",
            source: ({ locale }) => `./messages/${locale}.json`,
            location: "messages",
          }),
        ],
      };
      
      export default config;
      

      messages/{locale}.json reste à sa place. Chaque clé de haut niveau devient un dictionnaire ; useTranslations("about") mappe au dictionnaire about.

    3. Encapsuler next.config.ts

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

      createNextIntlPlugin() compose withIntlayer (surveillance du contenu, compilation des dictionnaires, la passe d'optimisation) et les alias next-intl@intlayer/next-intl pour Webpack et Turbopack. Compilez, et les chiffres dans les tableaux ci-dessus sont les vôtres.

    Ce que vous pouvez supprimer ensuite

    Fichier / motifRaison
    getRequestConfig dans src/i18n.tsAucun chargement de message par requête. Conservez le fichier uniquement s'il exporte également des helpers createNavigation
    messages={...} sur NextIntlClientProviderL'adaptateur lit la sortie compilée ; la prop est ignorée et enregistre un avertissement en développement
    await getMessages() dans les layoutsMême raison
    Per-page pick(messages, [...])Le compilateur effectue le picking, par composant

    Ce que vous gagnez au-delà des bytes

    • Typed keys. useTranslations("about") est typé par rapport au dictionnaire compilé about. t("does.not.exist") est une erreur TypeScript, pas un fallback à l'exécution.
    • npx intlayer test échoue dans CI quand une locale est manquante d'une clé. npx intlayer fill traduit les clés manquantes avec le fournisseur de votre choix (OpenAI, Anthropic, Mistral, Gemini...) en utilisant votre propre clé, et écrit le résultat dans messages/{locale}.json.
    • Visual Editor et CMS fonctionnent sur les mêmes dictionnaires, donc les non-développeurs peuvent éditer messages/fr.json via une UI et le fichier se met à jour.
    • Migration progressive vers .content.ts. N'importe quel composant peut passer de useTranslations("about") à useIntlayer("about") avec un fichier de contenu co-localisé, un par un. Les dictionnaires JSON et .content.ts coexistent et fusionnent.

    Limites à connaître avant de commencer

    • La configuration du routage se déplace vers intlayer.config.ts. createNavigation(routing) et createMiddleware(routing) conservent leur signature mais ignorent l'argument : les locales, la locale par défaut et la stratégie de préfixe proviennent de la configuration routing d'Intlayer. Si vous utilisez les pathnames localisés de next-intl (/about/a-propos), l'adaptateur ne les interpole pas ; routing.rewrite d'Intlayer couvre ce cas mais c'est une modification séparée.
    • useTranslations() sans namespace n'est pas lié. La passe d'optimisation a besoin d'un namespace statique pour savoir quel dictionnaire importer. Un appel nu fonctionne quand même, via un registre runtime qui référence chaque dictionnaire, ce qui est exactement la fuite que vous tentiez d'éliminer. Passez le namespace.
    • L'adaptateur n'est pas gratuit. 8.0 KB de runtime contre 5.5 KB pour next-intlayer, et +6-7 KB par page par rapport au build natif. C'est le prix de la surface API next-intl. Si vous en arrivez au point où chaque composant a été déplacé vers useIntlayer, abandonnez l'adaptateur.
    • messages, timeZone, now sur le provider sont ignorés. Les formateurs sont soutenus par Intl natif et seule la locale influence leur sortie; si vous comptiez sur un fuseau horaire forcé ou un now fixe pour des dates stables à l'hydratation, gérez-le au site d'appel.

    Quand utiliser quoi?

    • Restez sur next-intl si votre app est petite, votre bundle n'est pas une préoccupation, et votre équipe est à l'aise pour posséder les namespaces et pick() par page.
    • Utiliser @intlayer/next-intl si vous êtes actuellement sur next-intl et souhaitez les gains en termes de bundle, de fuite mémoire et d'hydratation, des clés typées et les outils CLI / CMS sans réécriture. C'est le point d'entrée recommandé pour toute base de code next-intl existante.
    • Opter pour la solution native (next-intlayer) pour les nouveaux projets, ou une fois que l'adapter a rempli son rôle. C'est la plus légère des trois (5,5 KB, +0,3 KB par page) et déverrouille les composants serveur synchrones, les fichiers .content.ts par composant et l'ensemble complet des fonctionnalités.

    Comparaisons connexes

    Conclusion

    @intlayer/next-intl fait une chose : elle change ce à quoi useTranslations est lié, d'un provider contenant chaque message à un dictionnaire compilé pour ce composant. Sur la même application Next.js, cela représente 6 KB par page, 2,7x plus petits composants, 0% de fuite et 2 ms d'hydratation, avant même que quelqu'un n'ouvre un fichier de composant. La navigation et les middlewares conservent leur API au-dessus de la configuration de routage d'Intlayer, et le runtime natif next-intlayer reste encore plus léger.

    Toutes les données brutes, les applications de test et les scripts se trouvent dans le référentiel Benchmark Bloom. Exécutez-le vous-même.

    Reportez-vous à la documentation « Why Intlayer? » pour plus de détails.

    Commentaires

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

    Articles similaires

    Derniers articles