---
createdAt: 2026-04-20
updatedAt: 2026-04-21
title: Best i18n solution for TanStack Start in 2026 - Benchmark Report
description: Compare TanStack Start internationalization libraries like react-i18next, use-intl, and Intlayer. Detailed performance report on bundle size, leakage, and reactivity.
keywords:
  - benchmark
  - i18n
  - intl
  - tanstack
  - performance
  - intlayer
slugs:
  - doc
  - benchmark
  - tanstack
history:
  - version: 8.7.5
    date: 2026-01-06
    changes: "Init benchmark"
---

# TanStack Start i18n Libraries - 2026 Benchmark Report

Cette page est un rapport de benchmark pour les solutions i18n pour TanStack Start.

## Table of Contents

<Toc/>

## Interactive Benchmark

<I18nBenchmark framework="tanstack" vertical/>

## Results reference:

<ClickToOpenIframe
  src="https://intlayer.org/markdown?url=https%3A%2F%2Fraw.githubusercontent.com%2Fintlayer-org%2Fbenchmark-i18n%2Fmain%2Freport%2Fscripts%2Fsummarize-tanstack.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-tanstack.md)

## Introduction

Les solutions d'internationalisation font partie des dependances les plus impactantes pour une application React. Sur TanStack Start, le risque principal est de charger du contenu inutile: des traductions d'autres pages et d'autres locales dans le bundle d'une seule route.

Au fur et a mesure que votre application grandit, ce probleme peut vite faire exploser le JavaScript envoye au client et ralentir la navigation.

En pratique, pour les implementations les moins optimisees, une page internationalisee peut devenir plusieurs fois plus lourde que la version sans i18n.

L'autre impact est sur la DX: declaration des contenus, types, organisation par namespaces, chargement dynamique, et gestion de la reactivite au changement de locale.

## Teste ton application

Pour detecter rapidement les problemes de leakage i18n, j'ai mis en place un scanner gratuit disponible [ici](https://intlayer.org/i18n-seo-scanner).

## Problematic

Deux axes sont essentiels pour limiter l'impact d'une application multilingue:

- Splitter le contenu par page / namespace pour eviter de charger des dictionnaires entiers inutilement
- Charger dynamiquement la bonne locale uniquement quand necessaire

Comprendre les limitations techniques de ces approches:

**Dynamic loading**

Sans chargement dynamique, la plupart des solutions incluent les messages en memoire des le premier rendu, ce qui cree un surcout important pour les applications avec beaucoup de routes et de locales.

Avec un chargement dynamique, il faut accepter un compromis: moins de JS initial, mais parfois une requete additionnelle au changement de langue.

**Split du contenu**

Les syntaxes basees sur `const t = useTranslation()` + `t('a.b.c')` sont tres pratiques mais poussent souvent a conserver de gros objets JSON en runtime. Ce modele rend le tree-shaking difficile si la bibliotheque ne propose pas une vraie strategie de split par page.

## Methodology

Pour ce benchmark, nous avons compare les librairies suivantes:

- `Base App` (No i18n library)
- `react-intlayer` (v8.7.5-canary.0)
- `react-i18next` (v17.0.2)
- `use-intl` (v4.9.1)
- `@lingui/core` (v5.3.0)
- `@inlang/paraglide-js` (v2.15.1)
- `tolgee` (v7.0.0)
- `react-intl` (v10.1.1)
- `wuchale` (v0.22.11)
- `gt-react` (vlatest)
- `lingo.dev` (v0.133.9)

Le framework utilise est `TanStack Start` avec une application multilingue de **10 pages** et **10 langues**.

Et nous avons comparé **4 stratégies 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-react` ou `lingo.dev` sont clairement des solutions à éviter. Ces solutions intègrent un combo vendor lock-in + empoisonnement de votre codebase. Et le comble étant que malgré de nombreuses heures à tenter de les implémenter, je n'ai jamais réussi à les faire fonctionner correctement sur TanStack Start (comme sur Next.js pour `gt-next`).

Liste de problèmes rencontrés :

**(General Translation)** (`gt-react@latest`) :

- Pour une application ~110kb, `gt-react` peut y ajouter plus de 440kb additionnel (ordre de grandeur observé sur l'implémentation Next.js du même benchmark).
- `Quota Exceeded, please upgrade your plan`, dès le premier build avec la solution General Translation.
- 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.
- These libraries use an anti-pattern through the `initializeGT()` function, blocking the bundle from tree-shaking cleanly.

**(Lingo.dev)** (`lingo.dev@0.133.9`) :

- Quota AI dépassé (ou dépendance serveur bloquante), rendant le flux de build / prod risqué 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 reset the config file with no reason.
- At build, it totally erased the generated JSONs when there was new content added. As a result, you could end up with only a few keys erasing hundreds of existing keys.
- I met reactivity issues with the library on TanStack Start: on locale change I had to force rerendering of the provider to make it work.

### 2 - Les solutions experimentales

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

L'idée de `Wuchale` est intéressante, mais n'est pas encore au stade de solution viable. J'ai rencontré des problèmes de réactivité avec la librairie, et ai dû forcer le rerendering de la provider pour faire fonctionner l'application sur TanStack Start. La documentation est aussi relativement peu claire, complexifiant la prise en main.

### 3 - Les solutions 'ok'

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

`Paraglide` est une solution offrant une approche innovante, 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 implémentation Next.js, ni pour TanStack Start. Le flow et la DX est également plus complexe que les autres solutions. Et je ne suis personnellement pas fan du fait de devoir régénérer des fichiers JS avant chaque push, créant un risque de conflits constant entre les développeurs via leur PR.

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

`Tolgee` est une solution répondant aux différents problèmes évoqués plus tôt. Mais j'ai trouvé la prise en main de la solution plus complexe que les autres solutions proposant des approches similaires. La solution ne propose pas de typesafety. Ce qui rend aussi la détection des clés manquantes à la compilation bien plus complexe. J'ai dû ré-encapsuler les fonctions de Tolgee par les miennes pour y ajouter la détection de clés manquantes.

Sur TanStack Start, j'ai aussi rencontré des problèmes de réactivité : au changement de locale j'ai dû forcer le rerendering du provider, et m'abonner aux événements de changement de locale pour que le chargement dans une autre langue se comporte correctement.

**(use-intl)** (`use-intl@4.9.1`) :

`use-intl` est la brique « intl » la plus en vogue dans l'écosystème React (même famille que `next-intl`), et aussi une des plus mise en avant par les agents AI, mais selon moi à tort dans un contexte performance-first. Le get-started est assez simple. Mais en pratique, le process d'optimisation pour limiter le leakage est assez complexe. De même, la cohabitation du dynamic loading + namespacing + TypeScript types ralentit énormément le développement.

Sur TanStack Start, vous n'avez pas les pièges spécifiques à Next.js (`setRequestLocale`, static rendering), mais vous gardez le même coeur du problème : sans discipline stricte, le bundle embarque vite trop de messages et la maintenance des namespaces par route devient vite pénible.

**(react-i18next)** (`react-i18next@17.0.2`) :

`react-i18next` est probablement la solution la plus populaire par le fait que ce soit l'une des premières à avoir répondu aux besoins d'internationalisation d'applications JavaScript. Cette solution propose aussi tout un tas de plugins communautaires pour répondre à certaines problématiques.

Pour autant, cette solution présente les mêmes grands points noirs que les stacks basées sur une fonction `t('a.b.c')` : optimisations possibles mais extrêmement chronophages, et risque élevé de mauvaises pratiques sur les gros projets (namespaces + chargement dynamique + types).

De plus, les formats de messages divergent : `use-intl` s'appuie sur le format ICU MessageFormat, alors que `i18next` utilise son propre format - ce qui complique encore plus si vous mélangez outillage ou migrations.

**(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'exécution de commandes `lingui extract` / `lingui compile` plus complexe que les autres approches, sans vrai avantage significatif dans ce benchmark TanStack Start. J'ai aussi noté le manque d'harmonie entre les syntaxes qui porte les IA à confusion (e.g. `t()`, `t''`, `i18n.t()`, `<Trans>`).

**(react-intl)** (`react-intl@10.1.1`) :

`react-intl` est une implémentation performante, produite par l'équipe Format.js. En revanche, la DX reste verbeuse : `const intl = useIntl()` + `intl.formatMessage({ id: "xx.xx" })` introduit de la complexité, un surcoût d'exécution JavaScript, et connecte l'instance i18n globale à beaucoup de nœuds du graphe React.

### 4 - Mes recommendations

Ce benchmark TanStack Start ne contient pas d'équivalent direct à `next-translate` (plugin Next.js + `getStaticProps`). Pour les équipes qui veulent absolument une API `t()` avec un écosystème mature, `react-i18next` et `use-intl` restent des choix « raisonnables », mais attendez-vous à investir beaucoup de temps dans l'optimisation pour éviter le leakage.

**(Intlayer)** (`react-intlayer@8.7.5-canary.0`) :

Enfin, je ne jugerais pas personnellement `react-intlayer`, par souci d'objectivité, étant donné qu'il s'agit de ma propre solution.

### Personal note

Cette note est personnelle et n'a pas d'impact sur les résultats du benchmark. Cependant, dans le monde de l'i18n on voit un consensus clair qui est d'utiliser une syntaxe telle que `const t = useTranslation('xx')` + `<>{t('xx.xx')}</>` pour traduire leur contenu.

Cependant, dans le cadre d'applications React, l'injection d'une fonction sous forme de `ReactNode` est selon moi un anti-pattern. De plus, cela introduit une complexité additionnelle, pouvant être évitée, et un overhead de JavaScript execution (bien qu'imperceptible).
