@intlayer/docs 7.5.13 → 7.5.14
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/ar/per-component_vs_centralized_i18n.md +248 -0
- package/blog/de/per-component_vs_centralized_i18n.md +248 -0
- package/blog/en/_per-component_vs_centralized_i18n.md +252 -0
- package/blog/en/per-component_vs_centralized_i18n.md +248 -0
- package/blog/en-GB/per-component_vs_centralized_i18n.md +247 -0
- package/blog/es/per-component_vs_centralized_i18n.md +245 -0
- package/blog/fr/per-component_vs_centralized_i18n.md +245 -0
- package/blog/hi/per-component_vs_centralized_i18n.md +249 -0
- package/blog/id/per-component_vs_centralized_i18n.md +248 -0
- package/blog/it/per-component_vs_centralized_i18n.md +247 -0
- package/blog/ja/per-component_vs_centralized_i18n.md +247 -0
- package/blog/ko/per-component_vs_centralized_i18n.md +246 -0
- package/blog/pl/per-component_vs_centralized_i18n.md +247 -0
- package/blog/pt/per-component_vs_centralized_i18n.md +246 -0
- package/blog/ru/per-component_vs_centralized_i18n.md +251 -0
- package/blog/tr/per-component_vs_centralized_i18n.md +244 -0
- package/blog/uk/per-component_vs_centralized_i18n.md +248 -0
- package/blog/vi/per-component_vs_centralized_i18n.md +246 -0
- package/blog/zh/per-component_vs_centralized_i18n.md +248 -0
- package/dist/cjs/common.cjs.map +1 -1
- package/dist/cjs/generated/blog.entry.cjs +20 -0
- package/dist/cjs/generated/blog.entry.cjs.map +1 -1
- package/dist/cjs/generated/docs.entry.cjs.map +1 -1
- package/dist/cjs/generated/frequentQuestions.entry.cjs.map +1 -1
- package/dist/cjs/generated/legal.entry.cjs.map +1 -1
- package/dist/esm/common.mjs.map +1 -1
- package/dist/esm/generated/blog.entry.mjs +20 -0
- package/dist/esm/generated/blog.entry.mjs.map +1 -1
- package/dist/esm/generated/docs.entry.mjs.map +1 -1
- package/dist/esm/generated/frequentQuestions.entry.mjs.map +1 -1
- package/dist/esm/generated/legal.entry.mjs.map +1 -1
- package/dist/types/generated/blog.entry.d.ts +1 -0
- package/dist/types/generated/blog.entry.d.ts.map +1 -1
- package/package.json +9 -9
- package/src/generated/blog.entry.ts +20 -0
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
---
|
|
2
|
+
createdAt: 2025-09-10
|
|
3
|
+
updatedAt: 2025-09-10
|
|
4
|
+
title: Per-Component vs. Centralized i18n: A New Approach with Intlayer
|
|
5
|
+
description: A deep dive into internationalization strategies in React, comparing centralized, per-key, and per-component approaches, and introducing Intlayer.
|
|
6
|
+
keywords:
|
|
7
|
+
- i18n
|
|
8
|
+
- React
|
|
9
|
+
- Internationalization
|
|
10
|
+
- Intlayer
|
|
11
|
+
- Optimization
|
|
12
|
+
- Bundle Size
|
|
13
|
+
slugs:
|
|
14
|
+
- blog
|
|
15
|
+
- per-component-vs-centralized-i18n
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
# Per-Component vs. Centralized i18n
|
|
19
|
+
|
|
20
|
+
The per-component approach is not a new concept. For example, in the Vue ecosystem, `vue-i18n` supports [Single File Component (SFC) i18n](https://vue-i18n.intlify.dev/guide/advanced/sfc.html). Nuxt also offers [per-component translations](https://i18n.nuxtjs.org/docs/guide/per-component-translations), and Angular employs a similar pattern through its [Feature Modules](https://v17.angular.io/guide/feature-modules).
|
|
21
|
+
|
|
22
|
+
Even in a Flutter app, we can often find this pattern:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
lib/
|
|
26
|
+
└── features/
|
|
27
|
+
└── login/
|
|
28
|
+
├── login_screen.dart
|
|
29
|
+
└── login_screen.i18n.dart # <- Translations live here
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
```dart fileName='lib/features/login/login_screen.i18n.dart'
|
|
33
|
+
import 'package:i18n_extension/i18n_extension.dart';
|
|
34
|
+
|
|
35
|
+
extension Localization on String {
|
|
36
|
+
static var _t = Translations.byText("en") +
|
|
37
|
+
{
|
|
38
|
+
"Hello": {
|
|
39
|
+
"en": "Hello",
|
|
40
|
+
"fr": "Bonjour",
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
String get i18n => localize(this, _t);
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
However, in the React world, we mainly see different approaches, that I will group in three categories:
|
|
49
|
+
|
|
50
|
+
<Columns>
|
|
51
|
+
<Column>
|
|
52
|
+
|
|
53
|
+
**Centralized approach** (i18next, next-intl, react-intl, lingui)
|
|
54
|
+
|
|
55
|
+
- (with no namespaces) considers a single source to retrieve content. By default, you load the content from all pages when your app loads.
|
|
56
|
+
|
|
57
|
+
</Column>
|
|
58
|
+
<Column>
|
|
59
|
+
|
|
60
|
+
**Granular approach** (intlayer, inlang)
|
|
61
|
+
|
|
62
|
+
- fine-grain the content retrieval per key, or per-component.
|
|
63
|
+
|
|
64
|
+
</Column>
|
|
65
|
+
</Columns>
|
|
66
|
+
|
|
67
|
+
> In this blog, I won't focus on compiler-based solutions, which I already covered here: [Compiler vs Declarative i18n](https://github.com/aymericzip/intlayer/blob/main/docs/blog/en/compiler_vs_declarative_i18n.md).
|
|
68
|
+
> Note that compiler-based i18n (e.g., Lingui) simply automates the extraction and loading of content. Under the hood, they often share the same limitations as others approaches.
|
|
69
|
+
|
|
70
|
+
> Note that the more you fine-grain how you retrieve your content, the more you risk inserting additional state and logic into your components.
|
|
71
|
+
|
|
72
|
+
Granular approaches are more flexible than centralized ones, but it's often a tradeoff. Even if "tree shaking" is advertised by that libraries, in practice, you'll often end up loading a page in every language.
|
|
73
|
+
|
|
74
|
+
So, broadly speaking, the decision breaks down like this:
|
|
75
|
+
|
|
76
|
+
- If your application has more pages than languages, you should favor a granular approach.
|
|
77
|
+
- If you have more languages than pages, you should lean toward a centralized approach.
|
|
78
|
+
|
|
79
|
+
Of course, the library authors are aware of these limitations and provide workarounds.
|
|
80
|
+
Among them: splitting into namespaces, dynamically loading JSON files (`await import()`), or purging content at build time.
|
|
81
|
+
|
|
82
|
+
At the same time, you should know that when you dynamically load your content, you introduce additional requests to your server. Each extra `useState` or hook means an extra server request.
|
|
83
|
+
|
|
84
|
+
> To fix this point, using dynamic import mode, Intlayer suggests grouping multiple content definitions under a same key, Intlayer will then merge that content. In parallel, Intlayer provide a proxy to group multiple content retrieval under a same request.
|
|
85
|
+
|
|
86
|
+
But from all that solution, it's clear that the most popular approach is the centralized one.
|
|
87
|
+
|
|
88
|
+
### So why is the Centralized approach so popular?
|
|
89
|
+
|
|
90
|
+
- First, i18next was the first solution to become widely used, following a philosophy inspired by PHP and Java architectures (MVC), which rely on a strict separation of concerns (keeping content separate from code). It arrived in 2011, establishing its standards even before the massive shift toward Component-Based Architectures (like React).
|
|
91
|
+
- Then, once a library is widely adopted, it becomes difficult to shift the ecosystem to other patterns.
|
|
92
|
+
- Using a centralized approach also makes things easier in Translation Management Systems such as Crowdin, Phrase, or Localized.
|
|
93
|
+
- The logic behind a per-component approach is more complex than a centralized one and takes extra time to develop, especially when you have to solve problems like identifying where the content is located.
|
|
94
|
+
|
|
95
|
+
### Ok, but why not just stick to a Centralized approach?
|
|
96
|
+
|
|
97
|
+
Let me tell you why it can be problematic for your app:
|
|
98
|
+
|
|
99
|
+
- **Unused Data:**
|
|
100
|
+
When a page loads, you often load the content from all other pages. (In a 10-page app, that's 90% unused content loaded). You lazy load a modal? The i18n library doesn't care, it loads the strings first anyway.
|
|
101
|
+
- **Performance:**
|
|
102
|
+
For each re-render, every single one of your components is hydrated with a massive JSON payload, which impacts your app's reactivity as it grows.
|
|
103
|
+
- **Maintenance:**
|
|
104
|
+
Maintaining large JSON files is painful. You have to jump across files to insert a translation, ensuring no translations are missing and no **orphan keys** are left behind.
|
|
105
|
+
- **Design-system:**
|
|
106
|
+
It creates incompatibility with design systems (e.g., a `LoginForm` component) and constrains component duplication across different apps.
|
|
107
|
+
|
|
108
|
+
**"But we invented Namespaces!"**
|
|
109
|
+
|
|
110
|
+
Sure, and that's an massive move forward. Let's look at the comparison of the main bundle size of a Vite + React + React Router v7 + Intlayer setup. We simulated a 20-page application.
|
|
111
|
+
|
|
112
|
+
The first example does not include lazy-loaded translations per locale and no namespace splitting. The second includes content purging + dynamic loading for translations.
|
|
113
|
+
|
|
114
|
+
| Optimized bundle | Bundle not optimized |
|
|
115
|
+
| ------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- |
|
|
116
|
+
|  |  |
|
|
117
|
+
|
|
118
|
+
So thanks for namespaces, we moved from this structure:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
locale/
|
|
122
|
+
├── en.json
|
|
123
|
+
├── fr.json
|
|
124
|
+
└── es.json
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
To this one:
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
locale/
|
|
131
|
+
├── en/
|
|
132
|
+
│ ├── common.json
|
|
133
|
+
│ ├── navbar.json
|
|
134
|
+
│ ├── footer.json
|
|
135
|
+
│ ├── home.json
|
|
136
|
+
│ └── about.json
|
|
137
|
+
├── fr/
|
|
138
|
+
│ └── ...
|
|
139
|
+
└── es/
|
|
140
|
+
└── ...
|
|
141
|
+
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Now you have to finely manage what part of your app content should be loaded, and where. Conclusion, the vast majority of projects just skip this part due to the complexity (see [next-i18next guide](https://github.com/aymericzip/intlayer/blob/main/docs/blog/en/i18n_using_next-i18next.md) for instance to see the challenges that represents (just) following good practices).
|
|
145
|
+
Consequently, those projects end up with the massive JSON loading problem explained earlier.
|
|
146
|
+
|
|
147
|
+
> Note that this problem is not specific to i18next, but to all centralized approaches listed above.
|
|
148
|
+
|
|
149
|
+
However, I want to remember your that not all granular approaches solve this. For instance, `vue-i18n SFC`'s or `inlang` approaches do not inherently lazy load the translations per locale, so you simply trade the bundle size problem for another one.
|
|
150
|
+
|
|
151
|
+
Moreover, without proper separation of concerns, it becomes much more difficult to extract and provide your translations to translators for review.
|
|
152
|
+
|
|
153
|
+
### How Intlayer's per-component approach solves this
|
|
154
|
+
|
|
155
|
+
Intlayer proceeds in several steps:
|
|
156
|
+
|
|
157
|
+
1. **Declaration:** Declare your content anywhere in your codebase using `*.content.{ts|jsx|cjs|json|json5|...}` files. This ensures separation of concerns while keeping content colocated. A content file can be per-locale or multilingual.
|
|
158
|
+
2. **Processing:** Intlayer runs a build step to process JS logic, handle missing translation fallbacks, generate TypeScript types, manage duplicated content, and fetch content from your CMS, and more.
|
|
159
|
+
3. **Purging:** When your app builds, Intlayer purges unused content (a bit like how Tailwind manages your classes) by replacing the content as follows:
|
|
160
|
+
|
|
161
|
+
**Declaration:**
|
|
162
|
+
|
|
163
|
+
```tsx
|
|
164
|
+
// src/MyComponent.tsx
|
|
165
|
+
export const MyComponent = () => {
|
|
166
|
+
const content = useIntlayer("my-key");
|
|
167
|
+
return <h1>{content.title}</h1>;
|
|
168
|
+
};
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
```tsx
|
|
172
|
+
// src/myComponent.content.ts
|
|
173
|
+
export const {
|
|
174
|
+
key: "my-key",
|
|
175
|
+
content: t({
|
|
176
|
+
en: { title: "My title" },
|
|
177
|
+
fr: { title: "Mon titre" }
|
|
178
|
+
})
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
**Processing:** Intlayer builds the dictionary based on the `.content` file and generates:
|
|
184
|
+
|
|
185
|
+
```json5
|
|
186
|
+
// .intlayer/dynamic_dictionary/en/my-key.json
|
|
187
|
+
{
|
|
188
|
+
"key": "my-key",
|
|
189
|
+
"content": { "title": "My title" },
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
**Replacement:** Intlayer transforms your component during the application build.
|
|
194
|
+
|
|
195
|
+
**- Static Import Mode:**
|
|
196
|
+
|
|
197
|
+
```tsx
|
|
198
|
+
// Representation of the component in JSX-like syntax
|
|
199
|
+
export const MyComponent = () => {
|
|
200
|
+
const content = useDictionary({
|
|
201
|
+
key: "my-key",
|
|
202
|
+
content: {
|
|
203
|
+
nodeType: "translation",
|
|
204
|
+
translation: {
|
|
205
|
+
en: { title: "My title" },
|
|
206
|
+
fr: { title: "Mon titre" },
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
return <h1>{content.title}</h1>;
|
|
212
|
+
};
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
**- Dynamic Import Mode:**
|
|
216
|
+
|
|
217
|
+
```tsx
|
|
218
|
+
// Representation of the component in JSX-like syntax
|
|
219
|
+
export const MyComponent = () => {
|
|
220
|
+
const content = useDictionaryAsync({
|
|
221
|
+
en: () =>
|
|
222
|
+
import(".intlayer/dynamic_dictionary/en/my-key.json", {
|
|
223
|
+
with: { type: "json" },
|
|
224
|
+
}).then((mod) => mod.default),
|
|
225
|
+
// Same for other languages
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
return <h1>{content.title}</h1>;
|
|
229
|
+
};
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
> `useDictionaryAsync` uses a Suspense-like mechanism to load the localized JSON only when needed.
|
|
233
|
+
|
|
234
|
+
**Key benefits of this per-component approach:**
|
|
235
|
+
|
|
236
|
+
- Keeping your content declaration close to your components allows better maintainability (e.g moving a components to another app or design system. Deleting the component folder remove the related content too, as you probably already do for your `.test`, `.stories`)
|
|
237
|
+
|
|
238
|
+
- A per-component approach prevents AI agents from needing to jump across all your different files. It treats all translations in one place, limiting the complexity of the task, and the amount of tokens used.
|
|
239
|
+
|
|
240
|
+
### Limitations
|
|
241
|
+
|
|
242
|
+
Of course, this approach comes with trade-offs:
|
|
243
|
+
|
|
244
|
+
- It is harder to connect to other l10n systems and extra tooling.
|
|
245
|
+
- You get locked in (which is basically already the case with any i18n solution due to their specific syntax).
|
|
246
|
+
|
|
247
|
+
That's the reason why Intlayer tries to provide a complete toolset for i18n (100% free and OSS), including AI translation using your own AI Provider and API keys. Intlayer also provides tooling to synchronize your JSON, functioning like ICU / vue-i18n / i18next message formatters to map the content to their specific formats.
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
---
|
|
2
|
+
createdAt: 2025-09-10
|
|
3
|
+
updatedAt: 2025-09-10
|
|
4
|
+
title: i18n por componente vs. centralizado: Un nuevo enfoque con Intlayer
|
|
5
|
+
description: Un análisis profundo de las estrategias de internacionalización en React, comparando enfoques centralizados, por clave y por componente, e introduciendo Intlayer.
|
|
6
|
+
keywords:
|
|
7
|
+
- i18n
|
|
8
|
+
- React
|
|
9
|
+
- Internacionalización
|
|
10
|
+
- Intlayer
|
|
11
|
+
- Optimización
|
|
12
|
+
- Tamaño del bundle
|
|
13
|
+
slugs:
|
|
14
|
+
- blog
|
|
15
|
+
- per-component-vs-centralized-i18n
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
# i18n por componente vs. centralizado
|
|
19
|
+
|
|
20
|
+
El enfoque por componente no es un concepto nuevo. Por ejemplo, en el ecosistema Vue, `vue-i18n` admite [i18n SFC (Single File Component)](https://vue-i18n.intlify.dev/guide/advanced/sfc.html). Nuxt también ofrece [traducciones por componente](https://i18n.nuxtjs.org/docs/guide/per-component-translations), y Angular emplea un patrón similar a través de sus [Feature Modules](https://v17.angular.io/guide/feature-modules).
|
|
21
|
+
|
|
22
|
+
Incluso en una app Flutter, a menudo encontramos este patrón:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
lib/
|
|
26
|
+
└── features/
|
|
27
|
+
└── login/
|
|
28
|
+
├── login_screen.dart
|
|
29
|
+
└── login_screen.i18n.dart # <- Las traducciones residen aquí
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
```dart fileName='lib/features/login/login_screen.i18n.dart'
|
|
33
|
+
import 'package:i18n_extension/i18n_extension.dart';
|
|
34
|
+
|
|
35
|
+
extension Localization on String {
|
|
36
|
+
static var _t = Translations.byText("en") +
|
|
37
|
+
{
|
|
38
|
+
"Hello": {
|
|
39
|
+
"en": "Hello",
|
|
40
|
+
"fr": "Bonjour",
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
String get i18n => localize(this, _t);
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Sin embargo, en el mundo React, principalmente vemos diferentes enfoques, que agruparé en tres categorías:
|
|
49
|
+
|
|
50
|
+
<Columns>
|
|
51
|
+
<Column>
|
|
52
|
+
|
|
53
|
+
**Enfoque centralizado** (i18next, next-intl, react-intl, lingui)
|
|
54
|
+
|
|
55
|
+
- (sin namespaces) considera una única fuente para obtener el contenido. Por defecto, cargas el contenido de todas las páginas cuando tu app se inicia.
|
|
56
|
+
|
|
57
|
+
</Column>
|
|
58
|
+
<Column>
|
|
59
|
+
|
|
60
|
+
**Enfoque granular** (intlayer, inlang)
|
|
61
|
+
|
|
62
|
+
- afina la recuperación de contenido por clave o por componente.
|
|
63
|
+
|
|
64
|
+
</Column>
|
|
65
|
+
</Columns>
|
|
66
|
+
|
|
67
|
+
> En este blog, no me centraré en soluciones basadas en compiladores, que ya cubrí aquí: [Compilador vs i18n declarativa](https://github.com/aymericzip/intlayer/blob/main/docs/blog/es/compiler_vs_declarative_i18n.md).
|
|
68
|
+
> Ten en cuenta que la i18n basada en compiladores (por ejemplo, Lingui) simplemente automatiza la extracción y la carga de contenido. Bajo el capó, a menudo comparten las mismas limitaciones que otros enfoques.
|
|
69
|
+
|
|
70
|
+
> Ten en cuenta que cuanto más granularices la forma en que recuperas tu contenido, mayor es el riesgo de introducir estado y lógica adicional en tus componentes.
|
|
71
|
+
|
|
72
|
+
Los enfoques granulares son más flexibles que los centralizados, pero a menudo implican un compromiso. Incluso si esas bibliotecas publicitan "tree shaking", en la práctica frecuentemente terminarás cargando una página en cada idioma.
|
|
73
|
+
|
|
74
|
+
Entonces, en términos generales, la decisión se desglosa así:
|
|
75
|
+
|
|
76
|
+
- Si tu aplicación tiene más páginas que idiomas, deberías favorecer un enfoque granular.
|
|
77
|
+
- Si tienes más idiomas que páginas, deberías inclinarte por un enfoque centralizado.
|
|
78
|
+
|
|
79
|
+
Por supuesto, los autores de las librerías son conscientes de estas limitaciones y ofrecen soluciones alternativas.
|
|
80
|
+
Entre ellas: dividir en namespaces, cargar dinámicamente archivos JSON (`await import()`), o purgar el contenido en tiempo de compilación.
|
|
81
|
+
|
|
82
|
+
Al mismo tiempo, debes saber que cuando cargas tu contenido dinámicamente, introduces solicitudes adicionales a tu servidor. Cada `useState` adicional o hook significa una solicitud extra al servidor.
|
|
83
|
+
|
|
84
|
+
> Para solucionar este punto, Intlayer sugiere agrupar múltiples definiciones de contenido bajo la misma clave; Intlayer luego fusionará ese contenido.
|
|
85
|
+
|
|
86
|
+
Pero de entre todas esas soluciones, está claro que el enfoque más popular es el centralizado.
|
|
87
|
+
|
|
88
|
+
### ¿Por qué es tan popular el enfoque centralizado?
|
|
89
|
+
|
|
90
|
+
- Primero, i18next fue la primera solución en volverse ampliamente usada, siguiendo una filosofía inspirada en arquitecturas PHP y Java (MVC), que se basan en una estricta separación de responsabilidades (mantener el contenido separado del código). Llegó en 2011, estableciendo sus estándares incluso antes del gran cambio hacia arquitecturas basadas en componentes (como React).
|
|
91
|
+
- Luego, una vez que una librería se adopta ampliamente, se vuelve difícil mover el ecosistema a otros patrones.
|
|
92
|
+
- Usar un enfoque centralizado también facilita las cosas en Sistemas de Gestión de Traducciones como Crowdin, Phrase o Localized.
|
|
93
|
+
- La lógica detrás de un enfoque por componente es más compleja que la de uno centralizado y requiere más tiempo para desarrollarse, especialmente cuando hay que resolver problemas como identificar dónde está ubicado el contenido.
|
|
94
|
+
|
|
95
|
+
### Ok, ¿pero por qué no optar simplemente por un enfoque centralizado?
|
|
96
|
+
|
|
97
|
+
Déjame explicarte por qué puede ser problemático para tu app:
|
|
98
|
+
|
|
99
|
+
- **Datos no usados:** Cuando se carga una página, a menudo se carga el contenido de todas las demás páginas. (En una app de 10 páginas, eso supone un 90% de contenido cargado que no se usa). ¿Haces lazy load de un modal? A la biblioteca i18n no le importa: de todas formas carga las cadenas primero.
|
|
100
|
+
- **Rendimiento:** En cada re-renderizado, todos y cada uno de tus componentes se hidratan con una enorme carga JSON, lo que afecta la reactividad de tu app conforme crece.
|
|
101
|
+
- **Mantenimiento:** Mantener archivos JSON grandes es doloroso. Tienes que saltar entre archivos para insertar una traducción, asegurándote de que no falten traducciones y de que no queden **claves huérfanas**.
|
|
102
|
+
- **Sistema de diseño:**
|
|
103
|
+
Crea incompatibilidades con los design systems (por ejemplo, un componente `LoginForm`) y limita la duplicación de componentes entre distintas aplicaciones.
|
|
104
|
+
|
|
105
|
+
**"¡Pero inventamos los Namespaces!"**
|
|
106
|
+
|
|
107
|
+
Claro, y eso es un gran avance. Veamos la comparación del tamaño del bundle principal de una configuración Vite + React + React Router v7 + Intlayer. Simulamos una aplicación de 20 páginas.
|
|
108
|
+
|
|
109
|
+
El primer ejemplo no incluye traducciones lazy-loaded por locale ni separación de namespaces. El segundo incluye purgado de contenido + carga dinámica de traducciones.
|
|
110
|
+
|
|
111
|
+
| Bundle optimizado | Bundle no optimizado |
|
|
112
|
+
| ------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
|
|
113
|
+
|  |  |
|
|
114
|
+
|
|
115
|
+
Así que, gracias a los namespaces, pasamos de esta estructura:
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
locale/
|
|
119
|
+
├── en.json
|
|
120
|
+
├── fr.json
|
|
121
|
+
└── es.json
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
A esta:
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
locale/
|
|
128
|
+
├── en/
|
|
129
|
+
│ ├── common.json
|
|
130
|
+
│ ├── navbar.json
|
|
131
|
+
│ ├── footer.json
|
|
132
|
+
│ ├── home.json
|
|
133
|
+
│ └── about.json
|
|
134
|
+
├── fr/
|
|
135
|
+
│ └── ...
|
|
136
|
+
└── es/
|
|
137
|
+
└── ...
|
|
138
|
+
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Ahora tienes que gestionar con precisión qué parte del contenido de tu aplicación debe cargarse y dónde. En conclusión, la gran mayoría de proyectos simplemente omite esta parte debido a la complejidad (véase la [guía de next-i18next](https://github.com/aymericzip/intlayer/blob/main/docs/blog/es/i18n_using_next-i18next.md) por ejemplo para comprobar los retos que supone (solo) seguir las buenas prácticas).
|
|
142
|
+
En consecuencia, esos proyectos acaban con el problema de la carga masiva de JSON explicado anteriormente.
|
|
143
|
+
|
|
144
|
+
> Ten en cuenta que este problema no es específico de i18next, sino de todos los enfoques centralizados mencionados arriba.
|
|
145
|
+
|
|
146
|
+
Sin embargo, quiero recordarte que no todos los enfoques granulares solucionan esto. Por ejemplo, los enfoques `vue-i18n SFC` o `inlang` no realizan de forma inherente lazy load de las traducciones por locale, por lo que simplemente cambias el problema del tamaño del bundle por otro.
|
|
147
|
+
|
|
148
|
+
Además, sin una separación adecuada de responsabilidades, se vuelve mucho más difícil extraer y proporcionar tus traducciones a los traductores para su revisión.
|
|
149
|
+
|
|
150
|
+
### Cómo el enfoque por componente de Intlayer resuelve esto
|
|
151
|
+
|
|
152
|
+
Intlayer procede en varios pasos:
|
|
153
|
+
|
|
154
|
+
1. **Declaración:** Declara tu contenido en cualquier parte de tu codebase usando archivos `*.content.{ts|jsx|cjs|json|json5|...}`. Esto asegura la separación de responsabilidades mientras mantiene el contenido co-localizado. Un archivo de contenido puede ser por-locale o multilingüe.
|
|
155
|
+
2. **Procesamiento:** Intlayer ejecuta un paso de build para procesar la lógica JS, gestionar los fallbacks de traducción faltantes, generar tipos de TypeScript, manejar contenido duplicado, obtener contenido desde tu CMS, y más.
|
|
156
|
+
3. **Purgado:** Cuando tu app se builda, Intlayer purga el contenido no usado (un poco como Tailwind gestiona tus clases) reemplazando el contenido de la siguiente manera:
|
|
157
|
+
|
|
158
|
+
**Declaración:**
|
|
159
|
+
|
|
160
|
+
```tsx
|
|
161
|
+
// src/MyComponent.tsx
|
|
162
|
+
export const MyComponent = () => {
|
|
163
|
+
const content = useIntlayer("my-key");
|
|
164
|
+
return <h1>{content.title}</h1>;
|
|
165
|
+
};
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
```tsx
|
|
169
|
+
// src/myComponent.content.ts
|
|
170
|
+
export const {
|
|
171
|
+
key: "my-key",
|
|
172
|
+
content: t({
|
|
173
|
+
es: { title: "Mi título" },
|
|
174
|
+
en: { title: "My title" },
|
|
175
|
+
fr: { title: "Mon titre" }
|
|
176
|
+
})
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
**Procesamiento:** Intlayer construye el diccionario basado en el archivo `.content` y genera:
|
|
182
|
+
|
|
183
|
+
```json5
|
|
184
|
+
// .intlayer/dynamic_dictionary/en/my-key.json
|
|
185
|
+
{
|
|
186
|
+
"key": "my-key",
|
|
187
|
+
"content": { "title": "Mi título" },
|
|
188
|
+
}
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
**Reemplazo:** Intlayer transforma tu componente durante la compilación de la aplicación.
|
|
192
|
+
|
|
193
|
+
**- Modo de importación estática:**
|
|
194
|
+
|
|
195
|
+
```tsx
|
|
196
|
+
// Representación del componente en sintaxis similar a JSX
|
|
197
|
+
export const MyComponent = () => {
|
|
198
|
+
const content = useDictionary({
|
|
199
|
+
key: "my-key",
|
|
200
|
+
content: {
|
|
201
|
+
nodeType: "translation",
|
|
202
|
+
translation: {
|
|
203
|
+
en: { title: "Mi título" },
|
|
204
|
+
fr: { title: "Mon titre" },
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
return <h1>{content.title}</h1>;
|
|
210
|
+
};
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
**- Modo de importación dinámica:**
|
|
214
|
+
|
|
215
|
+
```tsx
|
|
216
|
+
// Representación del componente en sintaxis similar a JSX
|
|
217
|
+
export const MyComponent = () => {
|
|
218
|
+
const content = useDictionaryAsync({
|
|
219
|
+
en: () =>
|
|
220
|
+
import(".intlayer/dynamic_dictionary/en/my-key.json", {
|
|
221
|
+
with: { type: "json" },
|
|
222
|
+
}).then((mod) => mod.default),
|
|
223
|
+
// Same for other languages
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
return <h1>{content.title}</h1>;
|
|
227
|
+
};
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
> `useDictionaryAsync` utiliza un mecanismo similar a Suspense para cargar el JSON localizado únicamente cuando es necesario.
|
|
231
|
+
|
|
232
|
+
**Beneficios clave de este enfoque por componente:**
|
|
233
|
+
|
|
234
|
+
- Mantener la declaración de tu contenido cerca de tus componentes permite una mejor mantenibilidad (p. ej., mover un componente a otra app o design system. Eliminar la carpeta del componente también elimina el contenido relacionado, como probablemente ya haces con tus `.test`, `.stories`)
|
|
235
|
+
|
|
236
|
+
- Un enfoque por componente evita que los agentes de IA tengan que saltar entre todos tus distintos archivos. Trata todas las traducciones en un solo lugar, limitando la complejidad de la tarea y la cantidad de tokens utilizados.
|
|
237
|
+
|
|
238
|
+
### Limitaciones
|
|
239
|
+
|
|
240
|
+
Por supuesto, este enfoque conlleva compromisos:
|
|
241
|
+
|
|
242
|
+
- Es más difícil conectarlo con otros sistemas de l10n y herramientas adicionales.
|
|
243
|
+
- Quedas atado a la solución (lock-in), que básicamente ya ocurre con cualquier solución i18n debido a su sintaxis específica.
|
|
244
|
+
|
|
245
|
+
Esa es la razón por la que Intlayer intenta proporcionar un conjunto de herramientas completo para i18n (100% gratuito y OSS), incluyendo traducción por IA usando tu propio AI Provider y claves de API. Intlayer también ofrece herramientas para sincronizar tus JSON, funcionando como formateadores de mensajes de ICU / vue-i18n / i18next para mapear el contenido a sus formatos específicos.
|