@intlayer/docs 8.7.5-canary.0 → 8.7.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/blog/de/next-i18next_vs_next-intl_vs_intlayer.md +0 -2
  2. package/blog/en-GB/next-i18next_vs_next-intl_vs_intlayer.md +0 -2
  3. package/blog/es/next-i18next_vs_next-intl_vs_intlayer.md +0 -2
  4. package/blog/fr/next-i18next_vs_next-intl_vs_intlayer.md +0 -2
  5. package/blog/id/list_i18n_technologies/frameworks/svelte.md +0 -2
  6. package/blog/it/next-i18next_vs_next-intl_vs_intlayer.md +0 -2
  7. package/blog/ja/next-i18next_vs_next-intl_vs_intlayer.md +0 -2
  8. package/blog/ko/next-i18next_vs_next-intl_vs_intlayer.md +0 -2
  9. package/blog/pl/list_i18n_technologies/frameworks/svelte.md +0 -2
  10. package/blog/pt/next-i18next_vs_next-intl_vs_intlayer.md +0 -2
  11. package/blog/ru/next-i18next_vs_next-intl_vs_intlayer.md +0 -2
  12. package/blog/vi/list_i18n_technologies/frameworks/svelte.md +0 -2
  13. package/blog/zh/next-i18next_vs_next-intl_vs_intlayer.md +0 -2
  14. package/dist/cjs/generated/docs.entry.cjs +60 -0
  15. package/dist/cjs/generated/docs.entry.cjs.map +1 -1
  16. package/dist/esm/generated/docs.entry.mjs +60 -0
  17. package/dist/esm/generated/docs.entry.mjs.map +1 -1
  18. package/dist/types/generated/docs.entry.d.ts +3 -0
  19. package/dist/types/generated/docs.entry.d.ts.map +1 -1
  20. package/docs/ar/benchmark/index.md +29 -0
  21. package/docs/ar/benchmark/nextjs.md +227 -0
  22. package/docs/ar/benchmark/tanstack.md +193 -0
  23. package/docs/ar/intlayer_with_tanstack.md +0 -2
  24. package/docs/de/benchmark/index.md +29 -0
  25. package/docs/de/benchmark/nextjs.md +227 -0
  26. package/docs/de/benchmark/tanstack.md +193 -0
  27. package/docs/en/benchmark/___NOTE.md +82 -0
  28. package/docs/en/benchmark/___nextjs.md +195 -0
  29. package/docs/en/benchmark/___tanstack.md +187 -0
  30. package/docs/en/benchmark/index.md +29 -0
  31. package/docs/en/benchmark/nextjs.md +228 -0
  32. package/docs/en/benchmark/tanstack.md +217 -0
  33. package/docs/en-GB/benchmark/index.md +29 -0
  34. package/docs/en-GB/benchmark/nextjs.md +228 -0
  35. package/docs/en-GB/benchmark/tanstack.md +193 -0
  36. package/docs/es/benchmark/index.md +29 -0
  37. package/docs/es/benchmark/nextjs.md +226 -0
  38. package/docs/es/benchmark/tanstack.md +193 -0
  39. package/docs/fr/benchmark/index.md +29 -0
  40. package/docs/fr/benchmark/nextjs.md +227 -0
  41. package/docs/fr/benchmark/tanstack.md +193 -0
  42. package/docs/hi/benchmark/index.md +29 -0
  43. package/docs/hi/benchmark/nextjs.md +227 -0
  44. package/docs/hi/benchmark/tanstack.md +193 -0
  45. package/docs/id/benchmark/index.md +29 -0
  46. package/docs/id/benchmark/nextjs.md +227 -0
  47. package/docs/id/benchmark/tanstack.md +193 -0
  48. package/docs/id/intlayer_with_react_native+expo.md +0 -2
  49. package/docs/it/benchmark/index.md +29 -0
  50. package/docs/it/benchmark/nextjs.md +227 -0
  51. package/docs/it/benchmark/tanstack.md +193 -0
  52. package/docs/ja/benchmark/index.md +29 -0
  53. package/docs/ja/benchmark/nextjs.md +227 -0
  54. package/docs/ja/benchmark/tanstack.md +193 -0
  55. package/docs/ko/benchmark/index.md +29 -0
  56. package/docs/ko/benchmark/nextjs.md +227 -0
  57. package/docs/ko/benchmark/tanstack.md +193 -0
  58. package/docs/ko/intlayer_with_tanstack.md +0 -2
  59. package/docs/pl/benchmark/index.md +29 -0
  60. package/docs/pl/benchmark/nextjs.md +227 -0
  61. package/docs/pl/benchmark/tanstack.md +193 -0
  62. package/docs/pt/benchmark/index.md +29 -0
  63. package/docs/pt/benchmark/nextjs.md +227 -0
  64. package/docs/pt/benchmark/tanstack.md +193 -0
  65. package/docs/ru/benchmark/index.md +29 -0
  66. package/docs/ru/benchmark/nextjs.md +227 -0
  67. package/docs/ru/benchmark/tanstack.md +193 -0
  68. package/docs/tr/benchmark/index.md +29 -0
  69. package/docs/tr/benchmark/nextjs.md +227 -0
  70. package/docs/tr/benchmark/tanstack.md +193 -0
  71. package/docs/uk/benchmark/index.md +29 -0
  72. package/docs/uk/benchmark/nextjs.md +227 -0
  73. package/docs/uk/benchmark/tanstack.md +193 -0
  74. package/docs/vi/benchmark/index.md +29 -0
  75. package/docs/vi/benchmark/nextjs.md +227 -0
  76. package/docs/vi/benchmark/tanstack.md +193 -0
  77. package/docs/zh/benchmark/index.md +29 -0
  78. package/docs/zh/benchmark/nextjs.md +227 -0
  79. package/docs/zh/benchmark/tanstack.md +193 -0
  80. package/package.json +6 -6
  81. package/src/generated/docs.entry.ts +60 -0
@@ -0,0 +1,217 @@
1
+ ---
2
+ createdAt: 2026-04-20
3
+ updatedAt: 2026-04-21
4
+ title: Best i18n solution for TanStack Start in 2026 - Benchmark Report
5
+ description: Compare TanStack Start internationalization libraries like react-i18next, use-intl, and Intlayer. Detailed performance report on bundle size, leakage, and reactivity.
6
+ keywords:
7
+ - benchmark
8
+ - i18n
9
+ - intl
10
+ - tanstack
11
+ - performance
12
+ - intlayer
13
+ slugs:
14
+ - doc
15
+ - benchmark
16
+ - tanstack
17
+ author: Aymeric PINEAU
18
+ applicationTemplate: https://github.com/intlayer-org/benchmark-i18n-tanstack-start-template
19
+ history:
20
+ - version: 8.7.5
21
+ date: 2026-01-06
22
+ changes: "Init benchmark"
23
+ ---
24
+
25
+ # TanStack Start i18n Libraries - 2026 Benchmark Report
26
+
27
+ This page is a benchmark report for i18n solutions on TanStack Start.
28
+
29
+ ## Table of Contents
30
+
31
+ <Toc/>
32
+
33
+ ## Interactive Benchmark
34
+
35
+ <I18nBenchmark framework="tanstack" vertical/>
36
+
37
+ ## Results reference:
38
+
39
+ <iframe
40
+ src="https://intlayer.org/markdown?url=https%3A%2F%2Fraw.githubusercontent.com%2Fintlayer-org%2Fbenchmark-i18n%2Fmain%2Freport%2Fscripts%2Fsummarize-tanstack.md"
41
+ width="100%"
42
+ height="600px"
43
+ style="border:none;">
44
+ </iframe>
45
+
46
+ > https://intlayer.org/markdown?url=https%3A%2F%2Fraw.githubusercontent.com%2Fintlayer-org%2Fbenchmark-i18n%2Fmain%2Freport%2Fscripts%2Fsummarize-tanstack.md
47
+
48
+ See complete benchmark repository [here](https://github.com/intlayer-org/benchmark-i18n/tree/main).
49
+
50
+ ## Introduction
51
+
52
+ Internationalization solutions are among the heaviest dependencies in a React app. On TanStack Start, the main risk is shipping unnecessary content: translations for other pages and other locales in a single route’s bundle.
53
+
54
+ As your app grows, that problem can quickly blow up the JavaScript sent to the client and slow down navigation.
55
+
56
+ In practice, for the least optimized implementations, an internationalized page can end up several times heavier than the version without i18n.
57
+
58
+ The other impact is on developer experience: how you declare content, types, namespace organization, dynamic loading, and reactivity when the locale changes.
59
+
60
+ ## Test your app
61
+
62
+ To quickly spot i18n leakage issues, I set up a free scanner available [here](https://intlayer.org/i18n-seo-scanner).
63
+
64
+ <iframe src="https://intlayer.org/i18n-seo-scanner" width="100%" height="600px" style="border:none;"/>
65
+
66
+ ## The problem
67
+
68
+ Two levers are essential to limit the cost of a multilingual app:
69
+
70
+ - Split content by page / namespace so you do not load whole dictionaries when you do not need them
71
+ - Load the right locale dynamically, only when needed
72
+
73
+ Understanding the technical limitations of these approaches:
74
+
75
+ **Dynamic loading**
76
+
77
+ Without dynamic loading, most solutions keep messages in memory from the first render, which adds significant overhead for apps with many routes and locales.
78
+
79
+ With dynamic loading, you accept a trade-off: less initial JS, but sometimes an extra request when switching language.
80
+
81
+ **Content splitting**
82
+
83
+ Syntaxes built around `const t = useTranslation()` + `t('a.b.c')` are very convenient but often encourage keeping large JSON objects at runtime. That model makes tree-shaking hard unless the library offers a real per-page split strategy.
84
+
85
+ ## Methodology
86
+
87
+ For this benchmark, we compared the following libraries:
88
+
89
+ - `Base App` (No i18n library)
90
+ - `react-intlayer` (v8.7.5-canary.0)
91
+ - `react-i18next` (v17.0.2)
92
+ - `use-intl` (v4.9.1)
93
+ - `@lingui/core` (v5.3.0)
94
+ - `@inlang/paraglide-js` (v2.15.1)
95
+ - `@tolgee/react` (v7.0.0)
96
+ - `react-intl` (v10.1.1)
97
+ - `wuchale` (v0.22.11)
98
+ - `gt-react` (vlatest)
99
+ - `lingo.dev` (v0.133.9)
100
+
101
+ The framework is `TanStack Start` with a multilingual app of **10 pages** and **10 languages**.
102
+
103
+ We compared **four loading strategies**:
104
+
105
+ | Strategy | No namespaces (global) | With namespaces (scoped) |
106
+ | :------------------ | :------------------------------------------- | :------------------------------------------------------------------- |
107
+ | **Static loading** | **Static**: Everything in memory at startup. | **Scoped static**: Split by namespace; everything loaded at startup. |
108
+ | **Dynamic loading** | **Dynamic**: On-demand loading per locale. | **Scoped dynamic**: Granular loading per namespace and locale. |
109
+
110
+ ## Strategy summary
111
+
112
+ - **Static**: Simple; no network latency after the initial load. Downside: large bundle size.
113
+ - **Dynamic**: Reduces initial weight (lazy-loading). Ideal when you have many locales.
114
+ - **Scoped static**: Keeps code organized (logical separation) without complex extra network requests.
115
+ - **Scoped dynamic**: Best approach for _code splitting_ and performance. Minimizes memory by loading only what the current view and active locale need.
116
+
117
+ ### What I measured:
118
+
119
+ I ran the same multilingual app in a real browser for every stack, then wrote down what actually showed up on the wire and how long things took. Sizes are reported **after normal web compression**, because that is closer to what people download than raw source counts.
120
+
121
+ - **Internationalization library size**: After bundling, tree-shaking and minification, the size of the i18n library is the size of the providers (e.g. `NextIntlClientProvider`) + hooks (e.g. `useTranslations`) code in an empty component. It does not include the loading of translation files. It answers how expensive the library is before your content enters the picture.
122
+
123
+ - **JavaScript per page**: For each benchmark route, how much script the browser pulls in for that visit, averaged across the pages in the suite (and across locales where the report rolls them up). Heavy pages are slow pages.
124
+
125
+ - **Leakage from other locales**: It's the content of the same page but in another language that would be loaded by mistake in the audited page. This content is unnecessary and should be avoided. (e.g. `/fr/about` page content in `/en/about` page bundle)
126
+
127
+ - **Leakage from other routes**: The same idea for **other screens** in the app: whether their copy is riding along when you only opened one page. (e.g. `/en/about` page content in `/en/contact` page bundle). A high score hints at weak splitting or over-broad bundles.
128
+
129
+ - **Average component bundle size**: Common UI pieces are measured **one at a time** instead of hiding inside one giant app number. It shows whether internationalization quietly inflates everyday components. For instance, if your component rerenders, it will load all that data from memory. Attaching a giant JSON to any component is like connecting a big store of unused data that will slow down your components’ performance.
130
+
131
+ - **Language switch responsiveness**: I flip the language using the app’s own control and time how long it takes until the page has clearly switched, what a visitor would notice, not a lab micro-step.
132
+
133
+ - **Rendering work after a language change**: A narrower follow-up: how much effort the interface took to repaint for the new language once the switch is in flight. Useful when the “felt” time and the framework cost diverge.
134
+
135
+ - **Initial page load time**: From navigation to the browser considering the page fully loaded for the scenarios I tested. Good for comparing cold starts.
136
+
137
+ - **Hydration time**: When the app exposes it, how long the client spends turning server HTML into something you can actually click. A dash in the tables means that implementation did not provide a reliable hydration figure in this benchmark.
138
+
139
+ ## Results in detail
140
+
141
+ ### 1 - Solutions to avoid
142
+
143
+ Some solutions, such as `gt-react` or `lingo.dev`, are clearly ones to steer clear of. They combine vendor lock-in with polluting your codebase. Worse: despite many hours trying to implement them, I never got them working properly on TanStack Start (similar to Next.js with `gt-next`).
144
+
145
+ Issues encountered:
146
+
147
+ **(General Translation)** (`gt-react@latest`):
148
+
149
+ - For an app around 110kb, `gt-react` can add more than 440kb extra (order of magnitude seen on the Next.js implementation in the same benchmark).
150
+ - `Quota Exceeded, please upgrade your plan` on the very first build with General Translation.
151
+ - 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.
152
+ - 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.
153
+ - These libraries use an anti-pattern through the `initializeGT()` function, blocking the bundle from tree-shaking cleanly.
154
+
155
+ **(Lingo.dev)** (`lingo.dev@0.133.9`):
156
+
157
+ - AI quota exceeded (or blocking server dependency), making build / production risky without paying.
158
+ - The compiler was missing almost 40% of the translated content. I had to rewrite all `.map` into flat component blocks to make it work.
159
+ - Their CLI is buggy and used to reset the config file for no reason.
160
+ - 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.
161
+ - 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.
162
+
163
+ ### 2 - Experimental solutions
164
+
165
+ **(Wuchale)** (`wuchale@0.22.11`):
166
+
167
+ The idea behind `Wuchale` is interesting but not yet a viable solution. I hit reactivity issues with the library and had to force rerendering of the provider to get the app working on TanStack Start. The documentation is also fairly unclear, which makes onboarding harder.
168
+
169
+ ### 3 - Acceptable solutions
170
+
171
+ **(Paraglide)** (`@inlang/paraglide-js@2.15.1`):
172
+
173
+ `Paraglide` offers an innovative, well-thought-out approach. Even so, in this benchmark the tree-shaking their company advertises did not work for my Next.js implementation or for TanStack Start. The workflow and DX are also more complex than other options.
174
+ Personally I dislike having to regenerate JS files before every push, which creates constant merge conflict risk via PRs. The tool also seems more focused on Vite than on Next.js.
175
+ Finally, in comparison with other solutions, Paraglide does not use a store (e.g. React context) to retrieve the current locale to render the content. For each node parsed, it will request the locale from the localStorage / cookie etc. It leads to execution of unnecessary logic that impacts the component reactivity.
176
+
177
+ **(Tolgee)** (`@tolgee/react@7.0.0`):
178
+
179
+ `Tolgee` addresses many of the issues mentioned earlier. I found it harder to get started with than other tools with similar approaches. It does not provide type safety, which also makes catching missing keys at compile time much harder. I had to wrap Tolgee’s APIs with my own to add missing-key detection.
180
+
181
+ On TanStack Start I also had reactivity problems: on locale change I had to force the provider to rerender and subscribe to locale-change events so loading in another language behaved correctly.
182
+
183
+ **(use-intl)** (`use-intl@4.9.1`):
184
+
185
+ `use-intl` is the most fashionable “intl” piece in the React ecosystem (same family as `next-intl`) and is often pushed by AI agents, but in my view wrongly so in a performance-first setting. Getting started is fairly simple. In practice, the process to optimize and limit leakage is quite complex. Likewise, combining dynamic loading + namespacing + TypeScript types slows development a lot.
186
+
187
+ On TanStack Start you avoid Next.js-specific traps (`setRequestLocale`, static rendering), but the core issue is the same: without strict discipline, the bundle quickly carries too many messages and per-route namespace maintenance becomes painful.
188
+
189
+ **(react-i18next)** (`react-i18next@17.0.2`):
190
+
191
+ `react-i18next` is probably the most popular option because it was among the first to serve JavaScript app i18n needs. It also has a wide set of community plugins for specific problems.
192
+
193
+ Still, it shares the same major downsides as stacks built on `t('a.b.c')`: optimizations are possible but very time-consuming, and large projects risk bad practices (namespaces + dynamic loading + types).
194
+
195
+ Message formats also diverge: `use-intl` uses ICU MessageFormat, while `i18next` uses its own format, which complicates tooling or migrations if you mix them.
196
+
197
+ **(Lingui)** (`@lingui/core@5.3.0`):
198
+
199
+ `Lingui` is often praised. Personally I found the workflow around `lingui extract` / `lingui compile` more complex than other approaches, without a clear upside in this TanStack Start benchmark. I also noticed inconsistent syntaxes that confuse AIs (e.g. `t()`, `t''`, `i18n.t()`, `<Trans>`).
200
+
201
+ **(react-intl)** (`react-intl@10.1.1`):
202
+
203
+ `react-intl` is a performant implementation from the Format.js team. The DX stays verbose: `const intl = useIntl()` + `intl.formatMessage({ id: "xx.xx" })` adds complexity, extra JavaScript work, and ties the global i18n instance to many nodes in the React tree.
204
+
205
+ ### 4 - Recommendations
206
+
207
+ This TanStack Start benchmark has no direct equivalent to `next-translate` (Next.js plugin + `getStaticProps`). For teams that really want a `t()` API with a mature ecosystem, `react-i18next` and `use-intl` remain “reasonable” choices, but expect to invest a lot of time optimizing to avoid leakage.
208
+
209
+ **(Intlayer)** (`react-intlayer@8.7.5-canary.0`):
210
+
211
+ I will not personally judge `react-intlayer` for objectivity’s sake, since it is my own solution.
212
+
213
+ ### Personal note
214
+
215
+ This note is personal and does not affect the benchmark results. Still, in the i18n world you often see consensus around a pattern like `const t = useTranslation('xx')` + `<>{t('xx.xx')}</>` for translated content.
216
+
217
+ In React apps, injecting a function as a `ReactNode` is, in my view, an anti-pattern. It also adds avoidable complexity and JavaScript execution overhead (even if it is barely noticeable).
@@ -0,0 +1,29 @@
1
+ ---
2
+ createdAt: 2026-04-20
3
+ updatedAt: 2026-04-20
4
+ title: Benchmark i18n libraries
5
+ description: Learn how Intlayer compares to other i18n libraries in terms of performance and bundle size.
6
+ keywords:
7
+ - benchmark
8
+ - i18n
9
+ - intl
10
+ - nextjs
11
+ - tanstack
12
+ - intlayer
13
+ slugs:
14
+ - doc
15
+ - benchmark
16
+ history:
17
+ - version: 8.7.5
18
+ date: 2026-01-06
19
+ changes: "Init benchmark"
20
+ ---
21
+
22
+ # Benchmark - Report
23
+
24
+ Benchmark Bloom is a performance benchmarking suite that measures the real-world impact of i18n (internationalisation) libraries across multiple React frameworks and loading strategies.
25
+
26
+ Find the detailed reports and technical documentation for each framework below:
27
+
28
+ - [**Next.js Benchmark Report**](https://github.com/aymericzip/intlayer/blob/main/docs/docs/en-GB/benchmark/nextjs.md)
29
+ - [**TanStack Start Benchmark Report**](https://github.com/aymericzip/intlayer/blob/main/docs/docs/en-GB/benchmark/tanstack.md)
@@ -0,0 +1,228 @@
1
+ ---
2
+ createdAt: 2026-04-20
3
+ updatedAt: 2026-04-21
4
+ title: Best i18n solution for Next.js in 2026 - Benchmark Report
5
+ description: Compare Next.js internationalisation (i18n) libraries like next-intl, next-i18next, and Intlayer. Detailed performance report on bundle size, leakage, and reactivity.
6
+ keywords:
7
+ - benchmark
8
+ - i18n
9
+ - intl
10
+ - nextjs
11
+ - performance
12
+ - intlayer
13
+ slugs:
14
+ - doc
15
+ - benchmark
16
+ - nextjs
17
+ author: Aymeric PINEAU
18
+ applicationTemplate: https://github.com/intlayer-org/benchmark-i18n
19
+ history:
20
+ - version: 8.7.5
21
+ date: 2026-01-06
22
+ changes: "Init benchmark"
23
+ ---
24
+
25
+ # Next.js i18n Libraries — 2026 Benchmark Report
26
+
27
+ This page is a benchmark report for i18n solutions on Next.js.
28
+
29
+ ## Table of Contents
30
+
31
+ <Toc/>
32
+
33
+ ## Interactive Benchmark
34
+
35
+ <I18nBenchmark framework="nextjs" vertical/>
36
+
37
+ ## Results reference:
38
+
39
+ <iframe
40
+ src="https://intlayer.org/markdown?url=https%3A%2F%2Fraw.githubusercontent.com%2Fintlayer-org%2Fbenchmark-i18n%2Fmain%2Freport%2Fscripts%2Fsummarize-nextjs.md"
41
+ width="100%"
42
+ height="600px"
43
+ style="border:none;">
44
+ </iframe>
45
+
46
+ > https://intlayer.org/markdown?url=https%3A%2F%2Fraw.githubusercontent.com%2Fintlayer-org%2Fbenchmark-i18n%2Fmain%2Freport%2Fscripts%2Fsummarize-nextjs.md
47
+
48
+ See complete benchmark repository [here](https://github.com/intlayer-org/benchmark-i18n).
49
+
50
+ ## Introduction
51
+
52
+ Internationalisation libraries have a heavy impact on your application. The main risk is loading content for every page and every language when the user only visits one page.
53
+
54
+ As your app grows, bundle size can grow exponentially, which can noticeably hurt performance.
55
+
56
+ As an example, for the worst offenders, once internationalised your page can end up nearly 4× larger.
57
+
58
+ Another impact of i18n libraries is slower development. Turning components into multilingual content across languages is time-consuming.
59
+
60
+ Because the problem is hard, many solutions exist—some focused on DX, others on performance or scalability, and so on.
61
+
62
+ Intlayer tries to optimise across these dimensions.
63
+
64
+ ## Test your app
65
+
66
+ To surface these issues, I built a free scanner you can try [here](https://intlayer.org/i18n-seo-scanner).
67
+
68
+ <iframe src="https://intlayer.org/i18n-seo-scanner" width="100%" height="600px" style="border:none;"/>
69
+
70
+ ## The problem
71
+
72
+ There are two major ways to limit the impact of a multilingual app on your bundle:
73
+
74
+ - Split your JSON (or content) across files / variables / namespaces so the bundler can tree-shake unused content for a given page
75
+ - Dynamically load your page content only in the page’s language
76
+
77
+ Technical limitations for these approaches:
78
+
79
+ **Dynamic loading**
80
+
81
+ Even when you declare routes like `[locale]/page.tsx`, with Webpack or Turbopack, and even if `generateStaticParams` is defined, the bundler does not treat `locale` as a static constant. That means it may pull content for all languages into each page. The main way to limit this is to load content via a dynamic import (e.g. `import('./locales/${locale}.json')`).
82
+
83
+ What happens at build time is that Next.js emits one JS bundle per locale (e.g. `./locales_fr_12345.js`). After the site is sent to the client, when the page runs, the browser performs an extra HTTP request for the needed JS file (e.g. `./locales_fr_12345.js`).
84
+
85
+ > Another way to address the same problem is to use `fetch()` to load JSON dynamically. That is how `Tolgee` works when JSON lives under `/public`, or `next-translate`, which relies on `getStaticProps` to load content. The flow is the same: the browser makes an extra HTTP request to load the asset.
86
+
87
+ **Content splitting**
88
+
89
+ If you use syntax like `const t = useTranslation()` + `t('my-object.my-sub-object.my-key')`, the entire JSON usually has to be in the bundle so the library can parse it and resolve the key. Much of that content then ships even when it is unused on the page.
90
+
91
+ To mitigate this, some libraries ask you to declare per page which namespaces to load, e.g. `next-i18next`, `next-intl`, `lingui`, `next-translate`, `next-international`.
92
+
93
+ By contrast, `Paraglide` adds an extra step before build to turn JSON into flat symbols like `const en_my_var = () => 'my value'`. In theory that enables tree-shaking unused content on the page. As we will see, that method still has trade-offs.
94
+
95
+ Finally, `Intlayer` applies a build-time optimisation so `useIntlayer('my-key')` is replaced with the corresponding content directly.
96
+
97
+ ## Methodology
98
+
99
+ For this benchmark, we compared the following libraries:
100
+
101
+ - `Base App` (No i18n library)
102
+ - `next-intlayer` (v8.7.5)
103
+ - `next-i18next` (v16.0.5)
104
+ - `next-intl` (v4.9.1)
105
+ - `@lingui/core` (v5.3.0)
106
+ - `next-translate` (v3.1.2)
107
+ - `next-international` (v1.3.1)
108
+ - `@inlang/paraglide-js` (v2.15.1)
109
+ - `tolgee` (v7.0.0)
110
+ - `@lingo.dev/compiler` (v0.4.0)
111
+ - `wuchale` (v0.22.11)
112
+ - `gt-next` (v6.16.5)
113
+
114
+ I used `Next.js` version `16.2.4` with the App Router.
115
+
116
+ I built a multilingual app with **10 pages** and **10 languages**.
117
+
118
+ I compared **four loading strategies**:
119
+
120
+ | Strategy | No namespaces (global) | With namespaces (scoped) |
121
+ | :------------------ | :------------------------------------------- | :------------------------------------------------------------------- |
122
+ | **Static loading** | **Static**: Everything in memory at startup. | **Scoped static**: Split by namespace; everything loaded at startup. |
123
+ | **Dynamic loading** | **Dynamic**: On-demand loading per locale. | **Scoped dynamic**: Granular loading per namespace and locale. |
124
+
125
+ ## Strategy summary
126
+
127
+ - **Static**: Simple; no network latency after the initial load. Downside: large bundle size.
128
+ - **Dynamic**: Reduces initial weight (lazy-loading). Ideal when you have many locales.
129
+ - **Scoped static**: Keeps code organised (logical separation) without complex extra network requests.
130
+ - **Scoped dynamic**: Best approach for _code splitting_ and performance. Minimises memory by loading only what the current view and active locale need.
131
+
132
+ ### What I measured:
133
+
134
+ I ran the same multilingual app in a real browser for every stack, then wrote down what actually showed up on the wire and how long things took. Sizes are reported **after normal web compression**, because that is closer to what people download than raw source counts.
135
+
136
+ - **Internationalisation library size**: After bundling, tree-shaking and minification, the size of the i18n library is the size of the providers (e.g. `NextIntlClientProvider`) + hooks (e.g. `useTranslations`) code in an empty component. It does not includes the loading of translation files. It answers how expensive the library is before your content enters the picture.
137
+
138
+ - **JavaScript per page**: For each benchmark route, how much script the browser pulls in for that visit, averaged across the pages in the suite (and across locales where the report rolls them up). Heavy pages are slow pages.
139
+
140
+ - **Leakage from other locales**: It's the content of the same page but in another language that would be loaded by mistake in the audited page. This content is unnecessary and should be avoided. (e.g. `/fr/about` page content in `/en/about` page bundle)
141
+
142
+ - **Leakage from other routes**: The same idea for **other screens** in the app: whether their copy is riding along when you only opened one page. (e.g. `/en/about` page content in `/en/contact` page bundle). A high score hints at weak splitting or over-broad bundles.
143
+
144
+ - **Average component bundle size**: Common UI pieces are measured **one at a time** instead of hiding inside one giant app number. It shows whether internationalisation quietly inflates everyday components. For instance, if your component rerender, it will load all that data from memory. Attaching a giant JSON to any component, is like connecting a big store of unused data that will slow down your components performance.
145
+
146
+ - **Language switch responsiveness**: I flip the language using the app’s own control and time how long it takes until the page has clearly switched, what a visitor would notice, not a lab micro-step.
147
+
148
+ - **Rendering work after a language change**: A narrower follow-up: how much effort the interface took to repaint for the new language once the switch is in flight. Useful when the “felt” time and the framework cost diverge.
149
+
150
+ - **Initial page load time**: From navigation to the browser considering the page fully loaded for the scenarios I tested. Good for comparing cold starts.
151
+
152
+ - **Hydration time**: When the app exposes it, how long the client spends turning server HTML into something you can actually click. A dash in the tables means that implementation did not provide a reliable hydration figure in this benchmark.
153
+
154
+ ## Results in detail
155
+
156
+ ### 1 — Solutions to avoid
157
+
158
+ Some solutions, such as `gt-next` or `lingo.dev`, are clearly best avoided. They combine vendor lock-in with polluting your codebase. Despite many hours trying to implement them, I never got them working, neither on TanStack Start nor on Next.js.
159
+
160
+ Issues encountered:
161
+
162
+ **(General Translation)** (`gt-next@6.16.5`):
163
+
164
+ - For a 110kb app, `gt-react` adds more than 440kb extra.
165
+ - `Quota Exceeded, please upgrade your plan` on the very first build with General Translation.
166
+ - 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.
167
+ - 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.
168
+ - The library blocks static rendering of Next.js pages.
169
+
170
+ **(Lingo.dev)** (`@lingo.dev/compiler@0.4.0`):
171
+
172
+ - AI quota exceeded, blocking the build entirely, so you cannot ship to production without paying.
173
+ - The compiler was missing almost 40% of the translated content. I had to rewrite all `.map` into flat component blocks to make it work.
174
+ - Their CLI is buggy and used to reset the config file for no reason.
175
+ - At build, it totally erased the generated JSONs when new content was added. As a result, a handful of keys could wipe out more than 300 existing keys.
176
+
177
+ ### 2 — Experimental solutions
178
+
179
+ **(Wuchale)** (`wuchale@0.22.11`):
180
+
181
+ The idea behind `Wuchale` is interesting but not yet viable. I hit reactivity issues and had to force rerendering of the provider to get the app working. The documentation is also fairly unclear, which makes onboarding harder.
182
+
183
+ **(Paraglide)** (`@inlang/paraglide-js@2.15.1`):
184
+
185
+ `Paraglide` offers an innovative, well-thought-out approach. Even so, in this benchmark the advertised tree-shaking did not work for my Next.js or TanStack Start setups. The workflow and DX are more complex than other options.
186
+ Personally I dislike having to regenerate JS files before every push, which creates constant merge conflict risk via PRs. The tool also seems more focused on Vite than on Next.js.
187
+ Even if in theory the tree-shaking strategy works, it does includes all locales in the bundle anyway. Paraglide offers not way to lazy-load the content. That says your page size will grow up correlated to the number of locales you have.
188
+ Finally, in comparison of other solutions, Paraglide does not use store (e.g. React context) to retrieve the current locale to render the content. For each node parsed, it will request the locale from the localeStorage / cookie etc. It leads to execution of unnecessary logic that impact the component reactivity.
189
+
190
+ ### 3 — Acceptable solutions
191
+
192
+ **(Tolgee)** (`tolgee@7.0.0`):
193
+
194
+ `Tolgee` addresses many of the issues mentioned earlier. I found it harder to adopt than similar tools. It does not provide type safety, which also makes catching missing keys at compile time harder. I had to wrap Tolgee’s functions with my own to add missing-key detection.
195
+
196
+ **(Next Intl)** (`next-intl@4.9.1`):
197
+
198
+ `next-intl` is the trendiest option and the one AI agents push most, but in my view wrongly so. Getting started is easy. In practice, optimising to limit leakage is complex. Combining dynamic loading + namespacing + TypeScript types slows development a lot. The package is also fairly heavy (~13kb for `NextIntlClientProvider` + `useTranslations`, which is more than 2× `next-intlayer`). **next-intl** used to block static rendering of Next.js pages. It provides a helper named `setRequestLocale()`. That seems partially addressed for centralised files like `en.json` / `fr.json`, but static rendering still breaks when content is split into namespaces such as `en/shared.json` / `fr/shared.json` / `es/shared.json`.
199
+
200
+ **(Next I18next)** (`next-i18next@16.0.5`):
201
+
202
+ `next-i18next` is probably the most popular option because it was among the first i18n solutions for JavaScript apps. It has many community plugins. It shares the same major downsides as `next-intl`. The package is especially heavy (~18kb for `I18nProvider` + `useTranslation`, about 3× `next-intlayer`).
203
+
204
+ Message formats also differ: `next-intl` uses ICU MessageFormat, while `i18next` uses its own format.
205
+
206
+ **(Next International)** (`next-international@1.3.1`):
207
+
208
+ `next-international` also tackles the issues above but does not differ much from `next-intl` or `next-i18next`. It includes `scopedT()` for namespace-specific translations, but using it has essentially no impact on bundle size.
209
+
210
+ **(Lingui)** (`@lingui/core@5.3.0`):
211
+
212
+ `Lingui` is often praised. Personally I found the `lingui extract` / `lingui compile` workflow more complex than alternatives, without a clear upside. I also noticed inconsistent syntaxes that confuse AIs (e.g. `t()`, `t''`, `i18n.t()`, `<Trans>`).
213
+
214
+ ### 4 — Recommendations
215
+
216
+ **(Next Translate)** (`next-translate@3.1.2`):
217
+
218
+ `next-translate` is my main recommendation if you like a `t()`-style API. It is elegant via `next-translate-plugin`, loading namespaces through `getStaticProps` with a Webpack / Turbopack loader. It is also the lightest option here (~2.5kb). For namespacing, defining namespaces per page or route in config is well thought out and easier to maintain than main alternatives like **next-intl** or **next-i18next**. In version `3.1.2`, I noted that static rendering did not work; Next.js fell back to dynamic rendering.
219
+
220
+ **(Intlayer)** (`next-intlayer@8.7.5`):
221
+
222
+ I will not personally judge `next-intlayer` for objectivity’s sake, since it is my own solution.
223
+
224
+ ### Personal note
225
+
226
+ This note is personal and does not affect the benchmark results. In the i18n world you often see consensus around `const t = useTranslation('xx')` + `<>{t('xx.xx')}</>`.
227
+
228
+ In React apps, injecting a function as a `ReactNode` is, in my view, an anti-pattern. It also adds avoidable complexity and JavaScript execution overhead (even if barely noticeable).