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

    Lingui VS Intlayer | Benchmark d'Internationalization (i18n) React & Next.js

    Lingui et Intlayer sont les deux bibliothèques de ce benchmark qui s'appuient sur un compilateur plutôt que sur un runtime pur. Lingui extrait les messages des macros au moment de la compilation et compile les catalogues par locale. Intlayer compile les dictionnaires par composant et les tree-shake par locale. Sur le papier, ils devraient être proches. Les chiffres montrent où ils divergent.

    Les données proviennent de Benchmark Bloom, une suite open-source qui construit la même application avec chaque bibliothèque et enregistre ce que le navigateur télécharge et exécute réellement.

    tl;dr: Lingui est le plus proche d'Intlayer en termes de JavaScript brut par page : 115-120 KB vs 118.6 KB sur TanStack Start une fois le chargement différé configuré, 148.6 KB vs 141.3 KB sur Next.js. L'écart s'élargit ailleurs : un composant Lingui compilé isolément pèse 58-153 KB contre 6-8 KB pour Intlayer, l'hydratation prend 28-34 ms contre 11-14 ms, le fallback pour la locale source fuit 3-15% des chaînes de caractères en dans les pages fr dans chaque setup optimisé, et atteindre ce setup optimisé signifie extraire, compiler et sélectionner manuellement les catalogs par route. Intlayer y arrive sans aucune configuration.

    En résumé

    • Lingui - Basé sur des macros ( t`...` , <Trans>, msg), ICU MessageFormat, catalogues .po / JSON, workflow lingui extract + lingui compile. Compile les IDs de message en hashes courts, supporte le chargement dynamique de catalogues par locale. Bien établi, framework-agnostique, forte histoire de tooling pour traducteurs autour de .po.
    • Intlayer - Modèle de contenu centré sur les composants. Les dictionnaires .content.ts se situent à côté du composant qu'ils servent, un compilateur au moment du build tree-shake et lazy-load par composant et par locale, les types TypeScript stricts sont générés à partir de votre contenu, et les traductions manquantes échouent au moment du build. Livré avec middleware, helpers SEO, un Éditeur Visuel / CMS et 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
    lingui/js-linguiGitHub Repo starsGitHub commit activityLast CommitDec 2016npmnpm downloads
    Les badges se mettent à jour automatiquement. Les snapshots varieront au fil du temps.

    Comparaison des fonctionnalités côte à côte

    FonctionnalitéIntlayer (react-intlayer / next-intlayer)Lingui (@lingui/core / @lingui/react)
    Traductions près des composants✅ Oui, .content.ts colocalisé avec chaque composant⚠️ Chaînes de caractères sources inline dans JSX via macros; traductions dans les catalogues .po centralisés
    Intégration TypeScript✅ Types stricts auto-générés à partir du contenu⚠️ Les macros sont typées ; les IDs de message ne le sont pas, les entrées de catalogue manquantes ne sont pas détectées
    Détection des traductions manquantes✅ Erreur TypeScript + erreur/avertissement au moment de la compilation⚠️ lingui extract rapporte les statistiques ; le fallback à l'exécution utilise la chaîne source
    Contenu riche (JSX / Markdown / composants)✅ Support direct<Trans> avec composants imbriqués
    Support ICU⚠️ WIP✅ Oui (macros plural, select, selectOrdinal)
    Formatage (dates, nombres, devises)useNumber, useDate, ... (Intl sous le capot)i18n.date(), i18n.number()
    Routage localisé et middleware✅ Proxy/middleware intégré, getMultilingualUrls❌ Pas dans le cœur
    SEO helpers (hreflang, sitemap, robots)✅ Built-in helpers❌ Manual
    Synchronous server componentsuseIntlayer from next-intlayer/server works in any child server component⚠️ Needs an I18n instance per request, passed down or set via setI18n
    Tree-shaking (ship only used content)✅ Per component, per locale, automated by the compiler⚠️ Per locale via lingui compile; per route needs manual catalog splitting
    Chargement lazyimportMode: 'dynamic' (une ligne de config)⚠️ import() manuel des catalogues compilés + i18n.load() / i18n.activate()
    Purger le contenu inutilisé✅ Les dictionnaires morts sont supprimés au moment du buildlingui extract --clean supprime les messages obsolètes
    Tester les traductions manquantes (CLI/CI)npx intlayer content test⚠️ Statistiques de lingui extract (pas de code de sortie d'erreur par défaut)
    Build pipeline✅ Un plugin (@intlayer/swc / @intlayer/babel / vite-intlayer)⚠️ Macro plugin (Babel ou SWC) + extract + compile étapes
    AI-powered translation✅ Intégré, utilise vos propres clés de provider❌ Non
    Visual Editor / CMS✅ Visual Editor gratuit + CMS optionnel❌ Non (.po fonctionne avec un TMS externe)
    Serveur MCP & Compétences Agent✅ Oui❌ Non
    Écosystème / communauté⚠️ Plus petit mais en croissance rapide✅ Établi, indépendant du framework

    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, tarification, produits, paramètres, équipe), 10 locales (en, fr, es, de, it, pt, zh, ja, ko, ru), composants identiques et contenu identique. Les pages sont mesurées en en et fr. Chaque bibliothèque est implémentée dans jusqu'à quatre stratégies de chargement, de la configuration naïve à la configuration optimale :

    StratégieDescriptionQui fait cela
    staticTous les catalogues compilés des locales importés et chargés à l'avancePrototypes rapides, code généré par IA
    dynamicSeul le catalogue de la locale active est import()é, mais il contient toutes les pagesLa plupart des projets
    scoped-staticUn catalogue par route, tous bundlés à l'avanceRare
    scoped-dynamicUn catalogue par route + lazy import(). Seulement la page actuelle, la locale actuelleApplications avec un budget de performance strict

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

    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, moyenné sur toutes les pages et locales.
    • Locale leak %: part des chaînes traduites trouvées dans le JS téléchargé qui appartiennent à une locale que l'utilisateur n'est pas en train de consulter (empreinte digitale sur en et fr, donc 50% signifie "l'autre locale mesurée est entièrement présente"; avec 10 locales bundlées, le gaspillage réel est plus élevé).
    • Page leak %: part des chaînes traduites trouvées dans le JS téléchargé qui appartiennent à une page sur laquelle l'utilisateur n'est pas.
    • Component avg: taille gzip moyenne de chaque composant compilé isolément. Montre combien de runtime i18n et de catalog un seul composant entraîne.
    • E2E réactivité : 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 de React.
    Les chiffres ci-dessous proviennent de l'exécution datée du 12-09-2026 avec @lingui/react 6.6.0 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 motif : ils augmentent avec votre contenu tandis que le coût d'exécution reste fixe.

    Résultats sur Next.js

    LibraryStrategyLib size (gz)Page JS avg (gz)Locale leakPage leakComponent avg (gz)E2E reactivityHydration
    base (sans i18n)-0.0 KB141.0 KB0.0%0.0%0.9 KB13.4 ms11.8 ms
    Linguistatic11.9 KB207.4 KB50.0%90.0%73.3 KB15.3 ms15.2 ms
    Linguidynamic11.9 KB145.4 KB2.8%89.9%19.9 KB15.7 ms12.7 ms
    Linguiscoped-static11.9 KB148.2 KB2.7%89.1%20.4 KB15.1 ms13.1 ms
    Linguiscoped-dynamic11.9 KB148.6 KB14.8%0.0%152.6 KB16.1 ms14.8 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

    Comment le lire

    • Coût d'exécution. Un composant vide coûte 11.9 KB gzip avec Lingui, 5.5 KB avec Intlayer. Sur la page complète, la meilleure configuration de Lingui se situe à +7.3 KB par rapport à Intlayer (148.6 vs 141.3 KB) ; Intlayer se situe à +0.3 KB par rapport à l'app de base.
    • La configuration naïve est coûteuse. Charger chaque catalog compilé en avant donne 207.4 KB par page, +66 KB par rapport à l'app de base. La moitié des chaînes empreintes appartiennent à la mauvaise locale, 90% à la mauvaise page.
    • Le chargement dynamique corrige la locale, pas la page. Avec un catalog par locale, la fuite de page reste à ~90% : l'intégralité du catalog fr se charge sur chaque page française. Atteindre 0% de fuite de page requiert la configuration scoped-dynamic : un catalog par route, extrait et compilé séparément, sélectionné manuellement dans chaque page.
    • La source-locale fallback fuit. Même dans les configurations optimisées, 3-15% des chaînes en sont incluses dans les pages fr. Les macros Lingui conservent le message source disponible comme fallback, ce qui se retrouve dans le bundle à côté de la traduction. Intlayer résout les fallbacks au moment de la compilation et ne livre que la locale active.
    • La taille du composant explose en scoped-dynamic. Chaque composant compilé isolément fait en moyenne 152.6 KB, car le catalog de chaque route est accessible depuis le composant qui l'importe. Le même composant avec useIntlayer() fait en moyenne 6.9 KB.

    Résultats sur TanStack Start

    LibraryStrategyLib size (gz)Page JS avg (gz)Locale leakPage leakComponent avg (gz)E2E reactivityHydration
    base (no i18n)-0.0 KB111.0 KB0.0%0.0%0.7 KB8.1 ms21.6 ms
    Linguistatic11.2 KB152.2 KB50.0%90.0%58.0 KB3.9 ms19.9 ms
    Linguidynamic11.2 KB115.2 KB9.3%0.0%85.5 KB5.9 ms28.0 ms
    Linguiscoped-static11.2 KB120.8 KB4.0%0.0%147.9 KB7.1 ms33.9 ms
    Linguiscoped-dynamic11.2 KB120.2 KB8.6%0.0%83.7 KB42.1 ms32.9 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
    @intlayer/lingui (compat)dynamic10.3 KB137.0 KB9.9%0.0%12.8 KB2.9 ms19.7 ms

    Comment le lire

    • Sur le JavaScript par page, Lingui gagne de justesse. Le dynamic de Lingui s'élève à 115.2 KB, 3.4 KB sous les 118.6 KB d'Intlayer. Les catalogues compilés de Lingui avec des IDs hachés sont compacts, et le routeur TanStack Start divise les routes suffisamment bien pour que la fuite de page soit déjà à 0% dans la ligne dynamic.
    • Tout ce qui concerne la taille de la page va dans l'autre sens. L'hydratation prend 28-34 ms avec Lingui contre 11-14 ms avec Intlayer : i18n.load() + i18n.activate() s'exécutent sur le client avant que React puisse hydrater. Les composants compilés isolément pèsent 58-148 KB contre 6-8 KB. La fuite de locale n'atteint jamais 0% (4-9%) en raison du fallback de locale source.
    • Le changement de locale dans la configuration optimisée est lent. scoped-dynamic Lingui prend 42 ms pour mettre à jour html[lang] : le nouveau catalogue de route doit être récupéré, chargé et activé avant que le changement soit visible. Intlayer bascule en 3-4 ms dans les deux modes.
    • La ligne static d'Intlayer a déjà 0% de fuite de page car seuls les dictionnaires importés par les composants de la page sont regroupés. Une ligne de config (importMode: 'dynamic') supprime aussi la fuite de locale.
    • @intlayer/lingui conserve la syntaxe des macros de Lingui et les sert à partir des dictionnaires Intlayer. Il échange une taille de page plus importante (137 KB, puisque le runtime des macros reste) pour des composants plus petits (12.8 KB) et une hydratation plus rapide que Lingui natif. C'est une étape de migration, pas la destination.

    Pourquoi l'écart ? Deux compilateurs, deux unités de travail

    Les deux bibliothèques compilent. La différence est ce qu' elles compilent.

    Lingui compile des catalogues. Les macros dans votre source sont extraites dans un fichier .po par locale, puis compilées dans un module JS par locale. L'unité est la locale. Diviser davantage, par route ou par composant, signifie créer plusieurs catalogues, configurer lingui.config.ts pour extraire chacun d'un ensemble différent de fichiers, et charger le bon dans chaque route. L'instance I18n au runtime est globale ; chaque appel useLingui() souscrit le composant à celle-ci.

    bash
    .
    ├── lingui.config.ts
    └── src
        ├── i18n.ts                          # setupI18n(), load(), activate()
        ├── locales
       ├── en
       ├── messages.po
       └── messages.mjs             # lingui compile output
       └── fr
           ├── messages.po
           └── messages.mjs
        ├── components
       └── Counter.tsx                  # const { t } = useLingui(); t`Increment`
        └── routes
            └── $locale
                └── about.tsx                # await import(`../locales/${locale}/messages.mjs`)
    

    Intlayer compile les dictionnaires. Chaque fichier .content.ts est un dictionnaire lié à une clé ; le compilateur résout quel composant importe quelle clé et émet, par dictionnaire et par locale, exactement le JSON dont ce composant a besoin. L'unité est le composant. La portée de la route est une conséquence : une page n'importe que les dictionnaires des composants qu'elle rend.

    bash
    .
    ├── intlayer.config.ts
    └── src
        ├── components
       └── Counter
           ├── index.tsx                # useIntlayer("counter")
           └── index.content.ts
        └── routes
            └── $locale
                ├── about.tsx
                └── about.content.ts
    

    C'est pourquoi le pattern scoped-dynamic est une sortie de build pour Intlayer et une configuration de projet pour Lingui.

    Pour obtenir les numéros de la ligne dynamic, définissez dictionary.importMode: 'dynamic' dans intlayer.config.ts. Voir la documentation d'optimisation du bundle.

    Expérience développeur

    Configuration

    Lingui

    lingui.config.ts
    import { defineConfig } from "@lingui/cli";
    
    export default defineConfig({
      sourceLocale: "en",
      locales: ["en", "fr"],
      catalogs: [
        {
          path: "<rootDir>/src/locales/{locale}/messages",
          include: ["src"],
        },
      ],
    });
    
    src/i18n.ts
    import { setupI18n } from "@lingui/core";
    
    export const loadCatalog = async (locale: string) => {
      const { messages } = await import(`./locales/${locale}/messages.mjs`);
      const i18n = setupI18n();
      i18n.load(locale, messages);
      i18n.activate(locale);
      return i18n;
    };
    

    Puis ajoutez @lingui/babel-plugin-lingui-macro (ou @lingui/swc-plugin) au bundler, exécutez lingui extract après avoir modifié la source, lingui compile avant de construire, et enveloppez l'arborescence dans <I18nProvider i18n={i18n}>.

    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;
    

    Ajoutez intlayer() à vite.config.ts (ou withIntlayer() à next.config.ts) et enveloppez l'arborescence dans <IntlayerProvider>. Aucune étape d'extraction ou de compilation : les dictionnaires sont construits lors de l'exécution du bundler.

    Composant

    Lingui

    src/components/Counter.tsx
    import { useState } from "react";
    import { useLingui } from "@lingui/react/macro";
    import { Trans } from "@lingui/react/macro";
    
    export const Counter = () => {
      const { t, i18n } = useLingui();
      const [count, setCount] = useState(0);
    
      return (
        <div>
          <p>{i18n.number(count)}</p>
          <button aria-label={t`Counter`} onClick={() => setCount((c) => c + 1)}>
            <Trans>Increment</Trans>
          </button>
        </div>
      );
    };
    

    Le texte anglais se trouve dans le composant ; le texte français se trouve dans src/locales/fr/messages.po sous un ID haché, après l'exécution de lingui extract. Oublier de l'exécuter ou d'exécuter compile bascule silencieusement vers l'anglais.

    Intlayer

    src/components/Counter/index.content.ts
    import { t, type Dictionary } from "intlayer";
    
    const counterContent = {
      key: "counter",
      content: {
        label: t({ fr: "Compteur", en: "Counter" }),
        increment: t({ fr: "Incrémenter", en: "Increment" }),
      },
    } satisfies Dictionary;
    
    export default counterContent;
    
    src/components/Counter/index.tsx
    import { useState } from "react";
    import { useIntlayer } from "react-intlayer";
    import { useNumber } from "react-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>
      );
    };
    

    Les deux locales se trouvent dans un seul fichier à côté du composant. Une valeur fr manquante est une erreur de build, une clé incorrecte est une erreur TypeScript.

    En dehors des composants

    Métadonnées, loaders, fonctions serveur : partout sans arborescence React.

    Lingui

    src/routes/$locale/about.tsx
    import { setupI18n } from "@lingui/core";
    import { msg } from "@lingui/core/macro";
    
    const title = msg`À propos de nous`;
    
    export const loader = async ({ params }: { params: { locale: string } }) => {
      const { messages } = await import(
        `../../locales/${params.locale}/messages.mjs`
      );
      const i18n = setupI18n({
        locale: params.locale,
        messages: { [params.locale]: messages },
      });
    
      return { title: i18n._(title) };
    };
    

    Une nouvelle instance I18n par appel, le bon catalogue chargé manuellement, et msg + i18n._() au lieu de t. Comme le benchmark le note, savoir quand utiliser t, t` ` , i18n.t(), msg ou <Trans> « n'est pas intuitif ».

    Intlayer

    src/routes/$locale/about.tsx
    import { getIntlayer } from "intlayer";
    
    export const loader = async ({ params }: { params: { locale: string } }) => {
      const { title } = getIntlayer("about-metadata", params.locale);
    
      return { title };
    };
    

    Conservez les macros Lingui, obtenez les dictionnaires Intlayer

    @intlayer/lingui est un adaptateur prêt à l'emploi pour @lingui/core et @lingui/react. Les macros continuent de se compiler comme avant ; le runtime i18n._() qu'elles compilent est servi à partir des dictionnaires Intlayer, avec des plugins de synchronisation .po qui maintiennent vos catalogues existants comme source de vérité. Les pluriels et sélections ICU s'affichent de manière identique.

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

    Conservez @lingui/babel-plugin-lingui-macro / @lingui/swc-plugin dans la build, exécutées avant le compilateur Intlayer. Consultez la documentation de compatibilité Lingui.

    Quand choisir quoi ?

    • Choisir Lingui si vous voulez ICU MessageFormat avec des macros typées, vos traducteurs travaillent en .po avec un pipeline TMS existant, vous préférez les chaînes source inline en JSX, et votre équipe est à l'aise avec la gestion du workflow extract / compile / catalog-splitting. Son JS par page est compétitif une fois le lazy loading configuré.
    • Choisir Intlayer si vous voulez du contenu scoped au composant, du TypeScript strict, des erreurs de clés manquantes au build-time, du tree-shaking et lazy loading sans effort, de petits composants, une hydratation rapide, un changement de locale instantané, et des outils éditoriaux intégrés (Visual Editor, CMS, traduction IA, serveur MCP). Particulièrement pertinent pour les grandes codebases modulaires et les design systems.
    • Choisir @intlayer/lingui si vous êtes sur Lingui et souhaitez migrer progressivement vers les dictionnaires Intlayer sans modifier les macros.

    Comparaisons associées

    Étoiles GitHub

    Les stars GitHub sont un indicateur fort de la popularité d'un projet, de la confiance de la communauté et de sa pertinence à long terme. Bien que ce ne soit pas une mesure directe de la qualité technique, ils reflètent le nombre de développeurs qui trouvent le projet utile, suivent sa progression et sont susceptibles de l'adopter.

    Star History Chart

    Conclusion

    Lingui est la plus forte bibliothèque runtime-plus-compiler dans ce benchmark. Ses catalogs compilés et hashés lui donnent du JavaScript par page dans quelques KB de Intlayer, et même légèrement en dessous sur TanStack Start. Si les bytes par page étaient la seule métrique, ce serait une égalité.

    Ce n'est pas le cas. Le compilateur de Lingui s'arrête à la locale ; tout ce qui est en dessous (catalogs par route, chargement différé, exclusion de la fallback du bundle) est configuration, et le benchmark montre le coût de cette limite : composants 10-20x plus volumineux, hydratation 2-3x plus lente, 3-15% de fuite de locale qui ne disparaît jamais, et un changement de locale de 42 ms dans la configuration optimisée. Le compilateur d'Intlayer fonctionne au niveau du composant, donc ces chiffres sont 6-8 KB, 11-14 ms, 0% et 3-4 ms sans aucune configuration.

    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.

    Consultez le document 'Pourquoi Intlayer ?' pour plus de détails.

    Commentaires

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

    Articles similaires

    Derniers articles