@intlayer/docs 8.7.4 → 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.
- package/blog/de/next-i18next_vs_next-intl_vs_intlayer.md +0 -2
- package/blog/en-GB/next-i18next_vs_next-intl_vs_intlayer.md +0 -2
- package/blog/es/next-i18next_vs_next-intl_vs_intlayer.md +0 -2
- package/blog/fr/next-i18next_vs_next-intl_vs_intlayer.md +0 -2
- package/blog/id/list_i18n_technologies/frameworks/svelte.md +0 -2
- package/blog/it/next-i18next_vs_next-intl_vs_intlayer.md +0 -2
- package/blog/ja/next-i18next_vs_next-intl_vs_intlayer.md +0 -2
- package/blog/ko/next-i18next_vs_next-intl_vs_intlayer.md +0 -2
- package/blog/pl/list_i18n_technologies/frameworks/svelte.md +0 -2
- package/blog/pt/next-i18next_vs_next-intl_vs_intlayer.md +0 -2
- package/blog/ru/next-i18next_vs_next-intl_vs_intlayer.md +0 -2
- package/blog/vi/list_i18n_technologies/frameworks/svelte.md +0 -2
- package/blog/zh/next-i18next_vs_next-intl_vs_intlayer.md +0 -2
- package/dist/cjs/generated/docs.entry.cjs +60 -0
- package/dist/cjs/generated/docs.entry.cjs.map +1 -1
- package/dist/esm/generated/docs.entry.mjs +60 -0
- package/dist/esm/generated/docs.entry.mjs.map +1 -1
- package/dist/types/generated/docs.entry.d.ts +3 -0
- package/dist/types/generated/docs.entry.d.ts.map +1 -1
- package/docs/ar/benchmark/index.md +29 -0
- package/docs/ar/benchmark/nextjs.md +227 -0
- package/docs/ar/benchmark/tanstack.md +193 -0
- package/docs/ar/intlayer_with_tanstack.md +0 -2
- package/docs/de/benchmark/index.md +29 -0
- package/docs/de/benchmark/nextjs.md +227 -0
- package/docs/de/benchmark/tanstack.md +193 -0
- package/docs/en/benchmark/___NOTE.md +82 -0
- package/docs/en/benchmark/___nextjs.md +195 -0
- package/docs/en/benchmark/___tanstack.md +187 -0
- package/docs/en/benchmark/index.md +29 -0
- package/docs/en/benchmark/nextjs.md +228 -0
- package/docs/en/benchmark/tanstack.md +217 -0
- package/docs/en-GB/benchmark/index.md +29 -0
- package/docs/en-GB/benchmark/nextjs.md +228 -0
- package/docs/en-GB/benchmark/tanstack.md +193 -0
- package/docs/es/benchmark/index.md +29 -0
- package/docs/es/benchmark/nextjs.md +226 -0
- package/docs/es/benchmark/tanstack.md +193 -0
- package/docs/fr/benchmark/index.md +29 -0
- package/docs/fr/benchmark/nextjs.md +227 -0
- package/docs/fr/benchmark/tanstack.md +193 -0
- package/docs/hi/benchmark/index.md +29 -0
- package/docs/hi/benchmark/nextjs.md +227 -0
- package/docs/hi/benchmark/tanstack.md +193 -0
- package/docs/id/benchmark/index.md +29 -0
- package/docs/id/benchmark/nextjs.md +227 -0
- package/docs/id/benchmark/tanstack.md +193 -0
- package/docs/id/intlayer_with_react_native+expo.md +0 -2
- package/docs/it/benchmark/index.md +29 -0
- package/docs/it/benchmark/nextjs.md +227 -0
- package/docs/it/benchmark/tanstack.md +193 -0
- package/docs/ja/benchmark/index.md +29 -0
- package/docs/ja/benchmark/nextjs.md +227 -0
- package/docs/ja/benchmark/tanstack.md +193 -0
- package/docs/ko/benchmark/index.md +29 -0
- package/docs/ko/benchmark/nextjs.md +227 -0
- package/docs/ko/benchmark/tanstack.md +193 -0
- package/docs/ko/intlayer_with_tanstack.md +0 -2
- package/docs/pl/benchmark/index.md +29 -0
- package/docs/pl/benchmark/nextjs.md +227 -0
- package/docs/pl/benchmark/tanstack.md +193 -0
- package/docs/pt/benchmark/index.md +29 -0
- package/docs/pt/benchmark/nextjs.md +227 -0
- package/docs/pt/benchmark/tanstack.md +193 -0
- package/docs/ru/benchmark/index.md +29 -0
- package/docs/ru/benchmark/nextjs.md +227 -0
- package/docs/ru/benchmark/tanstack.md +193 -0
- package/docs/tr/benchmark/index.md +29 -0
- package/docs/tr/benchmark/nextjs.md +227 -0
- package/docs/tr/benchmark/tanstack.md +193 -0
- package/docs/uk/benchmark/index.md +29 -0
- package/docs/uk/benchmark/nextjs.md +227 -0
- package/docs/uk/benchmark/tanstack.md +193 -0
- package/docs/vi/benchmark/index.md +29 -0
- package/docs/vi/benchmark/nextjs.md +227 -0
- package/docs/vi/benchmark/tanstack.md +193 -0
- package/docs/zh/benchmark/index.md +29 -0
- package/docs/zh/benchmark/nextjs.md +227 -0
- package/docs/zh/benchmark/tanstack.md +193 -0
- package/package.json +6 -6
- package/src/generated/docs.entry.ts +60 -0
|
@@ -0,0 +1,193 @@
|
|
|
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 internationalisation 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
|
+
Internationalisation 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 optimised implementations, an internationalised 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 organisation, 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` (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 organised (logical separation) without complex extra network requests.
|
|
115
|
+
- **Scoped dynamic**: Best approach for _code splitting_ and performance. Minimises memory by loading only what the current view and active locale need.
|
|
116
|
+
|
|
117
|
+
## Results in detail
|
|
118
|
+
|
|
119
|
+
### 1 — Solutions to avoid
|
|
120
|
+
|
|
121
|
+
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`).
|
|
122
|
+
|
|
123
|
+
Issues encountered:
|
|
124
|
+
|
|
125
|
+
**(General Translation)** (`gt-react@latest`):
|
|
126
|
+
|
|
127
|
+
- 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).
|
|
128
|
+
- `Quota Exceeded, please upgrade your plan` on the very first build with General Translation.
|
|
129
|
+
- 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.
|
|
130
|
+
- 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.
|
|
131
|
+
- These libraries use an anti-pattern through the `initializeGT()` function, blocking the bundle from tree-shaking cleanly.
|
|
132
|
+
|
|
133
|
+
**(Lingo.dev)** (`lingo.dev@0.133.9`):
|
|
134
|
+
|
|
135
|
+
- AI quota exceeded (or blocking server dependency), making build / production risky without paying.
|
|
136
|
+
- The compiler was missing almost 40% of the translated content. I had to rewrite all `.map` into flat component blocks to make it work.
|
|
137
|
+
- Their CLI is buggy and used to reset the config file for no reason.
|
|
138
|
+
- 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.
|
|
139
|
+
- 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.
|
|
140
|
+
|
|
141
|
+
### 2 — Experimental solutions
|
|
142
|
+
|
|
143
|
+
**(Wuchale)** (`wuchale@0.22.11`):
|
|
144
|
+
|
|
145
|
+
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.
|
|
146
|
+
|
|
147
|
+
### 3 — Acceptable solutions
|
|
148
|
+
|
|
149
|
+
**(Paraglide)** (`@inlang/paraglide-js@2.15.1`):
|
|
150
|
+
|
|
151
|
+
`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. Personally I am not a fan of having to regenerate JS files before every push, which creates constant merge conflict risk for developers via PRs.
|
|
152
|
+
|
|
153
|
+
**(Tolgee)** (`tolgee@7.0.0`):
|
|
154
|
+
|
|
155
|
+
`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.
|
|
156
|
+
|
|
157
|
+
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.
|
|
158
|
+
|
|
159
|
+
**(use-intl)** (`use-intl@4.9.1`):
|
|
160
|
+
|
|
161
|
+
`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 optimise and limit leakage is quite complex. Likewise, combining dynamic loading + namespacing + TypeScript types slows development a lot.
|
|
162
|
+
|
|
163
|
+
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.
|
|
164
|
+
|
|
165
|
+
**(react-i18next)** (`react-i18next@17.0.2`):
|
|
166
|
+
|
|
167
|
+
`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.
|
|
168
|
+
|
|
169
|
+
Still, it shares the same major downsides as stacks built on `t('a.b.c')`: optimisations are possible but very time-consuming, and large projects risk bad practices (namespaces + dynamic loading + types).
|
|
170
|
+
|
|
171
|
+
Message formats also diverge: `use-intl` uses ICU MessageFormat, while `i18next` uses its own format—which complicates tooling or migrations if you mix them.
|
|
172
|
+
|
|
173
|
+
**(Lingui)** (`@lingui/core@5.3.0`):
|
|
174
|
+
|
|
175
|
+
`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>`).
|
|
176
|
+
|
|
177
|
+
**(react-intl)** (`react-intl@10.1.1`):
|
|
178
|
+
|
|
179
|
+
`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.
|
|
180
|
+
|
|
181
|
+
### 4 — Recommendations
|
|
182
|
+
|
|
183
|
+
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 optimising to avoid leakage.
|
|
184
|
+
|
|
185
|
+
**(Intlayer)** (`react-intlayer@8.7.5-canary.0`):
|
|
186
|
+
|
|
187
|
+
I will not personally judge `react-intlayer` for objectivity’s sake, since it is my own solution.
|
|
188
|
+
|
|
189
|
+
### Personal note
|
|
190
|
+
|
|
191
|
+
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.
|
|
192
|
+
|
|
193
|
+
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: Comparativa de librerías i18n
|
|
5
|
+
description: Descubre cómo se compara Intlayer con otras librerías i18n en términos de rendimiento y tamaño del bundle.
|
|
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: "Inicio del benchmark"
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
# Benchmark - Informe
|
|
23
|
+
|
|
24
|
+
Benchmark Bloom es una suite de benchmarks de rendimiento que mide el impacto real de las librerías i18n (internacionalización) en varios frameworks de React y estrategias de carga.
|
|
25
|
+
|
|
26
|
+
A continuación encontrarás los informes detallados y la documentación técnica de cada framework:
|
|
27
|
+
|
|
28
|
+
- [**Informe de benchmark de Next.js**](https://github.com/aymericzip/intlayer/blob/main/docs/docs/es/benchmark/nextjs.md)
|
|
29
|
+
- [**Informe de benchmark de TanStack Start**](https://github.com/aymericzip/intlayer/blob/main/docs/docs/es/benchmark/tanstack.md)
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
---
|
|
2
|
+
createdAt: 2026-04-20
|
|
3
|
+
updatedAt: 2026-04-21
|
|
4
|
+
title: La mejor solución i18n para Next.js en 2026 - Informe de Benchmark
|
|
5
|
+
description: Compara librerías de internacionalización (i18n) para Next.js como next-intl, next-i18next e Intlayer. Informe detallado de rendimiento sobre tamaño del bundle, fugas y reactividad.
|
|
6
|
+
keywords:
|
|
7
|
+
- benchmark
|
|
8
|
+
- i18n
|
|
9
|
+
- intl
|
|
10
|
+
- nextjs
|
|
11
|
+
- rendimiento
|
|
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: "Inicio del benchmark"
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
# Librerías i18n para Next.js — Informe de Benchmark 2026
|
|
26
|
+
|
|
27
|
+
Esta página es un informe comparativo de soluciones i18n en Next.js.
|
|
28
|
+
|
|
29
|
+
## Tabla de Contenidos
|
|
30
|
+
|
|
31
|
+
<Toc/>
|
|
32
|
+
|
|
33
|
+
## Benchmark Interactivo
|
|
34
|
+
|
|
35
|
+
<I18nBenchmark framework="nextjs" vertical/>
|
|
36
|
+
|
|
37
|
+
## Referencia de resultados:
|
|
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
|
+
Consulta el repositorio completo del benchmark [aquí](https://github.com/intlayer-org/benchmark-i18n).
|
|
49
|
+
|
|
50
|
+
## Introducción
|
|
51
|
+
|
|
52
|
+
Las librerías de internacionalización tienen un gran impacto en tu aplicación. El principal riesgo es cargar contenido para cada página y cada idioma cuando el usuario solo visita una página.
|
|
53
|
+
|
|
54
|
+
A medida que tu aplicación crece, el tamaño del bundle puede aumentar exponencialmente, lo que puede perjudicar notablemente el rendimiento.
|
|
55
|
+
|
|
56
|
+
Como ejemplo, en los peores casos, una vez internacionalizada, tu página puede terminar siendo casi 4 veces más grande.
|
|
57
|
+
|
|
58
|
+
Otro impacto de las librerías i18n es la ralentización del desarrollo. Transformar componentes en contenido bilingüe o multilingüe requiere mucho tiempo.
|
|
59
|
+
|
|
60
|
+
Debido a que el problema es difícil, existen muchas soluciones: algunas enfocadas en la DX (experiencia del desarrollador), otras en el rendimiento o la escalabilidad, y así sucesivamente.
|
|
61
|
+
|
|
62
|
+
Intlayer intenta optimizar en todas estas dimensiones.
|
|
63
|
+
|
|
64
|
+
## Pon a prueba tu aplicación
|
|
65
|
+
|
|
66
|
+
Para sacar a la luz estos problemas, he creado un escáner gratuito que puedes probar [aquí](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
|
+
## El problema
|
|
71
|
+
|
|
72
|
+
Existen dos formas principales de limitar el impacto de una aplicación multilingüe en tu bundle:
|
|
73
|
+
|
|
74
|
+
- Dividir tu JSON (o contenido) en archivos / variables / namespaces para que el bundler pueda aplicar tree-shaking al contenido no utilizado en una página determinada.
|
|
75
|
+
- Cargar dinámicamente el contenido de tu página solo en el idioma del usuario.
|
|
76
|
+
|
|
77
|
+
Limitaciones técnicas de estos enfoques:
|
|
78
|
+
|
|
79
|
+
**Carga dinámica**
|
|
80
|
+
|
|
81
|
+
Incluso cuando declaras rutas como `[locale]/page.tsx`, con Webpack o Turbopack, e incluso si se define `generateStaticParams`, el bundler no trata `locale` como una constante estática. Eso significa que puede incluir el contenido de todos los idiomas en cada página. La forma principal de limitar esto es cargar el contenido a través de un import dinámico (por ejemplo, `import('./locales/${locale}.json')`).
|
|
82
|
+
|
|
83
|
+
Lo que sucede en tiempo de compilación es que Next.js emite un bundle JS por idioma (por ejemplo, `./locales_fr_12345.js`). Una vez que el sitio se envía al cliente, cuando la página se ejecuta, el navegador realiza una petición HTTP adicional para el archivo JS necesario (por ejemplo, `./locales_fr_12345.js`).
|
|
84
|
+
|
|
85
|
+
> Otra forma de abordar el mismo problema es usar `fetch()` para cargar el JSON dinámicamente. Así es como funciona `Tolgee` cuando el JSON reside en `/public`, o `next-translate`, que se basa en `getStaticProps` para cargar el contenido. El flujo es el mismo: el navegador realiza una petición HTTP adicional para cargar el recurso.
|
|
86
|
+
|
|
87
|
+
**División de contenido (Content splitting)**
|
|
88
|
+
|
|
89
|
+
Si usas una sintaxis como `const t = useTranslation()` + `t('mi-objeto.mi-subobjeto.mi-clave')`, normalmente todo el JSON tiene que estar en el bundle para que la librería pueda analizarlo y resolver la clave. Gran parte de ese contenido se envía incluso cuando no se utiliza en la página.
|
|
90
|
+
|
|
91
|
+
Para mitigar esto, algunas librerías te piden declarar por página qué namespaces cargar (por ejemplo, `next-i18next`, `next-intl`, `lingui`, `next-translate`, `next-international`).
|
|
92
|
+
|
|
93
|
+
Por el contrario, `Paraglide` añade un paso adicional antes de la compilación para convertir el JSON en símbolos planos como `const en_my_var = () => 'mi valor'`. En teoría, esto permite el tree-shaking del contenido no utilizado en la página. Como veremos, este método todavía tiene sus compromisos.
|
|
94
|
+
|
|
95
|
+
Finalmente, `Intlayer` aplica una optimización en tiempo de compilación para que `useIntlayer('mi-clave')` se reemplace directamente con el contenido correspondiente.
|
|
96
|
+
|
|
97
|
+
## Metodología
|
|
98
|
+
|
|
99
|
+
Para este benchmark, comparamos las siguientes librerías:
|
|
100
|
+
|
|
101
|
+
- `Base App` (Sin librería i18n)
|
|
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
|
+
Utilicé la versión `16.2.4` de `Next.js` con el App Router.
|
|
115
|
+
|
|
116
|
+
Construí una aplicación multilingüe con **10 páginas** y **10 idiomas**.
|
|
117
|
+
|
|
118
|
+
Comparé **cuatro estrategias de carga**:
|
|
119
|
+
|
|
120
|
+
| Estrategia | Sin namespaces (global) | Con namespaces (segmentado) |
|
|
121
|
+
| :----------------- | :------------------------------------------ | :----------------------------------------------------------------- |
|
|
122
|
+
| **Carga estática** | **Static**: Todo en memoria al inicio. | **Scoped static**: Dividido por namespace; todo cargado al inicio. |
|
|
123
|
+
| **Carga dinámica** | **Dynamic**: Carga bajo demanda por idioma. | **Scoped dynamic**: Carga granular por namespace e idioma. |
|
|
124
|
+
|
|
125
|
+
## Resumen de estrategias
|
|
126
|
+
|
|
127
|
+
- **Static**: Simple; sin latencia de red tras la carga inicial. Desventaja: gran tamaño de bundle.
|
|
128
|
+
- **Dynamic**: Reduce el peso inicial (lazy-loading). Ideal cuando se tienen muchos idiomas.
|
|
129
|
+
- **Scoped static**: Mantiene el código organizado (separación lógica) sin peticiones de red adicionales complejas.
|
|
130
|
+
- **Scoped dynamic**: El mejor enfoque para el _code splitting_ y el rendimiento. Minimiza el uso de memoria cargando solo lo que la vista actual y el idioma activo necesitan.
|
|
131
|
+
|
|
132
|
+
### Qué he medido:
|
|
133
|
+
|
|
134
|
+
He ejecutado la misma aplicación multilingüe en un navegador real para cada stack y he anotado qué se enviaba realmente por la red y cuánto tiempo tardaba. Los tamaños se reportan **después de la compresión web normal**, ya que es lo más cercano a lo que los usuarios descargan realmente.
|
|
135
|
+
|
|
136
|
+
- **Tamaño de la librería de internacionalización**: Después del bundling, tree-shaking y minificación, el tamaño de la librería i18n es el tamaño de los proveedores (ej. `NextIntlClientProvider`) + el código de los hooks (ej. `useTranslations`) en un componente vacío. No incluye la carga de los archivos de traducción. Responde a cuánto "cuesta" la librería antes de que entre el contenido.
|
|
137
|
+
|
|
138
|
+
- **JavaScript por página**: Para cada ruta del benchmark, cuánto script descarga el navegador para esa visita, promediado entre las páginas de la suite (y entre idiomas donde el informe los agrupa). Las páginas pesadas son páginas lentas.
|
|
139
|
+
|
|
140
|
+
- **Fuga desde otros idiomas (Leakage from other locales)**: Es el contenido de la misma página pero en otro idioma que se carga por error en la página auditada. Este contenido es innecesario y debe evitarse (ej. el contenido de la página `/fr/about` en el paquete de la página `/en/about`).
|
|
141
|
+
|
|
142
|
+
- **Fuga desde otras rutas**: La misma idea para **otras pantallas** de la aplicación: si sus textos se incluyen cuando solo has abierto una página (ej. el contenido de la página `/en/about` en el paquete de la página `/en/contact`). Una puntuación alta sugiere una división débil o paquetes excesivamente amplios.
|
|
143
|
+
|
|
144
|
+
- **Tamaño medio de bundle por componente**: Las piezas comunes de la interfaz se miden **una por una** en lugar de esconderse dentro de una cifra gigante de la aplicación. Muestra si la internacionalización infla silenciosamente los componentes cotidianos. Por ejemplo, si tu componente se vuelve a renderizar, cargará todos esos datos desde la memoria. Adjuntar un JSON gigante a cualquier componente es como conectar un gran almacén de datos no utilizados que ralentizará el rendimiento de tus componentes.
|
|
145
|
+
|
|
146
|
+
- **Responsividad al cambiar de idioma**: Cambio el idioma usando el control propio de la aplicación y cronometro cuánto tiempo pasa hasta que la página ha cambiado claramente, lo que un visitante notaría, no un paso de laboratorio microscópico.
|
|
147
|
+
|
|
148
|
+
- **Trabajo de renderizado tras un cambio de idioma**: Un seguimiento más específico: cuánto esfuerzo le costó a la interfaz volver a dibujarse para el nuevo idioma una vez que el cambio está en marcha. Útil cuando el tiempo "percibido" y el coste del framework divergen.
|
|
149
|
+
|
|
150
|
+
- **Tiempo de carga inicial de la página**: Desde la navegación hasta que el navegador considera la página completamente cargada para los escenarios que probé. Útil para comparar arranques en frío (cold starts).
|
|
151
|
+
|
|
152
|
+
- **Tiempo de hidratación**: Cuando la aplicación lo expone, cuánto tiempo tarda el cliente en convertir el HTML del servidor en algo en lo que realmente se puede hacer clic. Un guion en las tablas significa que esa implementación no proporcionó una cifra de hidratación fiable en este benchmark.
|
|
153
|
+
|
|
154
|
+
## Resultados en detalle
|
|
155
|
+
|
|
156
|
+
### 1 — Soluciones a evitar
|
|
157
|
+
|
|
158
|
+
Algunas soluciones, como `gt-next` o `lingo.dev`, deben evitarse claramente. Combinan el bloqueo del proveedor (vendor lock-in) con la contaminación de tu base de código. A pesar de pasar muchas horas intentando implementarlas, nunca logré que funcionaran, ni en TanStack Start ni en Next.js.
|
|
159
|
+
|
|
160
|
+
Problemas encontrados:
|
|
161
|
+
|
|
162
|
+
**(General Translation)** (`gt-next@6.16.5`):
|
|
163
|
+
|
|
164
|
+
- Para una aplicación de 110kb, `gt-react` añade más de 440kb extra.
|
|
165
|
+
- `Quota Exceeded, please upgrade your plan` en la primerísima compilación con General Translation.
|
|
166
|
+
- Las traducciones no se renderizan; obtengo el error `Error: <T> used on the client-side outside of <GTProvider>`, lo que parece ser un bug de la librería.
|
|
167
|
+
- Mientras implementaba **gt-tanstack-start-react**, también encontré un [problema](https://github.com/generaltranslation/gt/issues/1210#event-24510646961) con la librería: `does not provide an export named 'printAST' - @formatjs/icu-messageformat-parser`, que hacía que la aplicación se rompiera. Tras informar de este problema, el mantenedor lo solucionó en 24 horas.
|
|
168
|
+
- La librería bloquea el renderizado estático de las páginas de Next.js.
|
|
169
|
+
|
|
170
|
+
**(Lingo.dev)** (`@lingo.dev/compiler@0.4.0`):
|
|
171
|
+
|
|
172
|
+
- Cuota de IA excedida, bloqueando la compilación por completo, por lo que no puedes desplegar en producción sin pagar.
|
|
173
|
+
- El compilador omitía casi el 40% del contenido traducido. Tuve que reescribir todos los `.map` en bloques de componentes planos para que funcionara.
|
|
174
|
+
- Su CLI tiene fallos y solía resetear el archivo de configuración sin motivo.
|
|
175
|
+
- Al compilar, borraba totalmente los JSON generados cuando se añadía nuevo contenido. Como resultado, un puñado de claves podía eliminar más de 300 claves existentes.
|
|
176
|
+
|
|
177
|
+
### 2 — Soluciones experimentales
|
|
178
|
+
|
|
179
|
+
**(Wuchale)** (`wuchale@0.22.11`):
|
|
180
|
+
|
|
181
|
+
La idea detrás de `Wuchale` es interesante pero aún no es viable. Experimenté problemas de reactividad y tuve que forzar el renderizado del proveedor para que la aplicación funcionara. La documentación también es bastante confusa, lo que dificulta la adopción.
|
|
182
|
+
|
|
183
|
+
**(Paraglide)** (`@inlang/paraglide-js@2.15.1`):
|
|
184
|
+
|
|
185
|
+
`Paraglide` ofrece un enfoque innovador y bien pensado. Aun así, en este benchmark, el tree-shaking que su empresa anuncia no funcionó para mis configuraciones de Next.js o TanStack Start. El flujo de trabajo y la DX son más complejos que otras opciones. Personalmente, no me gusta tener que regenerar archivos JS antes de cada push, lo que crea un riesgo constante de conflictos de fusión a través de las PR. La herramienta también parece más enfocada en Vite que en Next.js.
|
|
186
|
+
Finalmente, en comparación con otras soluciones, Paraglide no usa un almacén (ej. contexto de React) para recuperar el idioma actual y renderizar el contenido. Por cada nodo analizado, solicita el idioma al localStorage / cookie, etc. Esto provoca la ejecución de lógica innecesaria que afecta a la reactividad del componente.
|
|
187
|
+
|
|
188
|
+
### 3 — Soluciones aceptables
|
|
189
|
+
|
|
190
|
+
**(Tolgee)** (`tolgee@7.0.0`):
|
|
191
|
+
|
|
192
|
+
`Tolgee` aborda muchos de los problemas mencionados anteriormente. Me resultó más difícil de adoptar que herramientas similares. No proporciona seguridad de tipos (type safety), lo que también dificulta detectar claves faltantes en tiempo de compilación. Tuve que envolver las funciones de Tolgee con las mías propias para añadir la detección de claves faltantes.
|
|
193
|
+
|
|
194
|
+
**(Next Intl)** (`next-intl@4.9.1`):
|
|
195
|
+
|
|
196
|
+
`next-intl` es la opción más de moda y la que más recomiendan los agentes de IA, pero en mi opinión, equivocadamente. Empezar es fácil. En la práctica, optimizar para limitar la fuga es complejo. Combinar carga dinámica + nombres de espacios + tipos de TypeScript ralentiza mucho el desarrollo. El paquete también es bastante pesado (~13kb para `NextIntlClientProvider` + `useTranslations`, que es más del doble de `next-intlayer`). **next-intl** solía bloquear el renderizado estático de las páginas de Next.js. Proporciona un asistente llamado `setRequestLocale()`. Eso parece haberse abordado parcialmente para archivos centralizados como `en.json` / `fr.json`, pero el renderizado estático sigue fallando cuando el contenido se divide en namespaces como `en/shared.json` / `fr/shared.json` / `es/shared.json`.
|
|
197
|
+
|
|
198
|
+
**(Next I18next)** (`next-i18next@16.0.5`):
|
|
199
|
+
|
|
200
|
+
`next-i18next` es probablemente la opción más popular porque fue una de las primeras soluciones i18n para aplicaciones JavaScript. Tiene muchos plugins comunitarios. Comparte las mismas desventajas principales que `next-intl`. El paquete es especialmente pesado (~18kb para `I18nProvider` + `useTranslation`, casi el triple que `next-intlayer`).
|
|
201
|
+
|
|
202
|
+
Los formatos de los mensajes también difieren: `next-intl` usa ICU MessageFormat, mientras que `i18next` usa su propio formato.
|
|
203
|
+
|
|
204
|
+
**(Next International)** (`next-international@1.3.1`):
|
|
205
|
+
|
|
206
|
+
`next-international` también aborda los problemas anteriores, pero no difiere mucho de `next-intl` o `next-i18next`. Incluye `scopedT()` para traducciones específicas de un namespace, pero su uso apenas tiene impacto en el tamaño del bundle.
|
|
207
|
+
|
|
208
|
+
**(Lingui)** (`@lingui/core@5.3.0`):
|
|
209
|
+
|
|
210
|
+
A menudo se elogia a `Lingui`. Personalmente, encontré el flujo de trabajo `lingui extract` / `lingui compile` más complejo que otras alternativas, sin una ventaja clara. También noté sintaxis inconsistentes que confunden a las IAs (ej. `t()`, `t''`, `i18n.t()`, `<Trans>`).
|
|
211
|
+
|
|
212
|
+
### 4 — Recomendaciones
|
|
213
|
+
|
|
214
|
+
**(Next Translate)** (`next-translate@3.1.2`):
|
|
215
|
+
|
|
216
|
+
`next-translate` es mi recomendación principal si te gusta una API de estilo `t()`. Es elegante a través de `next-translate-plugin`, cargando namespaces mediante `getStaticProps` con un cargador de Webpack / Turbopack. También es la opción más ligera aquí (~2.5kb). Para la segmentación por espacios de nombres, definirnamespaces por página o ruta en la configuración está bien pensado y es más fácil de mantener que las principales alternativas como **next-intl** o **next-i18next**. En la versión `3.1.2`, noté que el renderizado estático no funcionaba; Next.js recurría al renderizado dinámico.
|
|
217
|
+
|
|
218
|
+
**(Intlayer)** (`next-intlayer@8.7.5`):
|
|
219
|
+
|
|
220
|
+
No seré yo quien juzgue personalmente a `next-intlayer` por objetividad, ya que es mi propia solución.
|
|
221
|
+
|
|
222
|
+
### Nota personal
|
|
223
|
+
|
|
224
|
+
Esta nota es personal y no afecta los resultados del benchmark. En el mundo del i18n, a menudo se ve un consenso en torno al patrón `const t = useTranslation('xx')` + `<>{t('xx.xx')}</>`.
|
|
225
|
+
|
|
226
|
+
En las aplicaciones React, inyectar una función como un `ReactNode` es, en mi opinión, un antipatrón. También añade una complejidad evitable y una sobrecarga de ejecución de JavaScript (aunque sea apenas perceptible).
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
---
|
|
2
|
+
createdAt: 2026-04-20
|
|
3
|
+
updatedAt: 2026-04-21
|
|
4
|
+
title: La mejor solución i18n para TanStack Start en 2026 - Informe de Benchmark
|
|
5
|
+
description: Compara librerías de internacionalización para TanStack Start como react-i18next, use-intl e Intlayer. Informe detallado de rendimiento sobre tamaño del bundle, fugas y reactividad.
|
|
6
|
+
keywords:
|
|
7
|
+
- benchmark
|
|
8
|
+
- i18n
|
|
9
|
+
- intl
|
|
10
|
+
- tanstack
|
|
11
|
+
- rendimiento
|
|
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: "Inicio del benchmark"
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
# Librerías i18n para TanStack Start — Informe de Benchmark 2026
|
|
26
|
+
|
|
27
|
+
Esta página es un informe comparativo de soluciones i18n en TanStack Start.
|
|
28
|
+
|
|
29
|
+
## Tabla de Contenidos
|
|
30
|
+
|
|
31
|
+
<Toc/>
|
|
32
|
+
|
|
33
|
+
## Benchmark Interactivo
|
|
34
|
+
|
|
35
|
+
<I18nBenchmark framework="tanstack" vertical/>
|
|
36
|
+
|
|
37
|
+
## Referencia de resultados:
|
|
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
|
+
Consulta el repositorio completo del benchmark [aquí](https://github.com/intlayer-org/benchmark-i18n/tree/main).
|
|
49
|
+
|
|
50
|
+
## Introducción
|
|
51
|
+
|
|
52
|
+
Las soluciones de internacionalización se encuentran entre las dependencias más pesadas en una aplicación de React. En TanStack Start, el riesgo principal es enviar contenido innecesario: traducciones de otras páginas y de otros idiomas en el paquete de una sola ruta.
|
|
53
|
+
|
|
54
|
+
A medida que tu aplicación crece, ese problema puede disparar rápidamente la cantidad de JavaScript enviado al cliente y ralentizar la navegación.
|
|
55
|
+
|
|
56
|
+
En la práctica, en las implementaciones menos optimizadas, una página internacionalizada puede terminar siendo varias veces más pesada que la versión sin i18n.
|
|
57
|
+
|
|
58
|
+
El otro impacto es en la experiencia del desarrollador: cómo declaras el contenido, los tipos, la organización de los namespaces, la carga dinámica y la reactividad cuando cambia el idioma.
|
|
59
|
+
|
|
60
|
+
## Pon a prueba tu aplicación
|
|
61
|
+
|
|
62
|
+
Para detectar rápidamente problemas de fuga de i18n, he configurado un escáner gratuito disponible [aquí](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
|
+
## El problema
|
|
67
|
+
|
|
68
|
+
Dos palancas son esenciales para limitar el coste de una aplicación multilingüe:
|
|
69
|
+
|
|
70
|
+
- Dividir el contenido por página / namespace para no cargar diccionarios completos cuando no se necesitan.
|
|
71
|
+
- Cargar el idioma adecuado de forma dinámica, solo cuando sea necesario.
|
|
72
|
+
|
|
73
|
+
Entendiendo las limitaciones técnicas de estos enfoques:
|
|
74
|
+
|
|
75
|
+
**Carga dinámica**
|
|
76
|
+
|
|
77
|
+
Sin carga dinámica, la mayoría de las soluciones mantienen los mensajes en memoria desde el primer renderizado, lo que añade una sobrecarga significativa para aplicaciones con muchas rutas e idiomas.
|
|
78
|
+
|
|
79
|
+
Con la carga dinámica, aceptas un compromiso: menos JS inicial, pero a veces una petición extra al cambiar de idioma.
|
|
80
|
+
|
|
81
|
+
**División de contenido (Content splitting)**
|
|
82
|
+
|
|
83
|
+
Las sintaxis basadas en `const t = useTranslation()` + `t('a.b.c')` son muy cómodas pero a menudo fomentan el mantenimiento de grandes objetos JSON en tiempo de ejecución. Ese modelo dificulta el tree-shaking a menos que la librería ofrezca una estrategia real de división por página.
|
|
84
|
+
|
|
85
|
+
## Metodología
|
|
86
|
+
|
|
87
|
+
Para este benchmark, comparamos las siguientes librerías:
|
|
88
|
+
|
|
89
|
+
- `Base App` (Sin librería i18n)
|
|
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` (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
|
+
El framework es `TanStack Start` con una aplicación multilingüe de **10 páginas** y **10 idiomas**.
|
|
102
|
+
|
|
103
|
+
Comparamos **cuatro estrategias de carga**:
|
|
104
|
+
|
|
105
|
+
| Estrategia | Sin namespaces (global) | Con namespaces (segmentado) |
|
|
106
|
+
| :----------------- | :------------------------------------------ | :----------------------------------------------------------------- |
|
|
107
|
+
| **Carga estática** | **Static**: Todo en memoria al inicio. | **Scoped static**: Dividido por namespace; todo cargado al inicio. |
|
|
108
|
+
| **Carga dinámica** | **Dynamic**: Carga bajo demanda por idioma. | **Scoped dynamic**: Carga granular por namespace e idioma. |
|
|
109
|
+
|
|
110
|
+
## Resumen de estrategias
|
|
111
|
+
|
|
112
|
+
- **Static**: Simple; sin latencia de red tras la carga inicial. Desventaja: gran tamaño de bundle.
|
|
113
|
+
- **Dynamic**: Reduce el peso inicial (lazy-loading). Ideal cuando se tienen muchos idiomas.
|
|
114
|
+
- **Scoped static**: Mantiene el código organizado (separación lógica) sin peticiones de red adicionales complejas.
|
|
115
|
+
- **Scoped dynamic**: El mejor enfoque para el _code splitting_ y el rendimiento. Minimiza el uso de memoria cargando solo lo que la vista actual y el idioma activo necesitan.
|
|
116
|
+
|
|
117
|
+
## Resultados en detalle
|
|
118
|
+
|
|
119
|
+
### 1 — Soluciones a evitar
|
|
120
|
+
|
|
121
|
+
Algunas soluciones, como `gt-react` o `lingo.dev`, son claramente opciones de las que alejarse. Combinan el bloqueo del proveedor con la contaminación de tu base de código. Peor aún: a pesar de pasar muchas horas intentando implementarlas, nunca logré que funcionaran correctamente en TanStack Start (similar a Next.js con `gt-next`).
|
|
122
|
+
|
|
123
|
+
Problemas encontrados:
|
|
124
|
+
|
|
125
|
+
**(General Translation)** (`gt-react@latest`):
|
|
126
|
+
|
|
127
|
+
- Para una app de unos 110kb, `gt-react` puede añadir más de 440kb extra (orden de magnitud visto en la implementación de Next.js en este mismo benchmark).
|
|
128
|
+
- `Quota Exceeded, please upgrade your plan` en la primerísima compilación con General Translation.
|
|
129
|
+
- Las traducciones no se renderizan; obtengo el error `Error: <T> used on the client-side outside of <GTProvider>`, lo que parece ser un bug de la librería.
|
|
130
|
+
- Mientras implementaba **gt-tanstack-start-react**, también encontré un [problema](https://github.com/generaltranslation/gt/issues/1210#event-24510646961) con la librería: `does not provide an export named 'printAST' - @formatjs/icu-messageformat-parser`, que hacía que la aplicación se rompiera. Tras informar de este problema, el mantenedor lo solucionó en 24 horas.
|
|
131
|
+
- Estas librerías usan un antipatrón a través de la función `initializeGT()`, impidiendo que el bundle se limpie adecuadamente mediante tree-shaking.
|
|
132
|
+
|
|
133
|
+
**(Lingo.dev)** (`lingo.dev@0.133.9`):
|
|
134
|
+
|
|
135
|
+
- Cuota de IA excedida (o dependencia del servidor bloqueada), lo que hace que la compilación / producción sea arriesgada sin pagar.
|
|
136
|
+
- El compilador omitía casi el 40% del contenido traducido. Tuve que reescribir todos los `.map` en bloques de componentes planos para que funcionara.
|
|
137
|
+
- Su CLI tiene fallos y solía resetear el archivo de configuración sin motivo.
|
|
138
|
+
- Al compilar, borraba totalmente los JSON generados cuando se añadía nuevo contenido. Como resultado, podrías terminar con solo unas pocas claves eliminando cientos de claves existentes.
|
|
139
|
+
- Tuve problemas de reactividad con la librería en TanStack Start: al cambiar de idioma, tuve que forzar el renderizado del proveedor para que funcionara.
|
|
140
|
+
|
|
141
|
+
### 2 — Soluciones experimentales
|
|
142
|
+
|
|
143
|
+
**(Wuchale)** (`wuchale@0.22.11`):
|
|
144
|
+
|
|
145
|
+
La idea detrás de `Wuchale` es interesante pero todavía no es una solución viable. Experimenté problemas de reactividad con la librería y tuve que forzar el renderizado del proveedor para que la aplicación funcionara en TanStack Start. La documentación también es bastante confusa, lo que dificulta la adopción.
|
|
146
|
+
|
|
147
|
+
### 3 — Soluciones aceptables
|
|
148
|
+
|
|
149
|
+
**(Paraglide)** (`@inlang/paraglide-js@2.15.1`):
|
|
150
|
+
|
|
151
|
+
`Paraglide` ofrece un enfoque innovador y bien pensado. Aun así, en este benchmark, el tree-shaking que su empresa publicita no funcionó para mi implementación en Next.js ni para TanStack Start. El flujo de trabajo y la DX también son más complejos que otras opciones. Personalmente, no soy fan de tener que regenerar archivos JS antes de cada push, lo que crea un riesgo constante de conflictos de fusión para los desarrolladores a través de las PR.
|
|
152
|
+
|
|
153
|
+
**(Tolgee)** (`tolgee@7.0.0`):
|
|
154
|
+
|
|
155
|
+
`Tolgee` aborda muchos de los problemas mencionados anteriormente. Me resultó más difícil empezar con ella que con otras herramientas con enfoques similares. No proporciona seguridad de tipos, lo que también dificulta mucho detectar claves faltantes en tiempo de compilación. Tuve que envolver las API de Tolgee con las mías propias para añadir la detección de claves faltantes.
|
|
156
|
+
|
|
157
|
+
En TanStack Start también tuve problemas de reactividad: al cambiar de idioma, tuve que forzar el renderizado del proveedor y suscribirme a eventos de cambio de idioma para que la carga en otro idioma se comportara correctamente.
|
|
158
|
+
|
|
159
|
+
**(use-intl)** (`use-intl@4.9.1`):
|
|
160
|
+
|
|
161
|
+
`use-intl` es la pieza "intl" más de moda en el ecosistema de React (de la misma familia que `next-intl`) y a menudo la recomiendan los agentes de IA, pero en mi opinión, equivocadamente en una configuración donde el rendimiento es lo primero. Empezar es bastante sencillo. En la práctica, el proceso para optimizar y limitar la fuga es bastante complejo. Del mismo modo, combinar la carga dinámica + namespaces + tipos de TypeScript ralentiza mucho el desarrollo.
|
|
162
|
+
|
|
163
|
+
En TanStack Start evitas las trampas específicas de Next.js (`setRequestLocale`, renderizado estático), pero el problema principal es el mismo: sin una disciplina estricta, el bundle transporta rápidamente demasiados mensajes y el mantenimiento de los namespaces por ruta se vuelve pesado.
|
|
164
|
+
|
|
165
|
+
**(react-i18next)** (`react-i18next@17.0.2`):
|
|
166
|
+
|
|
167
|
+
`react-i18next` es probablemente la opción más popular porque fue una de las primeras en satisfacer las necesidades i18n de las aplicaciones JavaScript. También tiene un amplio conjunto de plugins comunitarios para problemas específicos.
|
|
168
|
+
|
|
169
|
+
Aun así, comparte las mismas desventajas principales que los stacks basados en `t('a.b.c')`: las optimizaciones son posibles pero consumen mucho tiempo, y los proyectos grandes corren el riesgo de caer en malas prácticas (namespaces + carga dinámica + tipos).
|
|
170
|
+
|
|
171
|
+
Los formatos de los mensajes también divergen: `use-intl` usa ICU MessageFormat, mientras que `i18next` usa su propio formato, lo que complica las herramientas o las migraciones si se mezclan.
|
|
172
|
+
|
|
173
|
+
**(Lingui)** (`@lingui/core@5.3.0`):
|
|
174
|
+
|
|
175
|
+
A menudo se elogia a `Lingui`. Personalmente, encontré el flujo de trabajo en torno a `lingui extract` / `lingui compile` más complejo que otros enfoques, sin una ventaja clara en este benchmark de TanStack Start. También noté sintaxis inconsistentes que confunden a las IAs (ej. `t()`, `t''`, `i18n.t()`, `<Trans>`).
|
|
176
|
+
|
|
177
|
+
**(react-intl)** (`react-intl@10.1.1`):
|
|
178
|
+
|
|
179
|
+
`react-intl` es una implementación de alto rendimiento del equipo de Format.js. La DX sigue siendo verbosa: `const intl = useIntl()` + `intl.formatMessage({ id: "xx.xx" })` añade complejidad, trabajo extra de JavaScript y vincula la instancia global de i18n a muchos nodos en el árbol de React.
|
|
180
|
+
|
|
181
|
+
### 4 — Recomendaciones
|
|
182
|
+
|
|
183
|
+
Este benchmark de TanStack Start no tiene un equivalente directo a `next-translate` (plugin de Next.js + `getStaticProps`). Para los equipos que realmente quieren una API `t()` con un ecosistema maduro, `react-i18next` y `use-intl` siguen siendo opciones "razonables", pero prepárate para invertir mucho tiempo optimizando para evitar fugas.
|
|
184
|
+
|
|
185
|
+
**(Intlayer)** (`react-intlayer@8.7.5-canary.0`):
|
|
186
|
+
|
|
187
|
+
No seré yo quien juzgue personalmente a `react-intlayer` por objetividad, ya que es mi propia solución.
|
|
188
|
+
|
|
189
|
+
### Nota personal
|
|
190
|
+
|
|
191
|
+
Esta nota es personal y no afecta los resultados del benchmark. Aun así, en el mundo del i18n, a menudo se ve un consenso en torno a un patrón como `const t = useTranslation('xx')` + `<>{t('xx.xx')}</>` para el contenido traducido.
|
|
192
|
+
|
|
193
|
+
En las aplicaciones React, inyectar una función como un `ReactNode` es, en mi opinión, un antipatrón. También añade una complejidad evitable y una sobrecarga de ejecución de JavaScript (aunque sea apenas perceptible).
|