---
createdAt: 2026-04-20
updatedAt: 2026-04-21
title: Best i18n solution for Next.js in 2026 - Benchmark Report
description: Compare Next.js internationalization (i18n) libraries like next-intl, next-i18next, and Intlayer. Detailed performance report on bundle size, leakage, and reactivity.
keywords:
  - benchmark
  - i18n
  - intl
  - nextjs
  - performance
  - intlayer
slugs:
  - doc
  - benchmark
  - nextjs
author: Aymeric PINEAU
history:
  - version: 8.7.5
    date: 2026-01-06
    changes: "Init benchmark"
---

# Next.js i18n Libraries - 2026 Benchmark Report

Cette page est un rapport de benchmark pour les solutions i18n pour Next.js.

## Table of Contents

<Toc/>

## Interactive Benchmark

<I18nBenchmark framework="nextjs" vertical/>

## Results reference:

<ClickToOpenIframe
src="https://intlayer.org/markdown?url=https%3A%2F%2Fraw.githubusercontent.com%2Fintlayer-org%2Fbenchmark-i18n%2Fmain%2Freport%2Fscripts%2Fsummarize-nextjs.md"
width="100%"
height="600px"
style="border:none;"
/>

> [See complete benchmark data](https://intlayer.org/markdown?url=https%3A%2F%2Fraw.githubusercontent.com%2Fintlayer-org%2Fbenchmark-i18n%2Fmain%2Freport%2Fscripts%2Fsummarize-nextjs.md)

## Introduction

Les solution d'internationalization sont des librairies impactantes pour votre application. Le risque principal est de faire charger le contenu de toutes les pages et dans toutes les langues pour une seule page consultée.

Au moment ou votre application grandit, la taille du bundle va evoluer exponentiellement, ce qui peut significativement ralentir les performances de votre application.

En guise d'example, pour les plus mauvais elèves, une fois internationalisée, votre page peut se retrouver pres de 4x plus volumineuse.

Un autre impact des solution d'internationalization est le ralentissement du developement. Transformer vos components et organizer vos contenu dans toutes les langues, est chonophage.

Le probleme etant complexe, un grand nombres de solutions ont vu le jour pour resoudre le probleme de l'i18n. Certaines solutions se focusant sur la DX, d'autres sur la performance, d'autres sur la scalabilite, etc.

Intlayer est une solution tentant d'optimiser l'ensemble de ces aspects.

## Teste ton application

Pour deteacter ces problematiques, j'ai mis en place un scanner gratuit que vous pouvez tester [ici](https://intlayer.org/i18n-seo-scanner).

## Problematic

Il existe deux grands aspects pour limiter l'impact d'une application multilingue sur le bundle de l'application:

- Diviser vos JSON (ou contenu) en plusieurs fichiers / variables / namespaces pour permettre au bundler de tree-shaker le contenu non utilisé dans votre page
- Loader dynamicement le contenu de votre page uniquement dans la langue de la page

Comprendre les limitations techniques pour ces approaches:

**Dynamic loading**

Meme en declarant vos routes tel que `[locale]/page.tsx`, que ce soit avec Webpack ou Turbopack, et bien que `getStaticParams` soit definie, le bundler de traite pas `locale` comme une variable statique. Cela signifie que le bundler va charger le contenu de toutes les langues pour chaque page. La seule option pour limiter ce probleme est de faire charger le contenu via un import dynamic (e.g. `import('./locales/${locale}.json')`).

Ce qu'il se passe au moment du build, c'est que Next.js va generer un fichier JS (bundle) pour chaque langue (e.g. `./locales_fr_12345.js`). Puis apres que le site soit envoyé au client, au moment ou la page est executée, c'est le browser qui va se changera d'efectuer une requette HTTP supplémentaire, demandant le fichier JS requis (e.g. `./locales_fr_12345.js`).

> Un autre moyen de repondre au meme probleme est d'utiliser la methode `fetch()` pour requester le fichier JSON dynamiquement. C'est le cas de `Tolgee` via le fait de placer les JSON dans le docier `/public` ou bien `next-transalte`, qui se base sur la fonction `getStaticProps` pour charger le contenu. Mais le flow reste le meme, le browser va faire une requette HTTP supplémentaire pour charger l'asset.

**Split du contenu**

Si vous utiliser une syntax tel que `const t = useTranslation()` + `t('my-object.my-sub-object.my-key')`, le JSON doit etre integralement chargé dans le bundle, pour ensuite etre parsé par la librairie afin d'en deduire la valeur de la clé. Or cela signifie qu'une grande partie du contenu sera chargée dans votre bundle, bien que non utilisé dans la page.

Pour repondre a ce probleme, certaines librairies suggerent de declarer manuellement pour chaque page quelles namespaces doivent etre chargées. C'est le cas de `next-i18next`, `next-intl`, `lingui`, `next-translate`, `next-international`.

En compaison, `Paraglide` propose l'ajout d'une etape supplimentaire, avant le build pour transformer le JSON en flat variable, tel que `const en_my_var = () => 'my value'`. Cela permet (en théorie) de tree-shaker le contenu non utilisé dans la page. Mais on le vera, cette methode n'est pas sans compromis.

Enfin, `Intlayer` propose une opitimization au build-time, permettant replacer l'usage de `useIntlayer('my-key')` et d'y attacher directement le contenu correspondant.

## Methodology

Pour ce benchmark, nous avons comparé les librairies suivantes:

- `Base App` (No i18n library)
- `next-intlayer` (v8.7.5)
- `next-i18next` (v16.0.5)
- `next-intl` (v4.9.1)
- `@lingui/core` (v5.3.0)
- `next-translate` (v3.1.2)
- `next-international` (v1.3.1)
- `@inlang/paraglide-js` (v2.15.1)
- `tolgee` (v7.0.0)
- `@lingo.dev/compiler` (v0.4.0)
- `wuchale` (v0.22.11)
- `gt-next` (v6.16.5)

J'ai utilisé le framework `Next.js` version `16.2.4` avec le mode `app router`.

J'ai créé une application multilingue avec **10 pages**, dans **10 langues**.

Et j'ai comparé **4 strategies de chargement**:

| Stratégie              | Pas de Namespaces (Global)                        | Avec Namespaces (Scoped)                                                |
| :--------------------- | :------------------------------------------------ | :---------------------------------------------------------------------- |
| **Chargement Static**  | **Static** : Tout en mémoire au démarrage.        | **Scoped Static** : Segmenté par namespace, tout chargé au démarrage.   |
| **Chargement Dynamic** | **Dynamic** : Chargement à la demande par locale. | **Scoped Dynamic** : Chargement granulaire par namespace et par locale. |

## Synthèse des stratégies

- **Static** : Simple, aucune latence réseau après le chargement initial. Inconvénient : bundle size élevé.
- **Dynamic** : Réduit le poids initial (lazy-loading). Idéal pour un grand nombre de locales.
- **Scoped Static** : Permet une organisation propre du code (séparation logique) sans surcoût de requêtes réseau complexes.
- **Scoped Dynamic** : Approche optimale pour le _Code Splitting_ et la performance. Minimise la charge mémoire en ne chargeant que le strict nécessaire pour la vue courante et la locale active.

## Details des Resultats

### 1 - Les solutions a fuire

Certaines solutions tel que `gt-next` ou `lingo.dev` sont clairement des solutions à éviter. C'est solutions integrent un combo vendor lock-in + poisonning de votre codebase. Et le comble etant que malgré de nombreuse heures a tenrer des les implementer, je n'ai jamais reussi a les faire fonctionner. Que ce soit pour Tanstack start ou Next.js.
Liste de problemes rencontrés:

**(General Translation)** (`gt-next@6.16.5`):

- Pour une application 110kb, `gt-react` y ajoute plus de 440kb additionel.
- `Quota Exceeded, please upgrade your plan`, dès le premier build avec la solution de general ransaltion.
- Translations are not rendered; I get the error `Error: <T> used on the client-side outside of <GTProvider>`, which seems to be a bug in the library.
- While implementing **gt-tanstack-start-react**, I also came across an [issue](https://github.com/generaltranslation/gt/issues/1210#event-24510646961) with the library: `does not provide an export named 'printAST' - @formatjs/icu-messageformat-parser`, which was making the application break. After reporting this issue, the maintainer fixed it within 24 hours.
- The library blocks static rendering of Next.js pages.

**(Lingo.dev)** (`@lingo.dev/compiler@0.4.0`):

- Quota AI dépassé, bloquant totalement le build. Il est alors impossible de push en production sans payer.
- The compiler was missing almost 40% of the translated content. I had to rewrite all `.map` into flat component blocks to make it work.
- Their CLI is buggy. and used to rezet the config file with no reason.
- At build, it totally erase the generated JSONs when there is new content added. As a result, you have only one 3 keys erasing >300 existing keys.

### 2 - Les solutions experimentales

**(Wuchale)** (`wuchale@0.22.11`):

L'idee de `Wuchale` est interessante, mais n'est pas encore au stade de solution viable. J'ai rencontré des problemes de reactivity avec la librairie, et ai du forcer le rerendering de la provider pour faire fonctionner l'application. La documentation est aussi relativement peu claire, complexifant la prise en main.

**(Paraglide)** (`@inlang/paraglide-js@2.15.1`):

`Paraglide` est une solution offrant une approache inovante, et bien pensée. Pour autant, dans le cadre de mon benchmark, on se rend compte que le tree-shaking que vend leur entreprise ne fonctionne ni pour mon implementation de Next.js, ni pour Tanstack start. Le flow et la DX est également plus complexe que les autres solutions. Et je ne suis personellement pas fan du fait de devoir regerer des fichiers JS avant chaque push, créant un risque de conflits constant entre les developpeurs via leur PR. Enfin, la solution semble davantage se focaliser sur Vite que sur Next.js.

### 3 - Les solutions 'ok'

**(Tolgee)** (`tolgee@7.0.0`):

`Tolgee` est une solution repondant aux different problemes evoqués plus tot. Mais j'ai trouvé la prise en main de la solution plus complexe que les autres solutions propsant des approches similaires. La solution ne propose pas de typesafety. Ce qui rend aussi la detection des clés manquantes a la compilation bien plus complexe. J'ai du rewraper les functions de Tolgee par les miennes pour y ajouter la detection de clés manquantes.

**(Next Intl)** (`next-intl@4.9.1`):

`next-intl` est la solution la plus en vogue, et aussi la plus mise en avant les agent AI, mais selon moi a tord. Le get-started est assez simple. Mais en pratique, le process d'optimization pour limiter le leakage est assez complexe. De meme, la cohabitation du dynamic loading + namespacing + typescript types ralentit enormement le developpement. De plus, librairie est assez lourde (~13kb pour `NextIntlClientProvider` + `useTranslations` hook, ce qui est >2x plus lourde que `next-intlayer`). **next-intl** used to block static rendering of Next.js pages. It provides a fix function named `setRequestLocale()`. This issue seems to have been partially solved for centralized content such as `en.json` / `fr.json` / etc. But the solution still blocks static rendering when content is scoped into namespaces such as `en/shared.json` / `fr/shared.json` / `es/shared.json` / etc.

**(Next I18next)** (`next-i18next@16.0.5`):

`next-i18next` est probablement la solution la plus populaire par le fait que ce soit la premiere solution a avoir vu le jour pour repondre aux besoins d'internationalisation d'application Javascript. Cette solution propose aussi un tat de pluggins communautaires pour repondre a certaines problematiques. Pour autant, cette solution presente les exactes meme points noirs que `next-intl`. De plus, librairie est particuliement lourde (~18kb pour `I18nProvider` + `useTranslation` hook, ce qui est 3x plus lourde que `next-intlayer`).
Aussi, les formats de messages divergent entre les librairies. `next-intl` utilise le format ICU MessageFormat, alors que `i18next` utilise son propre format.

**(Next International)** (`next-international@1.3.1`):

`next-international` est une solution repondant aussi aux different problemes evoqués plus tot, mais ne propose pas de difference significatif en rapport a `next-intl`, ou meme `next-i18next`. La solution inclut une fonction `scopedT()` qui permet de traduire le contenu dans un namespace spécifique. Cependant, l'utilisation de cette fonction n'a purement et simplement aucun impact sur la taille du bundle.

**(Lingui)** (`@lingui/core@5.3.0`):

`Lingui` est une solution pour laquelle j'ai entendu beaucoup de bien. Mais j'ai personnellement trouvé le flow basé sur l'execution de commandes `lingui extract` / `lingui compile` plus complexe que les autres approches, sans vrai avantage significatif. J'ai aussi noté le mangue d'armonie entre les syntaxes qui porte les IA a confution. (e.g. `t()`, `t''`, `i18n.t()`, `<Trans>`)

### 4 - Mes recommendations

**(Next Translate)** (`next-translate@3.1.2`):

`next-translate` est ici ma principale recommandation pour ceux aimant l'usage d'une fonction `t()` pour traduire leur contenu. Cette solution offre une approche elegante via `next-transalte-plugin`, optimisant le loading de vos namespaces via `getStaticProps` via un loader Webpack / Turbopack. C'est aussi la solution la plus légère de toute (~2.5kb only). For namespacing, picking namespaces at the config level is also well thought out: you can define namespaces for each page or route. It makes maintenance much easier than the main alternatives, such as **next-intl** or **next-i18next**. Cependant j'ai noté qu'en version `3.1.2`, le build en static rendering ne fonctionne pas. Next.js fallback alors sur le dynamic rendering.

**(Intlayer)** (`next-intlayer@8.7.5`):

Enfin, je ne jugerais pas personellement `next-intlayer`, par soucis d'objectivité, etant donnée qu'il s'agit de ma propre solution.

### Personal note

Cette note est personnelle et n'a pas d'impact sur les resultats du benchmark. Cependant, dans le monde de l'i18n on voit un concensus clair qui est d'utiliser une syntax tel que `const t = useTranslation('xx')` + `<>{t('xx.xx')}</>` pour traduire leur contenu.
Cependant, dans le cadre d'appplication React, l'injection d'une fonction sous forme de `ReactNode` est selon moi un anti-pattern. De plus, cela introduit une complexité additionnel, pouvant etre evitée, et un overhead de JavaScript execution (bien qu'imperceptible).
