@intlayer/docs 6.1.4 → 6.1.6-canary.0

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 (75) hide show
  1. package/blog/ar/next-i18next_vs_next-intl_vs_intlayer.md +1366 -75
  2. package/blog/ar/nextjs-multilingual-seo-comparison.md +364 -0
  3. package/blog/de/next-i18next_vs_next-intl_vs_intlayer.md +1288 -72
  4. package/blog/de/nextjs-multilingual-seo-comparison.md +362 -0
  5. package/blog/en/intlayer_with_next-i18next.mdx +431 -0
  6. package/blog/en/intlayer_with_next-intl.mdx +335 -0
  7. package/blog/en/next-i18next_vs_next-intl_vs_intlayer.md +583 -336
  8. package/blog/en/nextjs-multilingual-seo-comparison.md +360 -0
  9. package/blog/en-GB/next-i18next_vs_next-intl_vs_intlayer.md +1144 -37
  10. package/blog/en-GB/nextjs-multilingual-seo-comparison.md +360 -0
  11. package/blog/es/next-i18next_vs_next-intl_vs_intlayer.md +1236 -64
  12. package/blog/es/nextjs-multilingual-seo-comparison.md +363 -0
  13. package/blog/fr/next-i18next_vs_next-intl_vs_intlayer.md +1142 -75
  14. package/blog/fr/nextjs-multilingual-seo-comparison.md +362 -0
  15. package/blog/hi/nextjs-multilingual-seo-comparison.md +363 -0
  16. package/blog/it/next-i18next_vs_next-intl_vs_intlayer.md +1130 -55
  17. package/blog/it/nextjs-multilingual-seo-comparison.md +363 -0
  18. package/blog/ja/next-i18next_vs_next-intl_vs_intlayer.md +1150 -76
  19. package/blog/ja/nextjs-multilingual-seo-comparison.md +362 -0
  20. package/blog/ko/next-i18next_vs_next-intl_vs_intlayer.md +1139 -73
  21. package/blog/ko/nextjs-multilingual-seo-comparison.md +362 -0
  22. package/blog/pt/next-i18next_vs_next-intl_vs_intlayer.md +1143 -76
  23. package/blog/pt/nextjs-multilingual-seo-comparison.md +362 -0
  24. package/blog/ru/next-i18next_vs_next-intl_vs_intlayer.md +1150 -74
  25. package/blog/ru/nextjs-multilingual-seo-comparison.md +370 -0
  26. package/blog/tr/next-i18next_vs_next-intl_vs_intlayer.md +2 -0
  27. package/blog/tr/nextjs-multilingual-seo-comparison.md +362 -0
  28. package/blog/zh/next-i18next_vs_next-intl_vs_intlayer.md +1152 -75
  29. package/blog/zh/nextjs-multilingual-seo-comparison.md +394 -0
  30. package/dist/cjs/generated/blog.entry.cjs +16 -0
  31. package/dist/cjs/generated/blog.entry.cjs.map +1 -1
  32. package/dist/cjs/generated/docs.entry.cjs +16 -0
  33. package/dist/cjs/generated/docs.entry.cjs.map +1 -1
  34. package/dist/esm/generated/blog.entry.mjs +16 -0
  35. package/dist/esm/generated/blog.entry.mjs.map +1 -1
  36. package/dist/esm/generated/docs.entry.mjs +16 -0
  37. package/dist/esm/generated/docs.entry.mjs.map +1 -1
  38. package/dist/types/generated/blog.entry.d.ts +1 -0
  39. package/dist/types/generated/blog.entry.d.ts.map +1 -1
  40. package/dist/types/generated/docs.entry.d.ts +1 -0
  41. package/dist/types/generated/docs.entry.d.ts.map +1 -1
  42. package/docs/ar/component_i18n.md +186 -0
  43. package/docs/ar/vs_code_extension.md +48 -109
  44. package/docs/de/component_i18n.md +186 -0
  45. package/docs/de/vs_code_extension.md +46 -107
  46. package/docs/en/component_i18n.md +186 -0
  47. package/docs/en/interest_of_intlayer.md +2 -2
  48. package/docs/en/intlayer_with_nextjs_14.md +18 -1
  49. package/docs/en/intlayer_with_nextjs_15.md +18 -1
  50. package/docs/en/vs_code_extension.md +24 -114
  51. package/docs/en-GB/component_i18n.md +186 -0
  52. package/docs/en-GB/vs_code_extension.md +42 -103
  53. package/docs/es/component_i18n.md +182 -0
  54. package/docs/es/vs_code_extension.md +53 -114
  55. package/docs/fr/component_i18n.md +186 -0
  56. package/docs/fr/vs_code_extension.md +50 -111
  57. package/docs/hi/component_i18n.md +186 -0
  58. package/docs/hi/vs_code_extension.md +49 -110
  59. package/docs/it/component_i18n.md +186 -0
  60. package/docs/it/vs_code_extension.md +50 -111
  61. package/docs/ja/component_i18n.md +186 -0
  62. package/docs/ja/vs_code_extension.md +50 -111
  63. package/docs/ko/component_i18n.md +186 -0
  64. package/docs/ko/vs_code_extension.md +48 -109
  65. package/docs/pt/component_i18n.md +186 -0
  66. package/docs/pt/vs_code_extension.md +46 -107
  67. package/docs/ru/component_i18n.md +186 -0
  68. package/docs/ru/vs_code_extension.md +48 -109
  69. package/docs/tr/component_i18n.md +186 -0
  70. package/docs/tr/vs_code_extension.md +54 -115
  71. package/docs/zh/component_i18n.md +186 -0
  72. package/docs/zh/vs_code_extension.md +51 -105
  73. package/package.json +11 -11
  74. package/src/generated/blog.entry.ts +16 -0
  75. package/src/generated/docs.entry.ts +16 -0
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  createdAt: 2025-08-23
3
- updatedAt: 2025-09-25
3
+ updatedAt: 2025-09-29
4
4
  title: next-i18next vs next-intl vs Intlayer
5
5
  description: Compare next-i18next with next-intl and Intlayer for the internationalization (i18n) of a Next.js app
6
6
  keywords:
@@ -19,6 +19,8 @@ slugs:
19
19
 
20
20
  # next-i18next VS next-intl VS intlayer | Next.js Internationalization (i18n)
21
21
 
22
+ ![next-i18next VS next-intl VS intlayer](https://github.com/aymericzip/intlayer/blob/main/docs/assets/i18next-next-intl-intlayer.png?raw=true)
23
+
22
24
  Let’s take a look into the similarities and differences between three i18n options for Next.js: next-i18next, next-intl, and Intlayer.
23
25
 
24
26
  This is not a full tutorial. It’s a comparison to help you pick.
@@ -47,12 +49,12 @@ We focus on **Next.js 13+ App Router** (with **React Server Components**) and ev
47
49
 
48
50
  ---
49
51
 
50
- | Library | GitHub Stars | Total Commits | Last Commit | First Version | NPM Version | NPM Downloads |
51
- | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | ------------- | ---------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
52
- | `aymericzip/intlayer` | [![GitHub Repo stars](https://img.shields.io/github/stars/aymericzip/intlayer?style=flat&label=%E2%AD%90%20stars)](https://github.com/aymericzip/intlayer/stargazers) | [![GitHub commit activity](https://img.shields.io/github/commit-activity/t/aymericzip/intlayer?style=flat&label=commits)](https://github.com/aymericzip/intlayer/commits) | [![Last Commit](https://img.shields.io/github/last-commit/aymericzip/intlayer?style=flat)](https://github.com/aymericzip/intlayer/commits) | April 2024 | [![npm](https://img.shields.io/npm/v/intlayer?style=flat)](https://www.npmjs.com/package/intlayer) | [![npm downloads](https://img.shields.io/npm/dm/intlayer?style=flat)](https://www.npmjs.com/package/intlayer) |
53
- | `amannn/next-intl` | [![GitHub Repo stars](https://img.shields.io/github/stars/amannn/next-intl?style=flat&label=%E2%AD%90%20stars)](https://github.com/amannn/next-intl/stargazers) | [![GitHub commit activity](https://img.shields.io/github/commit-activity/t/amannn/next-intl?style=flat&label=commits)](https://github.com/amannn/next-intl/commits) | [![Last Commit](https://img.shields.io/github/last-commit/amannn/next-intl?style=flat)](https://github.com/amannn/next-intl/commits) | Nov 2020 | [![npm](https://img.shields.io/npm/v/next-intl?style=flat)](https://www.npmjs.com/package/next-intl) | [![npm downloads](https://img.shields.io/npm/dm/next-intl?style=flat)](https://www.npmjs.com/package/next-intl) |
54
- | `i18next/i18next` | [![GitHub Repo stars](https://img.shields.io/github/stars/i18next/i18next?style=flat&label=%E2%AD%90%20stars)](https://github.com/i18next/i18next/stargazers) | [![GitHub commit activity](https://img.shields.io/github/commit-activity/t/i18next/i18next?style=flat&label=commits)](https://github.com/i18next/i18next/commits) | [![Last Commit](https://img.shields.io/github/last-commit/i18next/i18next?style=flat)](https://github.com/i18next/i18next/commits) | Jan 2012 | [![npm](https://img.shields.io/npm/v/i18next?style=flat)](https://www.npmjs.com/package/i18next) | [![npm downloads](https://img.shields.io/npm/dm/i18next?style=flat)](https://www.npmjs.com/package/i18next) |
55
- | `i18next/next-i18next` | [![GitHub Repo stars](https://img.shields.io/github/stars/i18next/next-i18next?style=flat&label=%E2%AD%90%20stars)](https://github.com/i18next/next-i18next/stargazers) | [![GitHub commit activity](https://img.shields.io/github/commit-activity/t/i18next/next-i18next?style=flat&label=commits)](https://github.com/i18next/next-i18next/commits) | [![Last Commit](https://img.shields.io/github/last-commit/i18next/next-i18next?style=flat)](https://github.com/i18next/next-i18next/commits) | Nov 2018 | [![npm](https://img.shields.io/npm/v/next-i18next?style=flat)](https://www.npmjs.com/package/next-i18next) | [![npm downloads](https://img.shields.io/npm/dm/next-i18next?style=flat)](https://www.npmjs.com/package/next-i18next) |
52
+ | Library | GitHub Stars | Total Commits | Last Commit | First Version | NPM Version | NPM Downloads |
53
+ | ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- | ------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
54
+ | `aymericzip/intlayer` | [![GitHub Repo stars](https://img.shields.io/github/stars/aymericzip/intlayer?style=for-the-badge&label=%E2%AD%90%20stars)](https://github.com/aymericzip/intlayer/stargazers) | [![GitHub commit activity](https://img.shields.io/github/commit-activity/t/aymericzip/intlayer?style=for-the-badge&label=commits)](https://github.com/aymericzip/intlayer/commits) | [![Last Commit](https://img.shields.io/github/last-commit/aymericzip/intlayer?style=for-the-badge)](https://github.com/aymericzip/intlayer/commits) | April 2024 | [![npm](https://img.shields.io/npm/v/intlayer?style=for-the-badge)](https://www.npmjs.com/package/intlayer) | [![npm downloads](https://img.shields.io/npm/dm/intlayer?style=for-the-badge)](https://www.npmjs.com/package/intlayer) |
55
+ | `amannn/next-intl` | [![GitHub Repo stars](https://img.shields.io/github/stars/amannn/next-intl?style=for-the-badge&label=%E2%AD%90%20stars)](https://github.com/amannn/next-intl/stargazers) | [![GitHub commit activity](https://img.shields.io/github/commit-activity/t/amannn/next-intl?style=for-the-badge&label=commits)](https://github.com/amannn/next-intl/commits) | [![Last Commit](https://img.shields.io/github/last-commit/amannn/next-intl?style=for-the-badge)](https://github.com/amannn/next-intl/commits) | Nov 2020 | [![npm](https://img.shields.io/npm/v/next-intl?style=for-the-badge)](https://www.npmjs.com/package/next-intl) | [![npm downloads](https://img.shields.io/npm/dm/next-intl?style=for-the-badge)](https://www.npmjs.com/package/next-intl) |
56
+ | `i18next/i18next` | [![GitHub Repo stars](https://img.shields.io/github/stars/i18next/i18next?style=for-the-badge&label=%E2%AD%90%20stars)](https://github.com/i18next/i18next/stargazers) | [![GitHub commit activity](https://img.shields.io/github/commit-activity/t/i18next/i18next?style=for-the-badge&label=commits)](https://github.com/i18next/i18next/commits) | [![Last Commit](https://img.shields.io/github/last-commit/i18next/i18next?style=for-the-badge)](https://github.com/i18next/i18next/commits) | Jan 2012 | [![npm](https://img.shields.io/npm/v/i18next?style=for-the-badge)](https://www.npmjs.com/package/i18next) | [![npm downloads](https://img.shields.io/npm/dm/i18next?style=for-the-badge)](https://www.npmjs.com/package/i18next) |
57
+ | `i18next/next-i18next` | [![GitHub Repo stars](https://img.shields.io/github/stars/i18next/next-i18next?style=for-the-badge&label=%E2%AD%90%20stars)](https://github.com/i18next/next-i18next/stargazers) | [![GitHub commit activity](https://img.shields.io/github/commit-activity/t/i18next/next-i18next?style=for-the-badge&label=commits)](https://github.com/i18next/next-i18next/commits) | [![Last Commit](https://img.shields.io/github/last-commit/i18next/next-i18next?style=for-the-badge)](https://github.com/i18next/next-i18next/commits) | Nov 2018 | [![npm](https://img.shields.io/npm/v/next-i18next?style=for-the-badge)](https://www.npmjs.com/package/next-i18next) | [![npm downloads](https://img.shields.io/npm/dm/next-i18next?style=for-the-badge)](https://www.npmjs.com/package/next-i18next) |
56
58
 
57
59
  > Badges update automatically. Snapshots will vary over time.
58
60
 
@@ -140,6 +142,7 @@ Two important issues:
140
142
  > If I'm on the `/about` page, I don't want to load the content of the `/home` page
141
143
 
142
144
  - **Splitting by locale:**
145
+
143
146
  > If I'm on the `/fr/about` page, I don't want to load the content of the `/en/about` page
144
147
 
145
148
  Again, all three solutions are aware of these issues and allow managing these optimizations. The difference between the three solutions is the DX (Developer Experience).
@@ -162,14 +165,183 @@ How the library handles fallbacks is also important. Let's consider that the app
162
165
 
163
166
  In the case of `next-intl` and `next-i18next`, the library requires loading the JSON related to the current locale, but also to the fallback locale. Thus, considering that all content has been translated, each page will load 100% unnecessary content. **In comparison, `intlayer` processes the fallback at dictionary build time. Thus, each page will load only the content used.**
164
167
 
168
+ > Note: To optimize the bundle using `intlayer`, you need to set the `importMode: 'dynamic'` option in your `intlayer.config.ts` file. And ensure the plugin `@intlayer/babel` / `@intlayer/swc` is installed (installed by default using `vite-intlayer`).
169
+
165
170
  Here an example of the impact of bundle size optimization using `intlayer` in a vite + react application:
166
171
 
167
- | Optimized bundle | Bundle not optimized |
168
- | -------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
169
- | ![optimized bundle](https://github.com/aymericzip/intlayer/blob/main/docs/assets/bundle.png) | ![no optimized bundle](https://github.com/aymericzip/intlayer/blob/main/docs/assets/bundle_no_optimization.png) |
172
+ | Optimized bundle | Bundle not optimized |
173
+ | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
174
+ | ![optimized bundle](https://github.com/aymericzip/intlayer/blob/main/docs/assets/bundle.png?raw=true) | ![no optimized bundle](https://github.com/aymericzip/intlayer/blob/main/docs/assets/bundle_no_optimization.png?raw=true) |
175
+
176
+ ---
177
+
178
+ ## TypeScript & safety
179
+
180
+ <Columns>
181
+ <Column>
182
+
183
+ **next-intl**
184
+
185
+ - Solid TypeScript support, but **keys aren’t strictly typed by default**; you’ll maintain safety patterns manually.
186
+
187
+ </Column>
188
+ <Column>
189
+
190
+ **next-i18next**
191
+
192
+ - Base typings for hooks; **strict key typing requires extra tooling/config**.
193
+
194
+ </Column>
195
+ <Column>
196
+
197
+ **intlayer**
198
+
199
+ - **Generates strict types** from your content. **IDE autocompletion** and **compile-time errors** catch typos and missing keys before deploy.
200
+
201
+ </Column>
202
+ </Columns>
203
+
204
+ **Why it matters:** Strong typing shifts failures **left** (CI/build) instead of **right** (runtime).
205
+
206
+ ---
207
+
208
+ ## Missing translation handling
209
+
210
+ <Columns>
211
+ <Column>
212
+
213
+ **next-intl**
214
+
215
+ - Relies on **runtime fallbacks** (e.g., show the key or default locale). Build doesn’t fail.
216
+
217
+ </Column>
218
+ <Column>
219
+
220
+ **next-i18next**
221
+
222
+ - Relies on **runtime fallbacks** (e.g., show the key or default locale). Build doesn’t fail.
223
+
224
+ </Column>
225
+ <Column>
226
+
227
+ **intlayer**
228
+
229
+ - **Build-time detection** with **warnings/errors** for missing locales or keys.
230
+
231
+ </Column>
232
+ </Columns>
233
+
234
+ **Why it matters:** Catching gaps during build prevents “mystery strings” in production and aligns with strict release gates.
235
+
236
+ ---
237
+
238
+ ## Routing, middleware & URL strategy
239
+
240
+ <Columns>
241
+ <Column>
242
+
243
+ **next-intl**
244
+
245
+ - Works with **Next.js localized routing** on the App Router.
246
+
247
+ </Column>
248
+ <Column>
249
+
250
+ **next-i18next**
251
+
252
+ - Works with **Next.js localized routing** on the App Router.
253
+
254
+ </Column>
255
+ <Column>
256
+
257
+ **intlayer**
258
+
259
+ - All of the above, plus **i18n middleware** (locale detection via headers/cookies) and **helpers** to generate localized URLs and `<link rel="alternate" hreflang="…">` tags.
260
+
261
+ </Column>
262
+ </Columns>
263
+
264
+ **Why it matters:** Fewer custom glue layers; **consistent UX** and **clean SEO** across locales.
265
+
266
+ ---
267
+
268
+ ## Server Components (RSC) alignment
269
+
270
+ <Columns>
271
+ <Column>
272
+
273
+ **next-intl**
274
+
275
+ - Supports Next.js 13+. Often requires passing t-functions/formatters through component trees in hybrid setups.
276
+
277
+ </Column>
278
+ <Column>
279
+
280
+ **next-i18next**
281
+
282
+ - Supports Next.js 13+. Similar constraints with passing translation utilities across boundaries.
283
+
284
+ </Column>
285
+ <Column>
286
+
287
+ **intlayer**
288
+
289
+ - Supports Next.js 13+ and smooths the **server/client boundary** with a consistent API and RSC-oriented providers, avoiding shuttling formatters or t-functions.
290
+
291
+ </Column>
292
+ </Columns>
293
+
294
+ **Why it matters:** Cleaner mental model and fewer edge cases in hybrid trees.
170
295
 
171
296
  ---
172
297
 
298
+ ## DX, tooling & maintenance
299
+
300
+ <Columns>
301
+ <Column>
302
+
303
+ **next-intl**
304
+
305
+ - Commonly paired with external localization platforms and editorial workflows.
306
+
307
+ </Column>
308
+ <Column>
309
+
310
+ **next-i18next**
311
+
312
+ - Commonly paired with external localization platforms and editorial workflows.
313
+
314
+ </Column>
315
+ <Column>
316
+
317
+ **intlayer**
318
+
319
+ - Ships a **free Visual Editor** and **optional CMS** (Git-friendly or externalized), plus a **VSCode extension** and **AI-assisted translations** using your own provider keys.
320
+
321
+ </Column>
322
+ </Columns>
323
+
324
+ **Why it matters:** Lowers ops cost and shortens the loop between developers and content authors.
325
+
326
+ ## Integration with localization platforms (TMS)
327
+
328
+ Large organizations often rely on Translation Management Systems (TMS) like **Crowdin**, **Phrase**, **Lokalise**, **Localizely**, or **Localazy**.
329
+
330
+ - **Why companies care**
331
+ - **Collaboration & roles**: Multiple actors are involved: developers, product managers, translators, reviewers, marketing teams.
332
+ - **Scale & efficiency**: continuous localization, in‑context review.
333
+
334
+ - **next-intl / next-i18next**
335
+ - Typically use **centralized JSON catalogs**, so export/import with TMS is straightforward.
336
+ - Mature ecosystems and examples/integrations for the platforms above.
337
+
338
+ - **Intlayer**
339
+ - Encourages **decentralized, per-component dictionaries** and supports **TypeScript/TSX/JS/JSON/MD** content.
340
+ - This improves modularity in code, but can make plug‑and‑play TMS integration harder when a tool expects centralized, flat JSON files.
341
+ - Intlayer provides alternatives: **AI‑assisted translations** (using your own provider keys), a **Visual Editor/CMS**, and **CLI/CI** workflows to catch and prefill gaps.
342
+
343
+ > Note: `next-intl` and `i18next` also accepts TypeScript catalogs. If your team stores messages in `.ts` files or decentralizes them by feature, you can face similar TMS friction. However, many `next-intl` setups remain centralized in a `locales/` folder, which is a bit easier to refactor to JSON for TMS.
344
+
173
345
  ## Developer Experience
174
346
 
175
347
  This part makes a deep comparison between the three solutions. Rather than considering simple cases, as described in the 'getting started' documentation for each solution, we will consider a real use case, more similar to a real project.
@@ -184,32 +356,33 @@ The app structure is important to ensure good maintainability for your codebase.
184
356
 
185
357
  ```bash
186
358
  .
187
- ├── public
188
- │ └── locales
189
- │ ├── en
190
- │ │ ├── home.json
191
- │ │ └── navbar.json
192
- │ ├── fr
193
- │ │ ├── home.json
194
- │ │ └── navbar.json
195
- │ └── es
196
- │ ├── home.json
197
- │ └── navbar.json
198
- ├── next-i18next.config.js
359
+ ├── i18n.config.ts
199
360
  └── src
200
- ├── middleware.ts
361
+ ├── locales
362
+ │ ├── en
363
+ │ │ ├── common.json
364
+ │ │ └── about.json
365
+ │ └── fr
366
+ │ ├── common.json
367
+ │ └── about.json
201
368
  ├── app
202
- └── home.tsx
369
+ ├── i18n
370
+ │ │ └── server.ts
371
+ │ └── [locale]
372
+ │ ├── layout.tsx
373
+ │ └── about.tsx
203
374
  └── components
204
- └── Navbar
205
- └── index.tsx
375
+ ├── I18nProvider.tsx
376
+ ├── ClientComponent.tsx
377
+ └── ServerComponent.tsx
206
378
  ```
207
379
 
208
380
  </TabItem>
209
- <TabItem label="next-intl" value="next-intl">
381
+ <TabItem label="next-intl" value="next-intl">
210
382
 
211
383
  ```bash
212
384
  .
385
+ ├── i18n.ts
213
386
  ├── locales
214
387
  │ ├── en
215
388
  │ │ ├── home.json
@@ -220,11 +393,13 @@ The app structure is important to ensure good maintainability for your codebase.
220
393
  │ └── es
221
394
  │ ├── home.json
222
395
  │ └── navbar.json
223
- ├── i18n.ts
224
396
  └── src
225
397
  ├── middleware.ts
226
398
  ├── app
227
- └── home.tsx
399
+ ├── i18n
400
+ │ │ └── server.ts
401
+ │ └── [locale]
402
+ │ └── home.tsx
228
403
  └── components
229
404
  └── Navbar
230
405
  └── index.tsx
@@ -239,9 +414,11 @@ The app structure is important to ensure good maintainability for your codebase.
239
414
  └── src
240
415
  ├── middleware.ts
241
416
  ├── app
242
- │ └── home
243
- └── index.tsx
244
- │ └── index.content.ts
417
+ │ └── [locale]
418
+ ├── layout.tsx
419
+ │ └── home
420
+ │ ├── index.tsx
421
+ │ └── index.content.ts
245
422
  └── components
246
423
  └── Navbar
247
424
  ├── index.tsx
@@ -253,194 +430,287 @@ The app structure is important to ensure good maintainability for your codebase.
253
430
 
254
431
  #### Comparison
255
432
 
256
- ##### Configuration
257
-
258
- Intlayer uses a centralized configuration file to set up your locale, middleware, build, etc.
433
+ - **next-intl / next-i18next**: Centralized catalogs (JSON; namespaces/messages). Clear structure, integrates well with translation platforms, but can lead to more cross-file edits as apps grow.
434
+ - **Intlayer**: Per-component `.content.{ts|js|json}` dictionaries co-located with components. Easier component reuse and local reasoning; adds files and relies on build-time tooling.
259
435
 
260
- ##### Content declaration
261
-
262
- The centralized type of architecture slows down the development process and makes the codebase more complex to maintain for several reasons:
436
+ #### Setup and Loading Content
263
437
 
264
- 1. **For any new component created, you should:**
265
- - Create the new resource/namespace in the `locales` folder
266
- - Remember to import the new namespace in your page
267
- - Translate your content (often done manually by copy/paste from AI providers)
438
+ As mentioned previously, you must optimize how each JSON file is imported into your code.
439
+ How the library handles content loading is important.
268
440
 
269
- 2. **For any change made on your components, you should:**
270
- - Search for the related resource/namespace (far from the component)
271
- - Translate your content
272
- - Ensure your content is up to date for any locale
273
- - Verify your namespace doesn't include unused keys/values
274
- - Ensure the structure of your JSON files is the same for all locales
441
+ <Tab defaultTab="next-intl" group='techno'>
442
+ <TabItem label="next-i18next" value="next-i18next">
275
443
 
276
- On professional projects using these solutions, localization platforms are often used to help manage the translation of your content. However, this can quickly become costly for large projects.
444
+ ```ts fileName="i18n.config.ts"
445
+ export const locales = ["en", "fr"] as const;
446
+ export type Locale = (typeof locales)[number];
277
447
 
278
- To solve this problem, Intlayer adopts an approach that scopes your content per-component and keeps your content close to your component, as we often do with CSS (`styled-components`), types, documentation (`storybook`), or unit tests (`jest`).
448
+ export const defaultLocale: Locale = "en";
279
449
 
280
- This approach allows you to:
450
+ export const rtlLocales = ["ar", "he", "fa", "ur"] as const;
451
+ export const isRtl = (locale: string) =>
452
+ (rtlLocales as readonly string[]).includes(locale);
281
453
 
282
- 1. **Increase the speed of development**
283
- - `.content.{{ts|js|json}}` files can be created using a VSCode extension
284
- - Autocompletion AI tools in your IDE (such as GitHub Copilot) can help you declare your content, reducing copy/paste
454
+ export function localizedPath(locale: string, path: string) {
455
+ return locale === defaultLocale ? path : "/" + locale + path;
456
+ }
285
457
 
286
- 2. **Clean your codebase**
287
- - Reduce the complexity
288
- - Increase the maintainability
458
+ const ORIGIN = "https://example.com";
459
+ export function abs(locale: string, path: string) {
460
+ return ORIGIN + localizedPath(locale, path);
461
+ }
462
+ ```
289
463
 
290
- 3. **Duplicate your components and their related content more easily (Example: login/register components, etc.)**
291
- - By limiting the risk of impacting other components' content
292
- - By copy/pasting your content from one application to another without external dependencies
464
+ ```ts fileName="src/app/i18n/server.ts"
465
+ import { createInstance } from "i18next";
466
+ import { initReactI18next } from "react-i18next/initReactI18next";
467
+ import resourcesToBackend from "i18next-resources-to-backend";
468
+ import { defaultLocale } from "@/i18n.config";
469
+
470
+ // Load JSON resources from src/locales/<locale>/<namespace>.json
471
+ const backend = resourcesToBackend(
472
+ (locale: string, namespace: string) =>
473
+ import(`../../locales/${locale}/${namespace}.json`)
474
+ );
475
+
476
+ export async function initI18next(
477
+ locale: string,
478
+ namespaces: string[] = ["common"]
479
+ ) {
480
+ const i18n = createInstance();
481
+ await i18n
482
+ .use(initReactI18next)
483
+ .use(backend)
484
+ .init({
485
+ lng: locale,
486
+ fallbackLng: defaultLocale,
487
+ ns: namespaces,
488
+ defaultNS: "common",
489
+ interpolation: { escapeValue: false },
490
+ react: { useSuspense: false },
491
+ });
492
+ return i18n;
493
+ }
494
+ ```
293
495
 
294
- 4. **Avoid polluting your codebase with unused keys/values for unused components**
295
- - If you don't use a component, Intlayer will not import its related content
296
- - If you delete a component, you'll more easily remember to remove its related content as it will be present in the same folder
496
+ ```tsx fileName="src/components/I18nProvider.tsx"
497
+ "use client";
297
498
 
298
- 5. **Reduce reasoning cost for AI agents to declare your multilingual content**
299
- - The AI agent won't have to scan your entire codebase to know where to implement your content
300
- - Translations can easily be done by autocompletion AI tools in your IDE (such as GitHub Copilot)
499
+ import * as React from "react";
500
+ import { I18nextProvider } from "react-i18next";
501
+ import { createInstance } from "i18next";
502
+ import { initReactI18next } from "react-i18next/initReactI18next";
503
+ import resourcesToBackend from "i18next-resources-to-backend";
504
+ import { defaultLocale } from "@/i18n.config";
505
+
506
+ const backend = resourcesToBackend(
507
+ (locale: string, namespace: string) =>
508
+ import(`../../locales/${locale}/${namespace}.json`)
509
+ );
510
+
511
+ type Props = {
512
+ locale: string;
513
+ namespaces?: string[];
514
+ resources?: Record<string, any>; // { ns: bundle }
515
+ children: React.ReactNode;
516
+ };
301
517
 
302
- 6. **Optimize loading performance**
303
- - If a component is lazy-loaded, its related content will be loaded at the same time
518
+ export default function I18nProvider({
519
+ locale,
520
+ namespaces = ["common"],
521
+ resources,
522
+ children,
523
+ }: Props) {
524
+ const [i18n] = React.useState(() => {
525
+ const i = createInstance();
526
+
527
+ i.use(initReactI18next)
528
+ .use(backend)
529
+ .init({
530
+ lng: locale,
531
+ fallbackLng: defaultLocale,
532
+ ns: namespaces,
533
+ resources: resources ? { [locale]: resources } : undefined,
534
+ defaultNS: "common",
535
+ interpolation: { escapeValue: false },
536
+ react: { useSuspense: false },
537
+ });
538
+
539
+ return i;
540
+ });
304
541
 
305
- #### Setup and Loading Content
542
+ return <I18nextProvider i18n={i18n}>{children}</I18nextProvider>;
543
+ }
544
+ ```
306
545
 
307
- As mentioned previously, you must optimize how each JSON file is imported into your code.
308
- How the library handles content loading is important.
546
+ ```tsx fileName="src/app/[locale]/layout.tsx"
547
+ import type { ReactNode } from "react";
548
+ import { locales, defaultLocale, isRtl, type Locale } from "@/i18n.config";
309
549
 
310
- <Tab defaultTab="next-intl" group='techno'>
311
- <TabItem label="next-i18next" value="next-i18next">
550
+ export const dynamicParams = false;
312
551
 
313
- ```tsx fileName="next-i18next.config.js"
314
- module.exports = {
315
- i18n: {
316
- locales: ["en", "fr", "es"],
317
- defaultLocale: "en",
318
- },
319
- };
320
- ```
552
+ export function generateStaticParams() {
553
+ return locales.map((locale) => ({ locale }));
554
+ }
321
555
 
322
- ```tsx fileName="src/app/_app.tsx"
323
- import { appWithTranslation } from "next-i18next";
556
+ export default function LocaleLayout({
557
+ children,
558
+ params,
559
+ }: {
560
+ children: ReactNode;
561
+ params: { locale: string };
562
+ }) {
563
+ const locale: Locale = (locales as readonly string[]).includes(params.locale)
564
+ ? (params.locale as any)
565
+ : defaultLocale;
324
566
 
325
- const MyApp = ({ Component, pageProps }) => <Component {...pageProps} />;
567
+ const dir = isRtl(locale) ? "rtl" : "ltr";
326
568
 
327
- export default appWithTranslation(MyApp);
569
+ return (
570
+ <html lang={locale} dir={dir}>
571
+ <body>{children}</body>
572
+ </html>
573
+ );
574
+ }
328
575
  ```
329
576
 
330
- ```tsx fileName="src/app/[locale]/about/page.tsx"
331
- import type { GetStaticProps } from "next";
332
- import { serverSideTranslations } from "next-i18next/serverSideTranslations";
333
- import { useTranslation } from "next-i18next";
334
- import { I18nextProvider, initReactI18next } from "react-i18next";
335
- import { createInstance } from "i18next";
336
- import { ClientComponent, ServerComponent } from "@components";
577
+ ```tsx fileName="src/app/[locale]/about.tsx"
578
+ import I18nProvider from "@/components/I18nProvider";
579
+ import { initI18next } from "@/app/i18n/server";
580
+ import type { Locale } from "@/i18n.config";
581
+ import ClientComponent from "@/components/ClientComponent";
582
+ import ServerComponent from "@/components/ServerComponent";
337
583
 
338
- export default function HomePage() {
339
- // Déclarez explicitement le namespace utilisé par ce composant
340
- const resources = await loadMessagesFor(locale); // your loader (JSON, etc.)
584
+ // Force static rendering for the page
585
+ export const dynamic = "force-static";
341
586
 
342
- const i18n = createInstance();
343
- i18n.use(initReactI18next).init({
344
- lng: locale,
345
- fallbackLng: "en",
346
- resources,
347
- ns: ["common", "about"],
348
- defaultNS: "common",
349
- interpolation: { escapeValue: false },
350
- });
587
+ export default async function AboutPage({
588
+ params: { locale },
589
+ }: {
590
+ params: { locale: Locale };
591
+ }) {
592
+ const namespaces = ["common", "about"] as const;
351
593
 
352
- const { t } = useTranslation("about");
594
+ const i18n = await initI18next(locale, [...namespaces]);
595
+ const tAbout = i18n.getFixedT(locale, "about");
353
596
 
354
597
  return (
355
- <I18nextProvider i18n={i18n}>
598
+ <I18nProvider locale={locale} namespaces={[...namespaces]}>
356
599
  <main>
357
- <h1>{t("title")}</h1>
600
+ <h1>{tAbout("title")}</h1>
601
+
358
602
  <ClientComponent />
359
- <ServerComponent />
603
+ <ServerComponent t={tAbout} locale={locale} count={0} />
360
604
  </main>
361
- </I18nextProvider>
605
+ </I18nProvider>
362
606
  );
363
607
  }
364
-
365
- export const getStaticProps: GetStaticProps = async ({ locale }) => {
366
- // Ne préchargez que les namespaces nécessaires à CETTE page
367
- return {
368
- props: {
369
- ...(await serverSideTranslations(locale ?? "en", ["common", "about"])),
370
- },
371
- };
372
- };
373
608
  ```
374
609
 
375
610
  </TabItem>
376
611
  <TabItem label="next-intl" value="next-intl">
377
612
 
378
- ```tsx fileName="i18n.ts"
613
+ ```tsx fileName="src/i18n.ts"
379
614
  import { getRequestConfig } from "next-intl/server";
380
615
  import { notFound } from "next/navigation";
381
616
 
382
- // Can be imported from a shared config
383
- const locales = ["en", "fr", "es"];
617
+ export const locales = ["en", "fr", "es"] as const;
618
+ export const defaultLocale = "en" as const;
619
+
620
+ async function loadMessages(locale: string) {
621
+ // Load only the namespaces your layout/pages need
622
+ const [common, about] = await Promise.all([
623
+ import(`../locales/${locale}/common.json`).then((m) => m.default),
624
+ import(`../locales/${locale}/about.json`).then((m) => m.default),
625
+ ]);
626
+
627
+ return { common, about } as const;
628
+ }
384
629
 
385
630
  export default getRequestConfig(async ({ locale }) => {
386
- // Validate that the incoming `locale` parameter is valid
387
631
  if (!locales.includes(locale as any)) notFound();
388
632
 
389
633
  return {
390
- messages: (await import(`../messages/${locale}.json`)).default,
634
+ messages: await loadMessages(locale),
391
635
  };
392
636
  });
393
637
  ```
394
638
 
395
- ```tsx fileName="src/app/[locale]/about/layout.tsx"
396
- import { NextIntlClientProvider } from "next-intl";
397
- import { getMessages } from "next-intl/server";
398
- import pick from "lodash/pick";
639
+ ```tsx fileName="src/app/[locale]/layout.tsx"
640
+ import type { ReactNode } from "react";
641
+ import { locales } from "@/i18n";
642
+ import {
643
+ getLocaleDirection,
644
+ unstable_setRequestLocale,
645
+ } from "next-intl/server";
646
+
647
+ export const dynamic = "force-static";
648
+
649
+ export function generateStaticParams() {
650
+ return locales.map((locale) => ({ locale }));
651
+ }
399
652
 
400
653
  export default async function LocaleLayout({
401
654
  children,
402
655
  params,
403
656
  }: {
404
- children: React.ReactNode;
405
- params: { locale: string };
657
+ children: ReactNode;
658
+ params: Promise<{ locale: string }>;
406
659
  }) {
407
- const { locale } = params;
408
- // Les messages sont chargés côté serveur via src/i18n/request.ts
409
- // (voir docs next-intl). Ici on ne pousse au client qu'un sous-ensemble
410
- // utile aux composants client (optimisation du payload).
411
- const messages = await getMessages();
412
- const clientMessages = pick(messages, ["common", "about"]);
660
+ const { locale } = await params;
661
+
662
+ // Set the active request locale for this server render (RSC)
663
+ unstable_setRequestLocale(locale);
664
+
665
+ const dir = getLocaleDirection(locale);
413
666
 
414
667
  return (
415
- <html lang={locale}>
416
- <body>
417
- <NextIntlClientProvider locale={locale} messages={clientMessages}>
418
- {children}
419
- </NextIntlClientProvider>
420
- </body>
668
+ <html lang={locale} dir={dir}>
669
+ <body>{children}</body>
421
670
  </html>
422
671
  );
423
672
  }
424
673
  ```
425
674
 
426
675
  ```tsx fileName="src/app/[locale]/about/page.tsx"
427
- import { getTranslations } from "next-intl/server";
428
- import { ClientComponent, ServerComponent } from "@components";
676
+ import { getTranslations, getMessages, getFormatter } from "next-intl/server";
677
+ import { NextIntlClientProvider } from "next-intl";
678
+ import pick from "lodash/pick";
679
+ import ServerComponent from "@/components/ServerComponent";
680
+ import ClientComponentExample from "@/components/ClientComponentExample";
681
+
682
+ export const dynamic = "force-static";
429
683
 
430
- export default async function LandingPage({
684
+ export default async function AboutPage({
431
685
  params,
432
686
  }: {
433
- params: { locale: string };
687
+ params: Promise<{ locale: string }>;
434
688
  }) {
435
- // Chargement strictement côté serveur (pas hydraté au client)
436
- const t = await getTranslations("about");
689
+ const { locale } = await params;
690
+
691
+ // Messages are loaded server-side. Push only what's needed to the client.
692
+ const messages = await getMessages();
693
+ const clientMessages = pick(messages, ["common", "about"]);
694
+
695
+ // Strictly server-side translations/formatting
696
+ const tAbout = await getTranslations("about");
697
+ const tCounter = await getTranslations("about.counter");
698
+ const format = await getFormatter();
699
+
700
+ const initialFormattedCount = format.number(0);
437
701
 
438
702
  return (
439
- <main>
440
- <h1>{t("title")}</h1>
441
- <ClientComponent />
442
- <ServerComponent />
443
- </main>
703
+ <NextIntlClientProvider locale={locale} messages={clientMessages}>
704
+ <main>
705
+ <h1>{tAbout("title")}</h1>
706
+ <ClientComponentExample />
707
+ <ServerComponent
708
+ formattedCount={initialFormattedCount}
709
+ label={tCounter("label")}
710
+ increment={tCounter("increment")}
711
+ />
712
+ </main>
713
+ </NextIntlClientProvider>
444
714
  );
445
715
  }
446
716
  ```
@@ -449,12 +719,16 @@ export default async function LandingPage({
449
719
  <TabItem label="intlayer" value="intlayer">
450
720
 
451
721
  ```tsx fileName="intlayer.config.ts"
452
- export default {
722
+ import { type IntlayerConfig, Locales } from "intlayer";
723
+
724
+ const config: IntlayerConfig = {
453
725
  internationalization: {
454
- locales: ["en", "fr", "es"],
455
- defaultLocale: "en",
726
+ locales: [Locales.ENGLISH, Locales.FRENCH, Locales.SPANISH],
727
+ defaultLocale: Locales.ENGLISH,
456
728
  },
457
729
  };
730
+
731
+ export default config;
458
732
  ```
459
733
 
460
734
  ```tsx fileName="src/app/[locale]/layout.tsx"
@@ -467,14 +741,16 @@ import {
467
741
 
468
742
  export const dynamic = "force-static";
469
743
 
470
- const LandingLayout: NextLayoutIntlayer = async ({ children, params }) => {
744
+ const LocaleLayout: NextLayoutIntlayer = async ({ children, params }) => {
471
745
  const { locale } = await params;
472
746
 
473
747
  return (
474
748
  <html lang={locale} dir={getHTMLTextDir(locale)}>
475
- <IntlayerClientProvider locale={locale}>
476
- {children}
477
- </IntlayerClientProvider>
749
+ <body>
750
+ <IntlayerClientProvider locale={locale}>
751
+ {children}
752
+ </IntlayerClientProvider>
753
+ </body>
478
754
  </html>
479
755
  );
480
756
  };
@@ -511,13 +787,13 @@ export default LandingPage;
511
787
 
512
788
  #### Comparison
513
789
 
514
- In comparison to other solutions, Intlayer uses plugins to optimize the import of content at build time.
790
+ All three support per-locale content loading and providers.
515
791
 
516
- This means that the client context can be stored in the root layout, to reuse only one instance of the provider for all pages.
792
+ - With **next-intl/next-i18next**, you typically load selected messages/namespaces per route and place providers where needed.
517
793
 
518
- In comparison, the other solutions require rebuilding a custom provider for each page.
794
+ - With **Intlayer**, adds build-time analysis to infer usage, which can reduce manual wiring and may allow a single root provider.
519
795
 
520
- Intlayer also provides a Provider for your server components. We'll see why later.
796
+ Choose between explicit control and automation based on team preference.
521
797
 
522
798
  ### Usage in a client component
523
799
 
@@ -526,10 +802,12 @@ Let's take an example of a client component rendering a counter.
526
802
  <Tab defaultTab="next-intl" group='techno'>
527
803
  <TabItem label="next-i18next" value="next-i18next">
528
804
 
529
- **Translations (must be real JSON in `public/locales/...`)**
805
+ **Translations (one JSON per namespace under `src/locales/...`)**
530
806
 
531
- ```json fileName="public/locales/en/about.json"
807
+ ```json fileName="src/locales/en/about.json"
532
808
  {
809
+ "title": "About",
810
+ "description": "About page description",
533
811
  "counter": {
534
812
  "label": "Counter",
535
813
  "increment": "Increment"
@@ -537,8 +815,10 @@ Let's take an example of a client component rendering a counter.
537
815
  }
538
816
  ```
539
817
 
540
- ```json fileName="public/locales/fr/about.json"
818
+ ```json fileName="src/locales/fr/about.json"
541
819
  {
820
+ "title": "À propos",
821
+ "description": "Description de la page À propos",
542
822
  "counter": {
543
823
  "label": "Compteur",
544
824
  "increment": "Incrémenter"
@@ -546,19 +826,18 @@ Let's take an example of a client component rendering a counter.
546
826
  }
547
827
  ```
548
828
 
549
- **Client component**
829
+ **Client component (loads only the required namespace)**
550
830
 
551
- ```tsx fileName="src/components/ClientComponentExample.tsx"
831
+ ```tsx fileName="src/components/ClientComponent.tsx"
552
832
  "use client";
553
833
 
554
- import React, { useMemo, useState } from "react";
555
- import { useTranslation } from "next-i18next";
834
+ import React, { useState } from "react";
835
+ import { useTranslation } from "react-i18next";
556
836
 
557
- export default function ClientComponentExample() {
837
+ const ClientComponent = () => {
558
838
  const { t, i18n } = useTranslation("about");
559
839
  const [count, setCount] = useState(0);
560
840
 
561
- // next-i18next doesn't expose useNumber; use Intl.NumberFormat
562
841
  const numberFormat = new Intl.NumberFormat(i18n.language);
563
842
 
564
843
  return (
@@ -566,17 +845,19 @@ export default function ClientComponentExample() {
566
845
  <p>{numberFormat.format(count)}</p>
567
846
  <button
568
847
  aria-label={t("counter.label")}
569
- onClick={() => setCount((count) => count + 1)}
848
+ onClick={() => setCount((c) => c + 1)}
570
849
  >
571
850
  {t("counter.increment")}
572
851
  </button>
573
852
  </div>
574
853
  );
575
- }
854
+ };
855
+
856
+ export default ClientComponent;
576
857
  ```
577
858
 
578
- > Don't forget to add "about" namespace on the page serverSideTranslations
579
- > We take here the version of react 19.x.x, but for lower versions, you will need to use useMemo to store the instance of the formatter as it's a heavy function
859
+ > Ensure the page/provider includes only the namespaces you need (e.g. `about`).
860
+ > If you use React < 19, memoize heavy formatters like `Intl.NumberFormat`.
580
861
 
581
862
  </TabItem>
582
863
  <TabItem label="next-intl" value="next-intl">
@@ -609,7 +890,7 @@ export default function ClientComponentExample() {
609
890
  import React, { useState } from "react";
610
891
  import { useTranslations, useFormatter } from "next-intl";
611
892
 
612
- export default function ClientComponentExample() {
893
+ const ClientComponentExample = () => {
613
894
  // Scope directly to the nested object
614
895
  const t = useTranslations("about.counter");
615
896
  const format = useFormatter();
@@ -626,7 +907,7 @@ export default function ClientComponentExample() {
626
907
  </button>
627
908
  </div>
628
909
  );
629
- }
910
+ };
630
911
  ```
631
912
 
632
913
  > Don't forget to add "about" message on the page client message
@@ -658,7 +939,7 @@ export default counterContent;
658
939
  import React, { useState } from "react";
659
940
  import { useNumber, useIntlayer } from "next-intlayer";
660
941
 
661
- export default function ClientComponentExample() {
942
+ const ClientComponentExample = () => {
662
943
  const [count, setCount] = useState(0);
663
944
  const { label, increment } = useIntlayer("counter"); // returns strings
664
945
  const { number } = useNumber();
@@ -671,7 +952,7 @@ export default function ClientComponentExample() {
671
952
  </button>
672
953
  </div>
673
954
  );
674
- }
955
+ };
675
956
  ```
676
957
 
677
958
  </TabItem>
@@ -701,65 +982,60 @@ We will take the case of a UI component. This component is a server component, a
701
982
  <Tab defaultTab="next-intl" group='techno'>
702
983
  <TabItem label="next-i18next" value="next-i18next">
703
984
 
704
- ```tsx fileName="src/pages/about.tsx"
705
- import React from "react";
706
- import type { GetStaticProps } from "next";
707
- import { useTranslation } from "next-i18next";
708
- import { serverSideTranslations } from "next-i18next/serverSideTranslations";
709
-
985
+ ```tsx fileName="src/components/ServerComponent.tsx"
710
986
  type ServerComponentProps = {
711
- count: number;
712
987
  t: (key: string) => string;
713
- format: (value: number) => string;
988
+ locale: string;
989
+ count: number;
714
990
  };
715
991
 
716
- export default function ServerComponent({
717
- t,
718
- format,
719
- count,
720
- }: ServerComponentProps) {
992
+ const ServerComponent = ({ t, locale, count }: ServerComponentProps) => {
993
+ const formatted = new Intl.NumberFormat(locale).format(count);
994
+
721
995
  return (
722
996
  <div>
723
- <p>{format(count)}</p>
997
+ <p>{formatted}</p>
724
998
  <button aria-label={t("counter.label")}>{t("counter.increment")}</button>
725
999
  </div>
726
1000
  );
727
- }
728
- ```
1001
+ };
729
1002
 
730
- > As the server component cannot be async, you need to pass the translations and formatter function as props.
731
- >
732
- > - `const { t, i18n } = useTranslation("about");`
733
- > - `const formatted = new Intl.NumberFormat(i18n.language).format(initialCount);`
1003
+ export default ServerComponent;
1004
+ ```
734
1005
 
735
1006
  </TabItem>
736
1007
  <TabItem label="next-intl" value="next-intl">
737
1008
 
738
1009
  ```tsx fileName="src/components/ServerComponent.tsx"
739
- import { getTranslations, getFormatter } from "next-intl/server";
1010
+ type ServerComponentProps = {
1011
+ formattedCount: string;
1012
+ label: string;
1013
+ increment: string;
1014
+ };
740
1015
 
741
- export default async function ServerComponent({
742
- t,
743
- format,
744
- count,
745
- }: {
746
- t: (key: string) => string;
747
- format: (value: number) => string;
748
- count: number;
749
- }) {
1016
+ const ServerComponent = ({
1017
+ formattedCount,
1018
+ label,
1019
+ increment,
1020
+ }: ServerComponentProps) => {
750
1021
  return (
751
1022
  <div>
752
- <p>{format.number(count)}</p>
753
- <button aria-label={t("label")}>{t("increment")}</button>
1023
+ <p>{formattedCount}</p>
1024
+ <button aria-label={label}>{increment}</button>
754
1025
  </div>
755
1026
  );
756
- }
1027
+ };
1028
+
1029
+ export default ServerComponent;
757
1030
  ```
758
1031
 
759
1032
  > As the server component cannot be async, you need to pass the translations and formatter function as props.
760
1033
  >
1034
+ > In your page / layout:
1035
+ >
1036
+ > - `import { getTranslations, getFormatter } from "next-intl/server";`
761
1037
  > - `const t = await getTranslations("about.counter");`
762
- > - `const format = await getFormatter();`
1038
+ > - `const formatter = await getFormatter().then((formatter) => formatter.number());`
763
1039
 
764
1040
  </TabItem>
765
1041
  <TabItem label="intlayer" value="intlayer">
@@ -767,7 +1043,11 @@ export default async function ServerComponent({
767
1043
  ```tsx fileName="src/components/ServerComponent.tsx"
768
1044
  import { useIntlayer, useNumber } from "next-intlayer/server";
769
1045
 
770
- const ServerComponent = ({ count }: { count: number }) => {
1046
+ type ServerComponentProps = {
1047
+ count: number;
1048
+ };
1049
+
1050
+ const ServerComponent = ({ count }: ServerComponentProps) => {
771
1051
  const { label, increment } = useIntlayer("counter");
772
1052
  const { number } = useNumber();
773
1053
 
@@ -832,10 +1112,9 @@ export async function generateMetadata({
832
1112
  }): Promise<Metadata> {
833
1113
  const { locale } = params;
834
1114
 
835
- // Dynamically import the correct JSON file
836
- const messages = (
837
- await import("@/../public/locales/" + locale + "/about.json")
838
- ).default;
1115
+ // Import the correct JSON bundle from src/locales
1116
+ const messages = (await import("@/locales/" + locale + "/about.json"))
1117
+ .default;
839
1118
 
840
1119
  const languages = Object.fromEntries(
841
1120
  locales.map((locale) => [locale, localizedPath(locale, "/about")])
@@ -909,7 +1188,7 @@ export default function robots(): MetadataRoute.Robots {
909
1188
  ```tsx fileName="src/app/[locale]/about/layout.tsx"
910
1189
  import type { Metadata } from "next";
911
1190
  import { locales, defaultLocale } from "@/i18n";
912
- import { getTranslations, unstable_setRequestLocale } from "next-intl/server";
1191
+ import { getTranslations } from "next-intl/server";
913
1192
 
914
1193
  function localizedPath(locale: string, path: string) {
915
1194
  return locale === defaultLocale ? path : "/" + locale + path;
@@ -1061,149 +1340,126 @@ export default robots;
1061
1340
 
1062
1341
  > Intlayer provides a `getMultilingualUrls` function to generate multilingual URLs for your sitemap.
1063
1342
 
1064
- ---
1343
+ ### Middleware for locale routing
1065
1344
 
1066
- ## TypeScript & safety
1067
-
1068
- <Columns>
1069
- <Column>
1070
-
1071
- **next-intl**
1072
-
1073
- - Solid TypeScript support, but **keys aren’t strictly typed by default**; you’ll maintain safety patterns manually.
1074
-
1075
- </Column>
1076
- <Column>
1077
-
1078
- **next-i18next**
1079
-
1080
- - Base typings for hooks; **strict key typing requires extra tooling/config**.
1081
-
1082
- </Column>
1083
- <Column>
1084
-
1085
- **intlayer**
1086
-
1087
- - **Generates strict types** from your content. **IDE autocompletion** and **compile-time errors** catch typos and missing keys before deploy.
1088
-
1089
- </Column>
1090
- <Columns>
1345
+ <Tab defaultTab="next-intl" group='techno'>
1346
+ <TabItem label="next-i18next" value="next-i18next">
1091
1347
 
1092
- **Why it matters:** Strong typing shifts failures **left** (CI/build) instead of **right** (runtime).
1348
+ Add a middleware to handle locale detection and routing:
1093
1349
 
1094
- ---
1350
+ ```ts fileName="src/middleware.ts"
1351
+ import { NextResponse, type NextRequest } from "next/server";
1352
+ import { defaultLocale, locales } from "@/i18n.config";
1095
1353
 
1096
- ## Missing translation handling
1354
+ const PUBLIC_FILE = /\.[^/]+$/; // exclude files with extensions
1097
1355
 
1098
- **next-intl**
1356
+ export function middleware(request: NextRequest) {
1357
+ const { pathname } = request.nextUrl;
1099
1358
 
1100
- - Relies on **runtime fallbacks** (e.g., show the key or default locale). Build doesn’t fail.
1359
+ if (
1360
+ pathname.startsWith("/_next") ||
1361
+ pathname.startsWith("/api") ||
1362
+ pathname.startsWith("/static") ||
1363
+ PUBLIC_FILE.test(pathname)
1364
+ ) {
1365
+ return;
1366
+ }
1101
1367
 
1102
- **next-i18next**
1368
+ const hasLocale = locales.some(
1369
+ (l) => pathname === "/" + l || pathname.startsWith("/" + l + "/")
1370
+ );
1371
+ if (!hasLocale) {
1372
+ const locale = defaultLocale;
1373
+ const url = request.nextUrl.clone();
1374
+ url.pathname = "/" + locale + (pathname === "/" ? "" : pathname);
1375
+ return NextResponse.redirect(url);
1376
+ }
1377
+ }
1103
1378
 
1104
- - Relies on **runtime fallbacks** (e.g., show the key or default locale). Build doesn’t fail.
1379
+ export const config = {
1380
+ matcher: [
1381
+ // Match all paths except the ones starting with these and files with an extension
1382
+ "/((?!api|_next|static|.*\\..*).*)",
1383
+ ],
1384
+ };
1385
+ ```
1105
1386
 
1106
- **intlayer**
1387
+ </TabItem>
1388
+ <TabItem label="next-intl" value="next-intl">
1107
1389
 
1108
- - **Build-time detection** with **warnings/errors** for missing locales or keys.
1390
+ Add a middleware to handle locale detection and routing:
1109
1391
 
1110
- **Why it matters:** Catching gaps during build prevents “mystery strings” in production and aligns with strict release gates.
1392
+ ```ts fileName="src/middleware.ts"
1393
+ import createMiddleware from "next-intl/middleware";
1394
+ import { locales, defaultLocale } from "@/i18n";
1111
1395
 
1112
- ---
1396
+ export default createMiddleware({
1397
+ locales: [...locales],
1398
+ defaultLocale,
1399
+ localeDetection: true,
1400
+ });
1113
1401
 
1114
- ## Routing, middleware & URL strategy
1402
+ export const config = {
1403
+ // Skip API, Next internals and static assets
1404
+ matcher: ["/((?!api|_next|.*\\..*).*)"],
1405
+ };
1406
+ ```
1115
1407
 
1116
- <Columns>
1117
- <Column>
1408
+ </TabItem>
1409
+ <TabItem label="intlayer" value="intlayer">
1118
1410
 
1119
- **next-intl**
1411
+ Intlayer provides built-in middleware handling through the `next-intlayer` package configuration.
1120
1412
 
1121
- - Works with **Next.js localized routing** on the App Router.
1413
+ </TabItem>
1414
+ </Tab>
1122
1415
 
1123
- </Column>
1124
- <Column>
1416
+ ### Setup checklist and good practices
1125
1417
 
1126
- **next-i18next**
1418
+ <Tab defaultTab="next-intl" group='techno'>
1419
+ <TabItem label="next-i18next" value="next-i18next">
1127
1420
 
1128
- - Works with **Next.js localized routing** on the App Router.
1421
+ - Ensure `lang` and `dir` are set on the root `<html>` in `src/app/[locale]/layout.tsx`.
1422
+ - Split translations into namespaces (for example `common.json`, `about.json`) under `src/locales/<locale>/`.
1423
+ - Only load required namespaces in client components using `useTranslation('<ns>')` and by scoping `I18nProvider` with the same namespaces.
1424
+ - Keep pages static when possible: export `export const dynamic = 'force-static'` on pages; set `dynamicParams = false` and implement `generateStaticParams`.
1425
+ - Use sync server components nested under client boundaries by passing already-computed strings or the `t` function and the `locale`.
1426
+ - For SEO, set `alternates.languages` in metadata, list localized URLs in `sitemap.ts`, and disallow duplicate localized routes in `robots.ts`.
1427
+ - Prefer locale-aware formatters (e.g., `Intl.NumberFormat(locale)`) and memoize them on the client if using React < 19.
1129
1428
 
1130
- </Column>
1131
- <Column>
1429
+ </TabItem>
1430
+ <TabItem label="next-intl" value="next-intl">
1132
1431
 
1133
- **intlayer**
1432
+ - **Set html `lang` and `dir`**: In `src/app/[locale]/layout.tsx`, compute `dir` via `getLocaleDirection(locale)` and set `<html lang={locale} dir={dir}>`.
1433
+ - **Split messages by namespace**: Organize JSON per locale and namespace (e.g., `common.json`, `about.json`).
1434
+ - **Minimize client payload**: On pages, send only required namespaces to `NextIntlClientProvider` (e.g., `pick(messages, ['common', 'about'])`).
1435
+ - **Prefer static pages**: Export `export const dynamic = 'force-static'` and generate static params for all `locales`.
1436
+ - **Synchronous server components**: Keep server components sync by passing precomputed strings (translated labels, formatted numbers) rather than async calls or non-serializable functions.
1134
1437
 
1135
- - All of the above, plus **i18n middleware** (locale detection via headers/cookies) and **helpers** to generate localized URLs and `<link rel="alternate" hreflang="…">` tags.
1438
+ </TabItem>
1439
+ <TabItem label="intlayer" value="intlayer">
1136
1440
 
1137
- </Column>
1138
- </Columns>
1441
+ - **Modular content**: Co-locate content dictionaries with components using `.content.{ts|js|json}` files.
1442
+ - **Type safety**: Leverage TypeScript integration for compile-time content validation.
1443
+ - **Build-time optimization**: Use Intlayer's build tools for automatic tree-shaking and bundle optimization.
1444
+ - **Integrated tooling**: Take advantage of built-in routing, SEO helpers, and visual editor support.
1139
1445
 
1140
- **Why it matters:** Fewer custom glue layers; **consistent UX** and **clean SEO** across locales.
1446
+ </TabItem>
1447
+ </Tab>
1141
1448
 
1142
1449
  ---
1143
1450
 
1144
- ## Server Components (RSC) alignment
1145
-
1146
- <Columns>
1147
- <Column>
1148
-
1149
- **next-intl**
1150
-
1151
- - Supports Next.js 13+. Often requires passing t-functions/formatters through component trees in hybrid setups.
1152
-
1153
- </Column>
1154
- <Column>
1155
-
1156
- **next-i18next**
1157
-
1158
- - Supports Next.js 13+. Similar constraints with passing translation utilities across boundaries.
1159
-
1160
- </Column>
1161
- <Column>
1162
-
1163
- **intlayer**
1164
-
1165
- - Supports Next.js 13+ and smooths the **server/client boundary** with a consistent API and RSC-oriented providers, avoiding shuttling formatters or t-functions.
1166
-
1167
- </Column>
1168
- </Columns>
1169
-
1170
- **Why it matters:** Cleaner mental model and fewer edge cases in hybrid trees.
1171
-
1172
- ---
1451
+ ## And the winner is…
1173
1452
 
1174
- ## DX, tooling & maintenance
1453
+ It’s not simple. Each option has trade-offs. Here’s how I see it:
1175
1454
 
1176
1455
  <Columns>
1177
1456
  <Column>
1178
1457
 
1179
- **next-intl**
1180
-
1181
- - Commonly paired with external localization platforms and editorial workflows.
1182
-
1183
- </Column>
1184
- <Column>
1185
-
1186
1458
  **next-i18next**
1187
1459
 
1188
- - Commonly paired with external localization platforms and editorial workflows.
1189
-
1190
- </Column>
1191
- <Column>
1192
-
1193
- **intlayer**
1194
-
1195
- - Ships a **free Visual Editor** and **optional CMS** (Git-friendly or externalized), plus a **VSCode extension** and **AI-assisted translations** using your own provider keys.
1460
+ - mature, full of features, lots of community plugins, but higher setup cost. If you need **i18next’s plugin ecosystem** (e.g., advanced ICU rules via plugins) and your team already knows i18next, accepting **more configuration** for flexibility.
1196
1461
 
1197
1462
  </Column>
1198
- </Columns>
1199
-
1200
- **Why it matters:** Lowers ops cost and shortens the loop between developers and content authors.
1201
-
1202
- ## And the winner is…
1203
-
1204
- It’s not simple. Each option has trade-offs. Here’s how I see it:
1205
-
1206
- <Columns>
1207
1463
  <Column>
1208
1464
 
1209
1465
  **next-intl**
@@ -1213,13 +1469,6 @@ It’s not simple. Each option has trade-offs. Here’s how I see it:
1213
1469
  </Column>
1214
1470
  <Column>
1215
1471
 
1216
- **next-i18next**
1217
-
1218
- - mature, full of features, lots of community plugins, but higher setup cost. If you need **i18next’s plugin ecosystem** (e.g., advanced ICU rules via plugins) and your team already knows i18next, accepting **more configuration** for flexibility.
1219
-
1220
- </Column>
1221
- <Column>
1222
-
1223
1472
  **Intlayer**
1224
1473
 
1225
1474
  - built for modern Next.js, with modular content, type safety, tooling, and less boilerplate. If you value **component-scoped content**, **strict TypeScript**, **build-time guarantees**, **tree-shaking**, and **batteries-included** routing/SEO/editor tooling - especially for **Next.js App Router**, design-systems and **large, modular codebases**.
@@ -1233,8 +1482,6 @@ If you prefer minimal setup and accept some manual wiring, next-intl is a good p
1233
1482
 
1234
1483
  > **Future roadmap**: Intlayer also plans to develop plugins that work on top of **i18next** and **next-intl** solutions. This will give you the advantages of Intlayer for automation, syntax, and content management while keeping the security and stability provided by these established solutions in your application code.
1235
1484
 
1236
- ---
1237
-
1238
1485
  ## GitHub STARs
1239
1486
 
1240
1487
  GitHub stars are a strong indicator of a project's popularity, community trust, and long-term relevance. While not a direct measure of technical quality, they reflect how many developers find the project useful, follow its progress, and are likely to adopt it. For estimating the value of a project, stars help compare traction across alternatives and provide insights into ecosystem growth.