@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.
- package/blog/ar/next-i18next_vs_next-intl_vs_intlayer.md +1366 -75
- package/blog/ar/nextjs-multilingual-seo-comparison.md +364 -0
- package/blog/de/next-i18next_vs_next-intl_vs_intlayer.md +1288 -72
- package/blog/de/nextjs-multilingual-seo-comparison.md +362 -0
- package/blog/en/intlayer_with_next-i18next.mdx +431 -0
- package/blog/en/intlayer_with_next-intl.mdx +335 -0
- package/blog/en/next-i18next_vs_next-intl_vs_intlayer.md +583 -336
- package/blog/en/nextjs-multilingual-seo-comparison.md +360 -0
- package/blog/en-GB/next-i18next_vs_next-intl_vs_intlayer.md +1144 -37
- package/blog/en-GB/nextjs-multilingual-seo-comparison.md +360 -0
- package/blog/es/next-i18next_vs_next-intl_vs_intlayer.md +1236 -64
- package/blog/es/nextjs-multilingual-seo-comparison.md +363 -0
- package/blog/fr/next-i18next_vs_next-intl_vs_intlayer.md +1142 -75
- package/blog/fr/nextjs-multilingual-seo-comparison.md +362 -0
- package/blog/hi/nextjs-multilingual-seo-comparison.md +363 -0
- package/blog/it/next-i18next_vs_next-intl_vs_intlayer.md +1130 -55
- package/blog/it/nextjs-multilingual-seo-comparison.md +363 -0
- package/blog/ja/next-i18next_vs_next-intl_vs_intlayer.md +1150 -76
- package/blog/ja/nextjs-multilingual-seo-comparison.md +362 -0
- package/blog/ko/next-i18next_vs_next-intl_vs_intlayer.md +1139 -73
- package/blog/ko/nextjs-multilingual-seo-comparison.md +362 -0
- package/blog/pt/next-i18next_vs_next-intl_vs_intlayer.md +1143 -76
- package/blog/pt/nextjs-multilingual-seo-comparison.md +362 -0
- package/blog/ru/next-i18next_vs_next-intl_vs_intlayer.md +1150 -74
- package/blog/ru/nextjs-multilingual-seo-comparison.md +370 -0
- package/blog/tr/next-i18next_vs_next-intl_vs_intlayer.md +2 -0
- package/blog/tr/nextjs-multilingual-seo-comparison.md +362 -0
- package/blog/zh/next-i18next_vs_next-intl_vs_intlayer.md +1152 -75
- package/blog/zh/nextjs-multilingual-seo-comparison.md +394 -0
- package/dist/cjs/generated/blog.entry.cjs +16 -0
- package/dist/cjs/generated/blog.entry.cjs.map +1 -1
- package/dist/cjs/generated/docs.entry.cjs +16 -0
- package/dist/cjs/generated/docs.entry.cjs.map +1 -1
- package/dist/esm/generated/blog.entry.mjs +16 -0
- package/dist/esm/generated/blog.entry.mjs.map +1 -1
- package/dist/esm/generated/docs.entry.mjs +16 -0
- package/dist/esm/generated/docs.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/dist/types/generated/docs.entry.d.ts +1 -0
- package/dist/types/generated/docs.entry.d.ts.map +1 -1
- package/docs/ar/component_i18n.md +186 -0
- package/docs/ar/vs_code_extension.md +48 -109
- package/docs/de/component_i18n.md +186 -0
- package/docs/de/vs_code_extension.md +46 -107
- package/docs/en/component_i18n.md +186 -0
- package/docs/en/interest_of_intlayer.md +2 -2
- package/docs/en/intlayer_with_nextjs_14.md +18 -1
- package/docs/en/intlayer_with_nextjs_15.md +18 -1
- package/docs/en/vs_code_extension.md +24 -114
- package/docs/en-GB/component_i18n.md +186 -0
- package/docs/en-GB/vs_code_extension.md +42 -103
- package/docs/es/component_i18n.md +182 -0
- package/docs/es/vs_code_extension.md +53 -114
- package/docs/fr/component_i18n.md +186 -0
- package/docs/fr/vs_code_extension.md +50 -111
- package/docs/hi/component_i18n.md +186 -0
- package/docs/hi/vs_code_extension.md +49 -110
- package/docs/it/component_i18n.md +186 -0
- package/docs/it/vs_code_extension.md +50 -111
- package/docs/ja/component_i18n.md +186 -0
- package/docs/ja/vs_code_extension.md +50 -111
- package/docs/ko/component_i18n.md +186 -0
- package/docs/ko/vs_code_extension.md +48 -109
- package/docs/pt/component_i18n.md +186 -0
- package/docs/pt/vs_code_extension.md +46 -107
- package/docs/ru/component_i18n.md +186 -0
- package/docs/ru/vs_code_extension.md +48 -109
- package/docs/tr/component_i18n.md +186 -0
- package/docs/tr/vs_code_extension.md +54 -115
- package/docs/zh/component_i18n.md +186 -0
- package/docs/zh/vs_code_extension.md +51 -105
- package/package.json +11 -11
- package/src/generated/blog.entry.ts +16 -0
- package/src/generated/docs.entry.ts +16 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
createdAt: 2025-08-23
|
|
3
|
-
updatedAt: 2025-09-
|
|
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
|
+

|
|
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
|
|
51
|
-
| ---------------------- |
|
|
52
|
-
| `aymericzip/intlayer` | [](https://github.com/aymericzip/intlayer/stargazers) | [](https://github.com/aymericzip/intlayer/commits) | [](https://github.com/aymericzip/intlayer/commits) | April 2024 | [](https://www.npmjs.com/package/intlayer) | [](https://www.npmjs.com/package/intlayer) |
|
|
55
|
+
| `amannn/next-intl` | [](https://github.com/amannn/next-intl/stargazers) | [](https://github.com/amannn/next-intl/commits) | [](https://github.com/amannn/next-intl/commits) | Nov 2020 | [](https://www.npmjs.com/package/next-intl) | [](https://www.npmjs.com/package/next-intl) |
|
|
56
|
+
| `i18next/i18next` | [](https://github.com/i18next/i18next/stargazers) | [](https://github.com/i18next/i18next/commits) | [](https://github.com/i18next/i18next/commits) | Jan 2012 | [](https://www.npmjs.com/package/i18next) | [](https://www.npmjs.com/package/i18next) |
|
|
57
|
+
| `i18next/next-i18next` | [](https://github.com/i18next/next-i18next/stargazers) | [](https://github.com/i18next/next-i18next/commits) | [](https://github.com/i18next/next-i18next/commits) | Nov 2018 | [](https://www.npmjs.com/package/next-i18next) | [](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
|
|
168
|
-
|
|
|
169
|
-
|  |  |
|
|
172
|
+
| Optimized bundle | Bundle not optimized |
|
|
173
|
+
| ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
|
|
174
|
+
|  |  |
|
|
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
|
-
├──
|
|
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
|
-
├──
|
|
361
|
+
├── locales
|
|
362
|
+
│ ├── en
|
|
363
|
+
│ │ ├── common.json
|
|
364
|
+
│ │ └── about.json
|
|
365
|
+
│ └── fr
|
|
366
|
+
│ ├── common.json
|
|
367
|
+
│ └── about.json
|
|
201
368
|
├── app
|
|
202
|
-
│
|
|
369
|
+
│ ├── i18n
|
|
370
|
+
│ │ └── server.ts
|
|
371
|
+
│ └── [locale]
|
|
372
|
+
│ ├── layout.tsx
|
|
373
|
+
│ └── about.tsx
|
|
203
374
|
└── components
|
|
204
|
-
|
|
205
|
-
|
|
375
|
+
├── I18nProvider.tsx
|
|
376
|
+
├── ClientComponent.tsx
|
|
377
|
+
└── ServerComponent.tsx
|
|
206
378
|
```
|
|
207
379
|
|
|
208
380
|
</TabItem>
|
|
209
|
-
|
|
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
|
-
│
|
|
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
|
-
│ └──
|
|
243
|
-
│
|
|
244
|
-
│ └──
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
265
|
-
|
|
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
|
-
|
|
270
|
-
|
|
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
|
-
|
|
444
|
+
```ts fileName="i18n.config.ts"
|
|
445
|
+
export const locales = ["en", "fr"] as const;
|
|
446
|
+
export type Locale = (typeof locales)[number];
|
|
277
447
|
|
|
278
|
-
|
|
448
|
+
export const defaultLocale: Locale = "en";
|
|
279
449
|
|
|
280
|
-
|
|
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
|
-
|
|
283
|
-
|
|
284
|
-
|
|
454
|
+
export function localizedPath(locale: string, path: string) {
|
|
455
|
+
return locale === defaultLocale ? path : "/" + locale + path;
|
|
456
|
+
}
|
|
285
457
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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
|
-
|
|
295
|
-
|
|
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
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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
|
-
|
|
303
|
-
|
|
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
|
-
|
|
542
|
+
return <I18nextProvider i18n={i18n}>{children}</I18nextProvider>;
|
|
543
|
+
}
|
|
544
|
+
```
|
|
306
545
|
|
|
307
|
-
|
|
308
|
-
|
|
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
|
-
|
|
311
|
-
<TabItem label="next-i18next" value="next-i18next">
|
|
550
|
+
export const dynamicParams = false;
|
|
312
551
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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
|
-
|
|
323
|
-
|
|
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
|
|
567
|
+
const dir = isRtl(locale) ? "rtl" : "ltr";
|
|
326
568
|
|
|
327
|
-
|
|
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
|
|
331
|
-
import
|
|
332
|
-
import {
|
|
333
|
-
import {
|
|
334
|
-
import
|
|
335
|
-
import
|
|
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
|
-
|
|
339
|
-
|
|
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
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
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
|
|
594
|
+
const i18n = await initI18next(locale, [...namespaces]);
|
|
595
|
+
const tAbout = i18n.getFixedT(locale, "about");
|
|
353
596
|
|
|
354
597
|
return (
|
|
355
|
-
<
|
|
598
|
+
<I18nProvider locale={locale} namespaces={[...namespaces]}>
|
|
356
599
|
<main>
|
|
357
|
-
<h1>{
|
|
600
|
+
<h1>{tAbout("title")}</h1>
|
|
601
|
+
|
|
358
602
|
<ClientComponent />
|
|
359
|
-
<ServerComponent />
|
|
603
|
+
<ServerComponent t={tAbout} locale={locale} count={0} />
|
|
360
604
|
</main>
|
|
361
|
-
</
|
|
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
|
-
|
|
383
|
-
const
|
|
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:
|
|
634
|
+
messages: await loadMessages(locale),
|
|
391
635
|
};
|
|
392
636
|
});
|
|
393
637
|
```
|
|
394
638
|
|
|
395
|
-
```tsx fileName="src/app/[locale]/
|
|
396
|
-
import {
|
|
397
|
-
import {
|
|
398
|
-
import
|
|
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:
|
|
405
|
-
params: { locale: string }
|
|
657
|
+
children: ReactNode;
|
|
658
|
+
params: Promise<{ locale: string }>;
|
|
406
659
|
}) {
|
|
407
|
-
const { locale } = params;
|
|
408
|
-
|
|
409
|
-
//
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
const
|
|
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 {
|
|
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
|
|
684
|
+
export default async function AboutPage({
|
|
431
685
|
params,
|
|
432
686
|
}: {
|
|
433
|
-
params: { locale: string }
|
|
687
|
+
params: Promise<{ locale: string }>;
|
|
434
688
|
}) {
|
|
435
|
-
|
|
436
|
-
|
|
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
|
-
<
|
|
440
|
-
<
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
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
|
-
|
|
722
|
+
import { type IntlayerConfig, Locales } from "intlayer";
|
|
723
|
+
|
|
724
|
+
const config: IntlayerConfig = {
|
|
453
725
|
internationalization: {
|
|
454
|
-
locales: [
|
|
455
|
-
defaultLocale:
|
|
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
|
|
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
|
-
<
|
|
476
|
-
{
|
|
477
|
-
|
|
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
|
-
|
|
790
|
+
All three support per-locale content loading and providers.
|
|
515
791
|
|
|
516
|
-
|
|
792
|
+
- With **next-intl/next-i18next**, you typically load selected messages/namespaces per route and place providers where needed.
|
|
517
793
|
|
|
518
|
-
|
|
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
|
-
|
|
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 (
|
|
805
|
+
**Translations (one JSON per namespace under `src/locales/...`)**
|
|
530
806
|
|
|
531
|
-
```json fileName="
|
|
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="
|
|
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/
|
|
831
|
+
```tsx fileName="src/components/ClientComponent.tsx"
|
|
552
832
|
"use client";
|
|
553
833
|
|
|
554
|
-
import React, {
|
|
555
|
-
import { useTranslation } from "
|
|
834
|
+
import React, { useState } from "react";
|
|
835
|
+
import { useTranslation } from "react-i18next";
|
|
556
836
|
|
|
557
|
-
|
|
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((
|
|
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
|
-
>
|
|
579
|
-
>
|
|
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
|
-
|
|
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
|
-
|
|
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/
|
|
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
|
-
|
|
988
|
+
locale: string;
|
|
989
|
+
count: number;
|
|
714
990
|
};
|
|
715
991
|
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
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>{
|
|
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
|
-
|
|
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
|
-
|
|
1010
|
+
type ServerComponentProps = {
|
|
1011
|
+
formattedCount: string;
|
|
1012
|
+
label: string;
|
|
1013
|
+
increment: string;
|
|
1014
|
+
};
|
|
740
1015
|
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
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>{
|
|
753
|
-
<button aria-label={
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
836
|
-
const messages = (
|
|
837
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1354
|
+
const PUBLIC_FILE = /\.[^/]+$/; // exclude files with extensions
|
|
1097
1355
|
|
|
1098
|
-
|
|
1356
|
+
export function middleware(request: NextRequest) {
|
|
1357
|
+
const { pathname } = request.nextUrl;
|
|
1099
1358
|
|
|
1100
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1387
|
+
</TabItem>
|
|
1388
|
+
<TabItem label="next-intl" value="next-intl">
|
|
1107
1389
|
|
|
1108
|
-
|
|
1390
|
+
Add a middleware to handle locale detection and routing:
|
|
1109
1391
|
|
|
1110
|
-
|
|
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
|
-
|
|
1402
|
+
export const config = {
|
|
1403
|
+
// Skip API, Next internals and static assets
|
|
1404
|
+
matcher: ["/((?!api|_next|.*\\..*).*)"],
|
|
1405
|
+
};
|
|
1406
|
+
```
|
|
1115
1407
|
|
|
1116
|
-
|
|
1117
|
-
<
|
|
1408
|
+
</TabItem>
|
|
1409
|
+
<TabItem label="intlayer" value="intlayer">
|
|
1118
1410
|
|
|
1119
|
-
|
|
1411
|
+
Intlayer provides built-in middleware handling through the `next-intlayer` package configuration.
|
|
1120
1412
|
|
|
1121
|
-
|
|
1413
|
+
</TabItem>
|
|
1414
|
+
</Tab>
|
|
1122
1415
|
|
|
1123
|
-
|
|
1124
|
-
<Column>
|
|
1416
|
+
### Setup checklist and good practices
|
|
1125
1417
|
|
|
1126
|
-
|
|
1418
|
+
<Tab defaultTab="next-intl" group='techno'>
|
|
1419
|
+
<TabItem label="next-i18next" value="next-i18next">
|
|
1127
1420
|
|
|
1128
|
-
-
|
|
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
|
-
</
|
|
1131
|
-
<
|
|
1429
|
+
</TabItem>
|
|
1430
|
+
<TabItem label="next-intl" value="next-intl">
|
|
1132
1431
|
|
|
1133
|
-
**
|
|
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
|
-
|
|
1438
|
+
</TabItem>
|
|
1439
|
+
<TabItem label="intlayer" value="intlayer">
|
|
1136
1440
|
|
|
1137
|
-
|
|
1138
|
-
|
|
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
|
-
|
|
1446
|
+
</TabItem>
|
|
1447
|
+
</Tab>
|
|
1141
1448
|
|
|
1142
1449
|
---
|
|
1143
1450
|
|
|
1144
|
-
##
|
|
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
|
-
|
|
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
|
-
-
|
|
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.
|