@intlayer/docs 6.1.4 → 6.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/blog/ar/next-i18next_vs_next-intl_vs_intlayer.md +1135 -75
  2. package/blog/ar/nextjs-multilingual-seo-comparison.md +364 -0
  3. package/blog/de/next-i18next_vs_next-intl_vs_intlayer.md +1139 -72
  4. package/blog/de/nextjs-multilingual-seo-comparison.md +362 -0
  5. package/blog/en/next-i18next_vs_next-intl_vs_intlayer.md +224 -240
  6. package/blog/en/nextjs-multilingual-seo-comparison.md +360 -0
  7. package/blog/en-GB/next-i18next_vs_next-intl_vs_intlayer.md +1134 -37
  8. package/blog/en-GB/nextjs-multilingual-seo-comparison.md +360 -0
  9. package/blog/es/next-i18next_vs_next-intl_vs_intlayer.md +1122 -64
  10. package/blog/es/nextjs-multilingual-seo-comparison.md +363 -0
  11. package/blog/fr/next-i18next_vs_next-intl_vs_intlayer.md +1132 -75
  12. package/blog/fr/nextjs-multilingual-seo-comparison.md +362 -0
  13. package/blog/hi/nextjs-multilingual-seo-comparison.md +363 -0
  14. package/blog/it/next-i18next_vs_next-intl_vs_intlayer.md +1120 -55
  15. package/blog/it/nextjs-multilingual-seo-comparison.md +363 -0
  16. package/blog/ja/next-i18next_vs_next-intl_vs_intlayer.md +1140 -76
  17. package/blog/ja/nextjs-multilingual-seo-comparison.md +362 -0
  18. package/blog/ko/next-i18next_vs_next-intl_vs_intlayer.md +1129 -73
  19. package/blog/ko/nextjs-multilingual-seo-comparison.md +362 -0
  20. package/blog/pt/next-i18next_vs_next-intl_vs_intlayer.md +1133 -76
  21. package/blog/pt/nextjs-multilingual-seo-comparison.md +362 -0
  22. package/blog/ru/next-i18next_vs_next-intl_vs_intlayer.md +1142 -74
  23. package/blog/ru/nextjs-multilingual-seo-comparison.md +370 -0
  24. package/blog/tr/nextjs-multilingual-seo-comparison.md +362 -0
  25. package/blog/zh/next-i18next_vs_next-intl_vs_intlayer.md +1142 -75
  26. package/blog/zh/nextjs-multilingual-seo-comparison.md +394 -0
  27. package/dist/cjs/generated/blog.entry.cjs +16 -0
  28. package/dist/cjs/generated/blog.entry.cjs.map +1 -1
  29. package/dist/esm/generated/blog.entry.mjs +16 -0
  30. package/dist/esm/generated/blog.entry.mjs.map +1 -1
  31. package/dist/types/generated/blog.entry.d.ts +1 -0
  32. package/dist/types/generated/blog.entry.d.ts.map +1 -1
  33. package/docs/en/interest_of_intlayer.md +2 -2
  34. package/package.json +10 -10
  35. package/src/generated/blog.entry.ts +16 -0
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  createdAt: 2025-08-23
3
- updatedAt: 2025-08-23
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 internationalisation (i18n) of a Next.js app
6
6
  keywords:
@@ -19,7 +19,10 @@ slugs:
19
19
 
20
20
  # next-i18next VS next-intl VS intlayer | Next.js Internationalisation (i18n)
21
21
 
22
- This guide compares three widely used i18n options for **Next.js**: **next-intl**, **next-i18next**, and **Intlayer**.
22
+ Let’s take a look into the similarities and differences between three i18n options for Next.js: next-i18next, next-intl, and Intlayer.
23
+
24
+ This is not a full tutorial. It’s a comparison to help you pick.
25
+
23
26
  We focus on **Next.js 13+ App Router** (with **React Server Components**) and evaluate:
24
27
 
25
28
  1. **Architecture & content organisation**
@@ -32,16 +35,29 @@ We focus on **Next.js 13+ App Router** (with **React Server Components**) and ev
32
35
 
33
36
  > **tl;dr**: All three can localise a Next.js app. If you want **component-scoped content**, **strict TypeScript types**, **build-time missing-key checks**, **tree-shaken dictionaries**, and **first-class App Router + SEO helpers**, **Intlayer** is the most complete, modern choice.
34
37
 
38
+ > One confusion often made by developers is to think that `next-intl` is the Next.js version of `react-intl`. It's not—`next-intl` is maintained by [Amann](https://github.com/amannn), while `react-intl` is maintained by [FormatJS](https://github.com/formatjs/formatjs).
39
+
35
40
  ---
36
41
 
37
- ## High-level positioning
42
+ ## In short
38
43
 
39
44
  - **next-intl** - Lightweight, straightforward message formatting with solid Next.js support. Centralised catalogues are common; DX is simple, but safety and large-scale maintenance remain mostly your responsibility.
40
- - **next-i18next** - i18next in Next.js attire. Mature ecosystem and features via plugins (e.g., ICU), but configuration can be verbose and catalogues tend to centralise as projects grow.
45
+ - **next-i18next** - i18next in Next.js clothing. Mature ecosystem and features via plugins (e.g., ICU), but configuration can be verbose and catalogues tend to centralise as projects grow.
41
46
  - **Intlayer** - Component-centric content model for Next.js, **strict TS typing**, **build-time checks**, **tree-shaking**, **built-in middleware & SEO helpers**, optional **Visual Editor/CMS**, and **AI-assisted translations**.
42
47
 
43
48
  ---
44
49
 
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=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) |
53
+ | `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) |
54
+ | `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) |
55
+ | `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
+
57
+ > Badges update automatically. Snapshots will vary over time.
58
+
59
+ ---
60
+
45
61
  ## Side-by-Side Feature Comparison (Next.js focused)
46
62
 
47
63
  | Feature | `next-intlayer` (Intlayer) | `next-intl` | `next-i18next` |
@@ -62,83 +78,1164 @@ We focus on **Next.js 13+ App Router** (with **React Server Components**) and ev
62
78
  | **Ecosystem / Community** | ⚠️ Smaller but growing fast and reactive | ✅ Good | ✅ Good |
63
79
  | **Server-side Rendering & Server Components** | ✅ Yes, streamlined for SSR / React Server Components | ⚠️ Supported at page level but need to pass t-functions on component tree for children server components | ⚠️ Supported at page level but need to pass t-functions on component tree for children server components |
64
80
  | **Tree-shaking (load only used content)** | ✅ Yes, per-component at build time via Babel/SWC plugins | ⚠️ Partial | ⚠️ Partial |
65
- | **Lazy loading** | ✅ Yes, per-locale / per-dictionary | ✅ Yes (per-route/per-locale), need namespace management | ✅ Yes (per-route/per-locale), need namespace management |
81
+ | **Lazy loading** | ✅ Yes, per-locale / per-dictionary | ✅ Yes (per-route/per-locale), requires namespace management | ✅ Yes (per-route/per-locale), requires namespace management |
66
82
  | **Purge unused content** | ✅ Yes, per-dictionary at build time | ❌ No, can be managed manually with namespace management | ❌ No, can be managed manually with namespace management |
67
- | **Management of Large Projects** | ✅ Encourages modular, suited for design-system | ✅ Modular with setup | ✅ Modular with setup |
83
+ | **Management of Large Projects** | ✅ Encourages modular, suited for design system | ✅ Modular with setup | ✅ Modular with setup |
84
+ | **Testing Missing Translations (CLI/CI)** | ✅ CLI: `npx intlayer content test` (CI-friendly audit) | ⚠️ Not built-in; docs suggest `npx @lingual/i18n-check` | ⚠️ Not built-in; rely on i18next tools / runtime `saveMissing` |
68
85
 
69
86
  ---
70
87
 
71
- ## Deep-dive comparison
88
+ ## Introduction
89
+
90
+ Next.js provides built-in support for internationalised routing (e.g. locale segments). However, that feature does not perform translations on its own. You still need a library to render localised content to your users.
72
91
 
73
- ### 1) Architecture & scalability
92
+ Many i18n libraries exist, but in the Next.js ecosystem today, three are gaining traction: next-i18next, next-intl, and Intlayer.
93
+
94
+ ---
74
95
 
75
- - **next-intl / next-i18next**: Default to **centralised catalogues** per locale (plus **namespaces** in i18next). Works fine early on, but often becomes a big shared surface area with rising coupling and key churn.
76
- - **Intlayer**: Encourages **per-component** (or per-feature) dictionaries **co-located** with the code they serve. This lowers cognitive load, eases duplication/migration of UI pieces, and reduces cross-team conflicts. Unused content is naturally easier to spot and purge.
96
+ ## Architecture & scalability
97
+
98
+ - **next-intl / next-i18next**: Default to **centralised catalogues** per locale (plus **namespaces** in i18next). Works fine early on, but often becomes a large shared surface area with increasing coupling and key churn.
99
+ - **Intlayer**: Encourages **per-component** (or per-feature) dictionaries **co-located** with the code they serve. This reduces cognitive load, facilitates duplication/migration of UI pieces, and minimises cross-team conflicts. Unused content is naturally easier to identify and remove.
77
100
 
78
101
  **Why it matters:** In large codebases or design-system setups, **modular content** scales better than monolithic catalogues.
79
102
 
80
103
  ---
81
104
 
82
- ### 2) TypeScript & safety
105
+ ## Bundle sizes & dependencies
106
+
107
+ After building the application, the bundle is the JavaScript that the browser will load to render the page. Bundle size is therefore important for application performance.
108
+
109
+ Two components are important in the context of a multi-language application bundle:
110
+
111
+ - The application code
112
+ - The content loaded by the browser
113
+
114
+ ## Application Code
115
+
116
+ The significance of application code is minimal in this case. All three solutions are tree-shakable, meaning that unused parts of the code are not included in the bundle.
117
+
118
+ Here is a comparison of the JavaScript bundle size loaded by the browser for a multi-language application with the three solutions.
119
+
120
+ If we do not require any formatter in the application, the list of exported functions after tree-shaking will be:
121
+
122
+ - **next-intlayer**: `useIntlayer`, `useLocale`, `NextIntlClientProvider`, (Bundle size is 180.6 kB -> 78.6 kB (gzip))
123
+ - **next-intl**: `useTranslations`, `useLocale`, `NextIntlClientProvider`, (Bundle size is 101.3 kB -> 31.4 kB (gzip))
124
+ - **next-i18next**: `useTranslation`, `useI18n`, `I18nextProvider`, (Bundle size is 80.7 kB -> 25.5 kB (gzip))
125
+
126
+ These functions are merely wrappers around React context/state, so the overall impact of the i18n library on bundle size is minimal.
127
+
128
+ > Intlayer is slightly larger than `next-intl` and `next-i18next` because it incorporates more logic in the `useIntlayer` function. This is related to markdown and `intlayer-editor` integration.
129
+
130
+ ## Content and Translations
131
+
132
+ This part is often ignored by developers, but let us consider the case of an application composed of 10 pages in 10 languages. Let us assume that each page integrates 100% unique content to simplify the calculation (in reality, much content is redundant between pages, e.g., page title, header, footer, etc.).
133
+
134
+ A user wanting to visit the `/fr/about` page will load the content of one page in a given language. Ignoring content optimisation would mean loading 8,200% `((1 + (((10 pages - 1) × (10 languages - 1)))) × 100)` of the application content unnecessarily. Do you see the problem? Even if this content remains text, and while you probably prefer to think about optimising your site's images, you are sending useless content across the globe and making users' computers process it for nothing.
135
+
136
+ Two important issues:
137
+
138
+ - **Splitting by route:**
139
+
140
+ > If I'm on the `/about` page, I don't want to load the content of the `/home` page
141
+
142
+ - **Splitting by locale:**
143
+
144
+ > If I'm on the `/fr/about` page, I don't want to load the content of the `/en/about` page
145
+
146
+ Again, all three solutions are aware of these issues and allow managing these optimisations. The difference between the three solutions is the DX (Developer Experience).
147
+
148
+ `next-intl` and `next-i18next` use a centralised approach to manage translations, allowing splitting JSON by locale and by sub-files. In `next-i18next`, we call the JSON files 'namespaces'; `next-intl` allows declaring messages. In `intlayer`, we call the JSON files 'dictionaries'.
149
+
150
+ - In the case of `next-intl`, like `next-i18next`, content is loaded at the page/layout level, then this content is loaded into a context provider. This means the developer must manually manage the JSON files that will be loaded for each page.
151
+
152
+ > In practice, this implies that developers often skip this optimisation, preferring to load all content in the page's context provider for simplicity.
153
+
154
+ - In the case of `intlayer`, all content is loaded in the application. Then a plugin (`@intlayer/babel` / `@intlayer/swc`) takes care of optimising the bundle by loading only the content used on the page. The developer therefore doesn't need to manually manage the dictionaries that will be loaded. This allows better optimisation, better maintainability, and reduces development time.
155
+
156
+ As the application grows (especially when multiple developers work on the application), it is common to forget to remove content that is no longer used from JSON files.
157
+
158
+ > Note that all JSON is loaded in all cases (next-intl, next-i18next, intlayer).
159
+
160
+ This is why Intlayer's approach is more performant: if a component is no longer used, its dictionary is not loaded in the bundle.
83
161
 
84
- - **next-intl**: Solid TypeScript support, but **keys aren’t strictly typed by default**; you’ll maintain safety patterns manually.
85
- - **next-i18next**: Base typings for hooks; **strict key typing requires extra tooling/config**.
86
- - **Intlayer**: **Generates strict types** from your content. **IDE autocompletion** and **compile-time errors** catch typos and missing keys before deployment.
162
+ How the library handles fallbacks is also important. Let us consider that the application is in English by default, and the user visits the `/fr/about` page. If translations are missing in French, we will consider the English fallback.
163
+
164
+ 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.**
165
+
166
+ Here is an example of the impact of bundle size optimisation using `intlayer` in a vite + react application:
167
+
168
+ | Optimised bundle | Bundle not optimised |
169
+ | -------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
170
+ | ![optimised bundle](https://github.com/aymericzip/intlayer/blob/main/docs/assets/bundle.png) | ![no optimised bundle](https://github.com/aymericzip/intlayer/blob/main/docs/assets/bundle_no_optimization.png) |
171
+
172
+ ---
173
+
174
+ ## TypeScript & safety
175
+
176
+ <Columns>
177
+ <Column>
178
+
179
+ **next-intl**
180
+
181
+ - Solid TypeScript support, but **keys aren’t strictly typed by default**; you’ll maintain safety patterns manually.
182
+
183
+ </Column>
184
+ <Column>
185
+
186
+ **next-i18next**
187
+
188
+ - Base typings for hooks; **strict key typing requires extra tooling/config**.
189
+
190
+ </Column>
191
+ <Column>
192
+
193
+ **intlayer**
194
+
195
+ - **Generates strict types** from your content. **IDE autocompletion** and **compile-time errors** catch typos and missing keys before deployment.
196
+
197
+ </Column>
198
+ </Columns>
199
+
200
+ **Why it matters:** Strong typing shifts failures **left** (CI/build) instead of **right** (runtime).
201
+
202
+ ---
203
+
204
+ ## Missing translation handling
205
+
206
+ **next-intl**
207
+
208
+ - Relies on **runtime fallbacks** (e.g., show the key or default locale). Build doesn’t fail.
209
+
210
+ **next-i18next**
211
+
212
+ - Relies on **runtime fallbacks** (e.g., show the key or default locale). Build doesn’t fail.
213
+
214
+ **intlayer**
215
+
216
+ - **Build-time detection** with **warnings/errors** for missing locales or keys.
217
+
218
+ **Why it matters:** Catching gaps during build prevents “mystery strings” in production and aligns with strict release gates.
219
+
220
+ ---
221
+
222
+ ## Routing, middleware & URL strategy
223
+
224
+ - **Generates strict types** from your content. **IDE autocompletion** and **compile-time errors** catch typos and missing keys before deployment.
225
+
226
+ </Column>
227
+ </Columns>
87
228
 
88
229
  **Why it matters:** Strong typing shifts failures **left** (CI/build) instead of **right** (runtime).
89
230
 
90
231
  ---
91
232
 
92
- ### 3) Missing translation handling
233
+ ## Missing translation handling
234
+
235
+ **next-intl**
236
+
237
+ - Relies on **runtime fallbacks** (e.g., show the key or default locale). Build does not fail.
93
238
 
94
- - **next-intl / next-i18next**: Rely on **runtime fallbacks** (e.g., show the key or default locale). Build doesn’t fail.
95
- - **Intlayer**: **Build-time detection** with **warnings/errors** for missing locales or keys.
239
+ **next-i18next**
240
+
241
+ - Relies on **runtime fallbacks** (e.g., show the key or default locale). Build does not fail.
242
+
243
+ **intlayer**
244
+
245
+ - **Build-time detection** with **warnings/errors** for missing locales or keys.
96
246
 
97
247
  **Why it matters:** Catching gaps during build prevents “mystery strings” in production and aligns with strict release gates.
98
248
 
99
249
  ---
100
250
 
101
- ### 4) Routing, middleware & URL strategy
251
+ ## Routing, middleware & URL strategy
252
+
253
+ <Columns>
254
+ <Column>
255
+
256
+ **next-intl**
257
+
258
+ - Works with **Next.js localised routing** on the App Router.
259
+
260
+ </Column>
261
+ <Column>
262
+
263
+ **next-i18next**
264
+
265
+ - Works with **Next.js localised routing** on the App Router.
266
+
267
+ </Column>
268
+ <Column>
102
269
 
103
- - All three work with **Next.js localised routing** on the App Router.
104
- - **Intlayer** goes further with **i18n middleware** (locale detection via headers/cookies) and **helpers** to generate localised URLs and `<link rel="alternate" hreflang="…">` tags.
270
+ **intlayer**
271
+
272
+ - All of the above, plus **i18n middleware** (locale detection via headers/cookies) and **helpers** to generate localised URLs and `<link rel="alternate" hreflang="…">` tags.
273
+
274
+ </Column>
275
+ </Columns>
105
276
 
106
277
  **Why it matters:** Fewer custom glue layers; **consistent UX** and **clean SEO** across locales.
107
278
 
108
279
  ---
109
280
 
110
- ### 5) Server Components (RSC) alignment
281
+ ## Server Components (RSC) alignment
282
+
283
+ <Columns>
284
+ <Column>
285
+
286
+ **next-intl**
287
+
288
+ - Supports Next.js 13+. Often requires passing t-functions/formatters through component trees in hybrid setups.
289
+
290
+ </Column>
291
+ <Column>
292
+
293
+ **next-i18next**
294
+
295
+ - Supports Next.js 13+. Similar constraints with passing translation utilities across boundaries.
296
+
297
+ </Column>
298
+ <Column>
111
299
 
112
- - **All** support Next.js 13+.
113
- - **Intlayer** smooths the **server/client boundary** with a consistent API and providers designed for RSC, so you don’t shuttle formatters or t-functions through component trees.
300
+ **intlayer**
301
+
302
+ - Supports Next.js 13+ and smooths the **server/client boundary** with a consistent API and RSC-oriented providers, avoiding shuttling formatters or t-functions.
303
+
304
+ </Column>
305
+ </Columns>
114
306
 
115
307
  **Why it matters:** Cleaner mental model and fewer edge cases in hybrid trees.
116
308
 
117
309
  ---
118
310
 
119
- ### 6) Performance & loading behaviour
311
+ ## DX, tooling & maintenance
120
312
 
121
- - **next-intl / next-i18next**: Partial control via **namespaces** and **route-level splits**; risk of bundling unused strings if discipline slips.
122
- - **Intlayer**: **Tree-shakes** at build and **lazy-loads per dictionary/locale**. Unused content doesn’t ship.
313
+ <Columns>
314
+ <Column>
123
315
 
124
- **Why it matters:** Smaller bundles and faster start-up, especially on multi-locale sites.
316
+ **next-intl**
125
317
 
126
- ---
318
+ - Commonly paired with external localisation platforms and editorial workflows.
319
+
320
+ </Column>
321
+ <Column>
322
+
323
+ **next-i18next**
127
324
 
128
- ### 7) DX, tooling & maintenance
325
+ - Commonly paired with external localisation platforms and editorial workflows.
129
326
 
130
- - **next-intl / next-i18next**: You’ll typically wire up external platforms for translations and editorial workflows.
131
- - **Intlayer**: Ships a **free Visual Editor** and **optional CMS** (Git-friendly or externalised). Plus **VSCode extension** for content authoring and **AI-assisted translations** using your own provider keys.
327
+ </Column>
328
+ <Column>
329
+
330
+ **intlayer**
331
+
332
+ - Ships a **free Visual Editor** and **optional CMS** (Git-friendly or externalised), plus a **VSCode extension** and **AI-assisted translations** using your own provider keys.
333
+
334
+ </Column>
335
+ </Columns>
132
336
 
133
337
  **Why it matters:** Lowers operational costs and shortens the feedback loop between developers and content authors.
134
338
 
339
+ ## Integration with localisation platforms (TMS)
340
+
341
+ Large organisations often rely on Translation Management Systems (TMS) like **Crowdin**, **Phrase**, **Lokalise**, **Localizely**, or **Localazy**.
342
+
343
+ - **Why companies care**
344
+ - **Collaboration & roles**: Multiple actors are involved: developers, product managers, translators, reviewers, marketing teams.
345
+ - **Scale & efficiency**: continuous localisation, in‑context review.
346
+
347
+ - **next-intl / next-i18next**
348
+ - Typically use **centralised JSON catalogues**, so export/import with TMS is straightforward.
349
+ - Mature ecosystems and examples/integrations for the platforms above.
350
+
351
+ - **Intlayer**
352
+ - Encourages **decentralised, per-component dictionaries** and supports **TypeScript/TSX/JS/JSON/MD** content.
353
+ - This improves modularity in code, but can make plug‑and‑play TMS integration harder when a tool expects centralised, flat JSON files.
354
+ - Intlayer provides alternatives: **AI‑assisted translations** (using your own provider keys), a **Visual Editor/CMS**, and **CLI/CI** workflows to catch and prefill gaps.
355
+
356
+ > Note: `next-intl` and `i18next` also accept TypeScript catalogues. If your team stores messages in `.ts` files or decentralises them by feature, you may encounter similar TMS friction. However, many `next-intl` setups remain centralised in a `locales/` folder, which is somewhat easier to refactor to JSON for TMS.
357
+
358
+ ## Developer Experience
359
+
360
+ This section provides a detailed comparison between the three solutions. Rather than considering simple cases, as described in the 'getting started' documentation for each solution, we will examine a real use case, more akin to an actual project.
361
+
362
+ ### App structure
363
+
364
+ The app structure is important to ensure good maintainability of your codebase.
365
+
366
+ <Tab defaultTab="next-intl" group='techno'>
367
+
368
+ <TabItem label="next-i18next" value="next-i18next">
369
+
370
+ ```bash
371
+ .
372
+ ├── public
373
+ │ └── locales
374
+ │ ├── en
375
+ │ │ ├── home.json
376
+ │ │ └── navbar.json
377
+ │ ├── fr
378
+ │ │ ├── home.json
379
+ │ │ └── navbar.json
380
+ │ └── es
381
+ │ ├── home.json
382
+ │ └── navbar.json
383
+ ├── next-i18next.config.js
384
+ └── src
385
+ ├── middleware.ts
386
+ ├── app
387
+ │ └── home.tsx
388
+ └── components
389
+ └── Navbar
390
+ └── index.tsx
391
+ ```
392
+
393
+ </TabItem>
394
+ <TabItem label="next-intl" value="next-intl">
395
+
396
+ ```bash
397
+ .
398
+ ├── locales
399
+ │ ├── en
400
+ │ │ ├── home.json
401
+ │ │ └── navbar.json
402
+ │ ├── fr
403
+ │ │ ├── home.json
404
+ │ │ └── navbar.json
405
+ │ └── es
406
+ │ ├── home.json
407
+ │ └── navbar.json
408
+ ├── i18n.ts
409
+ └── src
410
+ ├── middleware.ts
411
+ ├── app
412
+ │ └── home.tsx
413
+ └── components
414
+ └── Navbar
415
+ └── index.tsx
416
+ ```
417
+
418
+ </TabItem>
419
+ <TabItem label="intlayer" value="intlayer">
420
+
421
+ ```bash
422
+ .
423
+ ├── intlayer.config.ts
424
+ └── src
425
+ ├── middleware.ts
426
+ ├── app
427
+ │ └── home
428
+ │ └── index.tsx
429
+ │ └── index.content.ts
430
+ └── components
431
+ └── Navbar
432
+ ├── index.tsx
433
+ └── index.content.ts
434
+ ```
435
+
436
+ </TabItem>
437
+ </Tab>
438
+
439
+ #### Comparison
440
+
441
+ - **next-intl / next-i18next**: Centralised catalogues (JSON; namespaces/messages). Clear structure, integrates well with translation platforms, but can lead to more cross-file edits as apps grow.
442
+ - **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.
443
+
444
+ #### Setup and Loading Content
445
+
446
+ As mentioned previously, you must optimise how each JSON file is imported into your code.
447
+ How the library handles content loading is important.
448
+
449
+ <Tab defaultTab="next-intl" group='techno'>
450
+ <TabItem label="next-i18next" value="next-i18next">
451
+
452
+ ```tsx fileName="next-i18next.config.js"
453
+ module.exports = {
454
+ i18n: {
455
+ locales: ["en", "fr", "es"],
456
+ defaultLocale: "en",
457
+ },
458
+ };
459
+ ```
460
+
461
+ ```tsx fileName="src/app/_app.tsx"
462
+ import { appWithTranslation } from "next-i18next";
463
+
464
+ const MyApp = ({ Component, pageProps }) => <Component {...pageProps} />;
465
+
466
+ export default appWithTranslation(MyApp);
467
+ ```
468
+
469
+ ```tsx fileName="src/app/[locale]/about/page.tsx"
470
+ import type { GetStaticProps } from "next";
471
+ import { serverSideTranslations } from "next-i18next/serverSideTranslations";
472
+ import { useTranslation } from "next-i18next";
473
+ import { I18nextProvider, initReactI18next } from "react-i18next";
474
+ import { createInstance } from "i18next";
475
+ import { ClientComponent, ServerComponent } from "@components";
476
+
477
+ export default function HomePage({ locale }: { locale: string }) {
478
+ // Explicitly declare the namespace used by this component
479
+ const resources = await loadMessagesFor(locale); // your loader (JSON, etc.)
480
+
481
+ const i18n = createInstance();
482
+ i18n.use(initReactI18next).init({
483
+ lng: locale,
484
+ fallbackLng: "en",
485
+ resources,
486
+ ns: ["common", "about"],
487
+ defaultNS: "common",
488
+ interpolation: { escapeValue: false },
489
+ });
490
+
491
+ const { t } = useTranslation("about");
492
+
493
+ return (
494
+ <I18nextProvider i18n={i18n}>
495
+ <main>
496
+ <h1>{t("title")}</h1>
497
+ <ClientComponent />
498
+ <ServerComponent />
499
+ </main>
500
+ </I18nextProvider>
501
+ );
502
+ }
503
+
504
+ export const getStaticProps: GetStaticProps = async ({ locale }) => {
505
+ // Only preload the namespaces required for THIS page
506
+ return {
507
+ props: {
508
+ ...(await serverSideTranslations(locale ?? "en", ["common", "about"])),
509
+ },
510
+ };
511
+ };
512
+ ```
513
+
514
+ </TabItem>
515
+ <TabItem label="next-intl" value="next-intl">
516
+
517
+ ```tsx fileName="i18n.ts"
518
+ import { getRequestConfig } from "next-intl/server";
519
+ import { notFound } from "next/navigation";
520
+
521
+ // Can be imported from a shared config
522
+ const locales = ["en", "fr", "es"];
523
+
524
+ export default getRequestConfig(async ({ locale }) => {
525
+ // Validate that the incoming `locale` parameter is valid
526
+ if (!locales.includes(locale as any)) notFound();
527
+
528
+ return {
529
+ messages: (await import(`../messages/${locale}.json`)).default,
530
+ };
531
+ });
532
+ ```
533
+
534
+ ```tsx fileName="src/app/[locale]/about/layout.tsx"
535
+ import { NextIntlClientProvider } from "next-intl";
536
+ import { getMessages, unstable_setRequestLocale } from "next-intl/server";
537
+ import pick from "lodash/pick";
538
+
539
+ export default async function LocaleLayout({
540
+ children,
541
+ params,
542
+ }: {
543
+ children: React.ReactNode;
544
+ params: { locale: string };
545
+ }) {
546
+ const { locale } = params;
547
+
548
+ // Set the active request locale for this server render (RSC)
549
+ unstable_setRequestLocale(locale);
550
+
551
+ // Messages are loaded server-side via src/i18n/request.ts
552
+ // (see next-intl docs). Here we only send a subset to the client
553
+ // that is needed for client components (payload optimisation).
554
+ const messages = await getMessages();
555
+ const clientMessages = pick(messages, ["common", "about"]);
556
+
557
+ return (
558
+ <html lang={locale}>
559
+ <body>
560
+ <NextIntlClientProvider locale={locale} messages={clientMessages}>
561
+ {children}
562
+ </NextIntlClientProvider>
563
+ </body>
564
+ </html>
565
+ );
566
+ }
567
+ ```
568
+
569
+ ```tsx fileName="src/app/[locale]/about/page.tsx"
570
+ import { getTranslations } from "next-intl/server";
571
+ import { ClientComponent, ServerComponent } from "@components";
572
+
573
+ export default async function LandingPage({
574
+ params,
575
+ }: {
576
+ params: { locale: string };
577
+ }) {
578
+ // Strictly server-side loading (not hydrated on the client)
579
+ const t = await getTranslations("about");
580
+
581
+ return (
582
+ <main>
583
+ <h1>{t("title")}</h1>
584
+ <ClientComponent />
585
+ <ServerComponent />
586
+ </main>
587
+ );
588
+ }
589
+ ```
590
+
591
+ </TabItem>
592
+ <TabItem label="intlayer" value="intlayer">
593
+
594
+ ```tsx fileName="intlayer.config.ts"
595
+ export default {
596
+ internationalization: {
597
+ locales: ["en", "fr", "es"],
598
+ defaultLocale: "en",
599
+ },
600
+ };
601
+ ```
602
+
603
+ ```tsx fileName="src/app/[locale]/layout.tsx"
604
+ import { getHTMLTextDir } from "intlayer";
605
+ import {
606
+ IntlayerClientProvider,
607
+ generateStaticParams,
608
+ type NextLayoutIntlayer,
609
+ } from "next-intlayer";
610
+
611
+ export const dynamic = "force-static";
612
+
613
+ const LandingLayout: NextLayoutIntlayer = async ({ children, params }) => {
614
+ const { locale } = await params;
615
+
616
+ return (
617
+ <html lang={locale} dir={getHTMLTextDir(locale)}>
618
+ <IntlayerClientProvider locale={locale}>
619
+ {children}
620
+ </IntlayerClientProvider>
621
+ </html>
622
+ );
623
+ };
624
+
625
+ export default LandingLayout;
626
+ ```
627
+
628
+ ```tsx fileName="src/app/[locale]/about/page.tsx"
629
+ import { PageContent } from "@components/PageContent";
630
+ import type { NextPageIntlayer } from "next-intlayer";
631
+ import { IntlayerServerProvider, useIntlayer } from "next-intlayer/server";
632
+ import { ClientComponent, ServerComponent } from "@components";
633
+
634
+ const LandingPage: NextPageIntlayer = async ({ params }) => {
635
+ const { locale } = await params;
636
+ const { title } = useIntlayer("about", locale);
637
+
638
+ return (
639
+ <IntlayerServerProvider locale={locale}>
640
+ <main>
641
+ <h1>{title}</h1>
642
+ <ClientComponent />
643
+ <ServerComponent />
644
+ </main>
645
+ </IntlayerServerProvider>
646
+ );
647
+ };
648
+
649
+ export default LandingPage;
650
+ ```
651
+
652
+ </TabItem>
653
+ </Tab>
654
+
655
+ #### Comparison
656
+
657
+ All three support per-locale content loading and providers.
658
+
659
+ - With **next-intl/next-i18next**, you typically load selected messages/namespaces per route and place providers where needed.
660
+
661
+ - With **Intlayer**, it adds build-time analysis to infer usage, which can reduce manual wiring and may allow a single root provider.
662
+
663
+ Choose between explicit control and automation based on team preference.
664
+
665
+ ### Usage in a client component
666
+
667
+ Let's take an example of a client component rendering a counter.
668
+
669
+ <Tab defaultTab="next-intl" group='techno'>
670
+ <TabItem label="next-i18next" value="next-i18next">
671
+
672
+ **Translations (must be real JSON in `public/locales/...`)**
673
+
674
+ ```json fileName="public/locales/en/about.json"
675
+ {
676
+ "counter": {
677
+ "label": "Counter",
678
+ "increment": "Increment"
679
+ }
680
+ }
681
+ ```
682
+
683
+ ```json fileName="public/locales/fr/about.json"
684
+ {
685
+ "counter": {
686
+ "label": "Compteur",
687
+ "increment": "Incrémenter"
688
+ }
689
+ }
690
+ ```
691
+
692
+ **Client component**
693
+
694
+ ```tsx fileName="src/components/ClientComponentExample.tsx"
695
+ "use client";
696
+
697
+ import React, { useMemo, useState } from "react";
698
+ import { useTranslation } from "next-i18next";
699
+
700
+ const ClientComponentExample = () => {
701
+ const { t, i18n } = useTranslation("about");
702
+ const [count, setCount] = useState(0);
703
+
704
+ // next-i18next doesn't expose useNumber; use Intl.NumberFormat
705
+ const numberFormat = new Intl.NumberFormat(i18n.language);
706
+
707
+ return (
708
+ <div>
709
+ <p>{numberFormat.format(count)}</p>
710
+ <button
711
+ aria-label={t("counter.label")}
712
+ onClick={() => setCount((count) => count + 1)}
713
+ >
714
+ {t("counter.increment")}
715
+ </button>
716
+ </div>
717
+ );
718
+ };
719
+ ```
720
+
721
+ > Don't forget to add the "about" namespace on the page serverSideTranslations
722
+ > We are using React version 19.x.x here, but for earlier versions, you will need to use useMemo to store the instance of the formatter as it is a heavy function
723
+
724
+ </TabItem>
725
+ <TabItem label="next-intl" value="next-intl">
726
+
727
+ **Translations (shape reused; load them into next-intl messages as you prefer)**
728
+
729
+ ```json fileName="locales/en/about.json"
730
+ {
731
+ "counter": {
732
+ "label": "Counter",
733
+ "increment": "Increment"
734
+ }
735
+ }
736
+ ```
737
+
738
+ ```json fileName="locales/fr/about.json"
739
+ {
740
+ "counter": {
741
+ "label": "Compteur",
742
+ "increment": "Incrémenter"
743
+ }
744
+ }
745
+ ```
746
+
747
+ **Client component**
748
+
749
+ ```tsx fileName="src/components/ClientComponentExample.tsx"
750
+ "use client";
751
+
752
+ import React, { useState } from "react";
753
+ import { useTranslations, useFormatter } from "next-intl";
754
+
755
+ const ClientComponentExample = () => {
756
+ // Scope directly to the nested object
757
+ const t = useTranslations("about.counter");
758
+ const format = useFormatter();
759
+ const [count, setCount] = useState(0);
760
+
761
+ return (
762
+ <div>
763
+ <p>{format.number(count)}</p>
764
+ <button
765
+ aria-label={t("label")}
766
+ onClick={() => setCount((count) => count + 1)}
767
+ >
768
+ {t("increment")}
769
+ </button>
770
+ </div>
771
+ );
772
+ };
773
+ ```
774
+
775
+ > Don't forget to add the "about" message to the page client message
776
+
777
+ </TabItem>
778
+ <TabItem label="intlayer" value="intlayer">
779
+
780
+ **Content**
781
+
782
+ ```ts fileName="src/components/ClientComponentExample/index.content.ts"
783
+ import { t, type Dictionary } from "intlayer";
784
+
785
+ const counterContent = {
786
+ key: "counter",
787
+ content: {
788
+ label: t({ "en-GB": "Counter", en: "Counter", fr: "Compteur" }),
789
+ increment: t({ "en-GB": "Increment", en: "Increment", fr: "Incrémenter" }),
790
+ },
791
+ } satisfies Dictionary;
792
+
793
+ export default counterContent;
794
+ ```
795
+
796
+ **Client component**
797
+
798
+ ```tsx fileName="src/components/ClientComponentExample/index.tsx"
799
+ "use client";
800
+
801
+ import React, { useState } from "react";
802
+ import { useNumber, useIntlayer } from "next-intlayer";
803
+
804
+ const ClientComponentExample = () => {
805
+ const [count, setCount] = useState(0);
806
+ const { label, increment } = useIntlayer("counter"); // returns strings
807
+ const { number } = useNumber();
808
+
809
+ return (
810
+ <div>
811
+ <p>{number(count)}</p>
812
+ <button aria-label={label} onClick={() => setCount((count) => count + 1)}>
813
+ {increment}
814
+ </button>
815
+ </div>
816
+ );
817
+ };
818
+ ```
819
+
820
+ </TabItem>
821
+ </Tab>
822
+
823
+ #### Comparison
824
+
825
+ - **Number formatting**
826
+ - **next-i18next**: no `useNumber`; use `Intl.NumberFormat` (or i18next-icu).
827
+ - **next-intl**: `useFormatter().number(value)`.
828
+ - **Intlayer**: built-in `useNumber()`.
829
+
830
+ - **Keys**
831
+ - Maintain a nested structure (`about.counter.label`) and scope your hook accordingly (`useTranslation("about")` + `t("counter.label")` or `useTranslations("about.counter")` + `t("label")`).
832
+
833
+ - **File locations**
834
+ - **next-i18next** expects JSON in `public/locales/{lng}/{ns}.json`.
835
+ - **next-intl** is flexible; load messages however you configure.
836
+ - **Intlayer** stores content in TS/JS dictionaries and resolves by key.
837
+
838
+ ---
839
+
840
+ ### Usage in a server component
841
+
842
+ We will take the case of a UI component. This component is a server component, and should be able to be inserted as a child of a client component. (page (server component) -> client component -> server component). As this component can be inserted as a child of a client component, it cannot be async.
843
+
844
+ <Tab defaultTab="next-intl" group='techno'>
845
+ <TabItem label="next-i18next" value="next-i18next">
846
+
847
+ ```tsx fileName="src/pages/about.tsx"
848
+ import type { GetStaticProps } from "next";
849
+ import { useTranslation } from "next-i18next";
850
+
851
+ type ServerComponentProps = {
852
+ count: number;
853
+ };
854
+
855
+ const ServerComponent = ({ count }: ServerComponentProps) => {
856
+ const { t, i18n } = useTranslation("about");
857
+ const formatted = new Intl.NumberFormat(i18n.language).format(count);
858
+
859
+ return (
860
+ <div>
861
+ <p>{formatted}</p>
862
+ <button aria-label={t("counter.label")}>{t("counter.increment")}</button>
863
+ </div>
864
+ );
865
+ };
866
+ ```
867
+
868
+ </TabItem>
869
+ <TabItem label="next-intl" value="next-intl">
870
+
871
+ ```tsx fileName="src/components/ServerComponent.tsx"
872
+ type ServerComponentProps = {
873
+ count: number;
874
+ t: (key: string) => string;
875
+ };
876
+
877
+ const ServerComponent = ({ t, count }: ServerComponentProps) => {
878
+ const formatted = new Intl.NumberFormat(i18n.language).format(count);
879
+
880
+ return (
881
+ <div>
882
+ <p>{formatted}</p>
883
+ <button aria-label={t("label")}>{t("increment")}</button>
884
+ </div>
885
+ );
886
+ };
887
+ ```
888
+
889
+ > As the server component cannot be async, you need to pass the translations and formatter function as props.
890
+ >
891
+ > - `const t = await getTranslations("about.counter");`
892
+ > - `const format = await getFormatter();`
893
+
894
+ </TabItem>
895
+ <TabItem label="intlayer" value="intlayer">
896
+
897
+ ```tsx fileName="src/components/ServerComponent.tsx"
898
+ import { useIntlayer, useNumber } from "next-intlayer/server";
899
+
900
+ const ServerComponent = ({ count }: { count: number }) => {
901
+ const { label, increment } = useIntlayer("counter");
902
+ const { number } = useNumber();
903
+
904
+ return (
905
+ <div>
906
+ <p>{number(count)}</p>
907
+ <button aria-label={label}>{increment}</button>
908
+ </div>
909
+ );
910
+ };
911
+ );
912
+ };
913
+ ```
914
+
915
+ </TabItem>
916
+ </Tab>
917
+
918
+ > Intlayer exposes **server-safe** hooks via `next-intlayer/server`. To function, `useIntlayer` and `useNumber` use hook-like syntax, similar to the client hooks, but rely under the hood on the server context (`IntlayerServerProvider`).
919
+
920
+ ### Metadata / Sitemap / Robots
921
+
922
+ Translating content is excellent. However, people often forget that the main goal of internationalisation is to make your website more visible to the world. I18n is an incredible lever to enhance your website's visibility.
923
+
924
+ Here is a list of best practices regarding multilingual SEO.
925
+
926
+ - set hreflang meta tags in the `<head>` tag
927
+ > This helps search engines to understand which languages are available on the page
928
+ - list all page translations in the sitemap.xml using the `http://www.w3.org/1999/xhtml` XML schema
929
+ >
930
+ - do not forget to exclude prefixed pages from the robots.txt (e.g. `/dashboard`, and `/fr/dashboard`, `/es/dashboard`)
931
+ >
932
+ - use a custom Link component to redirect to the most localised page (e.g. in French `<a href="/fr/about">À propos</a>`)
933
+ >
934
+
935
+ Developers often forget to properly reference their pages across locales.
936
+
937
+ <Tab defaultTab="next-intl" group='techno'>
938
+
939
+ <TabItem label="next-i18next" value="next-i18next">
940
+
941
+ ```ts fileName="i18n.config.ts"
942
+ export const locales = ["en", "fr"] as const;
943
+ export type Locale = (typeof locales)[number];
944
+ export const defaultLocale: Locale = "en";
945
+
946
+ export function localizedPath(locale: string, path: string) {
947
+ return locale === defaultLocale ? path : "/" + locale + path;
948
+ }
949
+
950
+ const ORIGIN = "https://example.com";
951
+ export function abs(locale: string, path: string) {
952
+ return ORIGIN + localizedPath(locale, path);
953
+ }
954
+ ```
955
+
956
+ ```tsx fileName="src/app/[locale]/about/layout.tsx"
957
+ import type { Metadata } from "next";
958
+ import { locales, defaultLocale, localizedPath } from "@/i18n.config";
959
+
960
+ export async function generateMetadata({
961
+ params,
962
+ }: {
963
+ params: { locale: string };
964
+ }): Promise<Metadata> {
965
+ const { locale } = params;
966
+
967
+ // Dynamically import the correct JSON file
968
+ const messages = (
969
+ await import("@/../public/locales/" + locale + "/about.json")
970
+ ).default;
971
+
972
+ const languages = Object.fromEntries(
973
+ locales.map((locale) => [locale, localizedPath(locale, "/about")])
974
+ );
975
+
976
+ return {
977
+ title: messages.title,
978
+ description: messages.description,
979
+ alternates: {
980
+ canonical: localizedPath(locale, "/about"),
981
+ languages: { ...languages, "x-default": "/about" },
982
+ },
983
+ };
984
+ }
985
+
986
+ export default async function AboutPage() {
987
+ return <h1>About</h1>;
988
+ }
989
+ ```
990
+
991
+ ```ts fileName="src/app/sitemap.ts"
992
+ import type { MetadataRoute } from "next";
993
+ import { locales, defaultLocale, abs } from "@/i18n.config";
994
+
995
+ export default function sitemap(): MetadataRoute.Sitemap {
996
+ const languages = Object.fromEntries(
997
+ locales.map((locale) => [locale, abs(locale, "/about")])
998
+ );
999
+ return [
1000
+ {
1001
+ url: abs(defaultLocale, "/about"),
1002
+ lastModified: new Date(),
1003
+ changeFrequency: "monthly",
1004
+ priority: 0.7,
1005
+ alternates: { languages },
1006
+ },
1007
+ ];
1008
+ }
1009
+ ```
1010
+
1011
+ ```ts fileName="src/app/robots.ts"
1012
+ import type { MetadataRoute } from "next";
1013
+ import { locales, defaultLocale, localizedPath } from "@/i18n.config";
1014
+
1015
+ const ORIGIN = "https://example.com";
1016
+
1017
+ const expandAllLocales = (path: string) => [
1018
+ localizedPath(defaultLocale, path),
1019
+ ...locales
1020
+ .filter((locale) => locale !== defaultLocale)
1021
+ .map((locale) => localizedPath(locale, path)),
1022
+ ];
1023
+
1024
+ export default function robots(): MetadataRoute.Robots {
1025
+ const disallow = [
1026
+ ...expandAllLocales("/dashboard"),
1027
+ ...expandAllLocales("/admin"),
1028
+ ];
1029
+
1030
+ return {
1031
+ rules: { userAgent: "*", allow: ["/"], disallow },
1032
+ host: ORIGIN,
1033
+ sitemap: ORIGIN + "/sitemap.xml",
1034
+ };
1035
+ }
1036
+ ```
1037
+
1038
+ </TabItem>
1039
+ <TabItem label="next-intl" value="next-intl">
1040
+
1041
+ ```tsx fileName="src/app/[locale]/about/layout.tsx"
1042
+ import type { Metadata } from "next";
1043
+ import { locales, defaultLocale } from "@/i18n";
1044
+ import { getTranslations } from "next-intl/server";
1045
+
1046
+ function localizedPath(locale: string, path: string) {
1047
+ return locale === defaultLocale ? path : "/" + locale + path;
1048
+ }
1049
+
1050
+ export async function generateMetadata({
1051
+ params,
1052
+ }: {
1053
+ params: { locale: string };
1054
+ }): Promise<Metadata> {
1055
+ const { locale } = params;
1056
+ const t = await getTranslations({ locale, namespace: "about" });
1057
+
1058
+ const url = "/about";
1059
+ const languages = Object.fromEntries(
1060
+ locales.map((locale) => [locale, localizedPath(locale, url)])
1061
+ );
1062
+
1063
+ return {
1064
+ title: t("title"),
1065
+ description: t("description"),
1066
+ alternates: {
1067
+ canonical: localizedPath(locale, url),
1068
+ languages: { ...languages, "x-default": url },
1069
+ },
1070
+ };
1071
+ }
1072
+
1073
+ // ... Rest of the page code
1074
+ ```
1075
+
1076
+ ```tsx fileName="src/app/sitemap.ts"
1077
+ import type { MetadataRoute } from "next";
1078
+ import { locales, defaultLocale } from "@/i18n";
1079
+
1080
+ const origin = "https://example.com";
1081
+
1082
+ const formatterLocalizedPath = (locale: string, path: string) =>
1083
+ locale === defaultLocale ? origin + path : origin + "/" + locale + path;
1084
+
1085
+ export default function sitemap(): MetadataRoute.Sitemap {
1086
+ const aboutLanguages = Object.fromEntries(
1087
+ locales.map((l) => [l, formatterLocalizedPath(l, "/about")])
1088
+ );
1089
+
1090
+ return [
1091
+ {
1092
+ url: formatterLocalizedPath(defaultLocale, "/about"),
1093
+ lastModified: new Date(),
1094
+ changeFrequency: "monthly",
1095
+ priority: 0.7,
1096
+ alternates: { languages: aboutLanguages },
1097
+ },
1098
+ ];
1099
+ }
1100
+ ```
1101
+
1102
+ ```tsx fileName="src/app/robots.ts"
1103
+ import type { MetadataRoute } from "next";
1104
+ import { locales, defaultLocale } from "@/i18n";
1105
+
1106
+ const origin = "https://example.com";
1107
+ const withAllLocales = (path: string) => [
1108
+ path,
1109
+ ...locales
1110
+ .filter((locale) => locale !== defaultLocale)
1111
+ .map((locale) => "/" + locale + path),
1112
+ ];
1113
+
1114
+ export default function robots(): MetadataRoute.Robots {
1115
+ const disallow = [
1116
+ ...withAllLocales("/dashboard"),
1117
+ ...withAllLocales("/admin"),
1118
+ ];
1119
+
1120
+ return {
1121
+ rules: { userAgent: "*", allow: ["/"], disallow },
1122
+ host: origin,
1123
+ sitemap: origin + "/sitemap.xml",
1124
+ };
1125
+ }
1126
+ ```
1127
+
1128
+ </TabItem>
1129
+ <TabItem label="intlayer" value="intlayer">
1130
+
1131
+ ```typescript fileName="src/app/[locale]/about/layout.tsx"
1132
+ import { getIntlayer, getMultilingualUrls } from "intlayer";
1133
+ import type { Metadata } from "next";
1134
+ import type { LocalPromiseParams } from "next-intlayer";
1135
+
1136
+ export const generateMetadata = async ({
1137
+ params,
1138
+ }: LocalPromiseParams): Promise<Metadata> => {
1139
+ const { locale } = await params;
1140
+
1141
+ const metadata = getIntlayer("page-metadata", locale);
1142
+
1143
+ const multilingualUrls = getMultilingualUrls("/about");
1144
+
1145
+ return {
1146
+ ...metadata,
1147
+ alternates: {
1148
+ canonical: multilingualUrls[locale as keyof typeof multilingualUrls],
1149
+ languages: { ...multilingualUrls, "x-default": "/about" },
1150
+ },
1151
+ };
1152
+ };
1153
+
1154
+ // ... Rest of the page code
1155
+ ```
1156
+
1157
+ ```tsx fileName="src/app/sitemap.ts"
1158
+ import { getMultilingualUrls } from "intlayer";
1159
+ import type { MetadataRoute } from "next";
1160
+
1161
+ const sitemap = (): MetadataRoute.Sitemap => [
1162
+ {
1163
+ url: "https://example.com/about",
1164
+ alternates: {
1165
+ languages: { ...getMultilingualUrls("https://example.com/about") },
1166
+ },
1167
+ },
1168
+ ];
1169
+ ```
1170
+
1171
+ ```tsx fileName="src/app/robots.ts"
1172
+ import { getMultilingualUrls } from "intlayer";
1173
+ import type { MetadataRoute } from "next";
1174
+
1175
+ const getAllMultilingualUrls = (urls: string[]) =>
1176
+ urls.flatMap((url) => Object.values(getMultilingualUrls(url)) as string[]);
1177
+
1178
+ const robots = (): MetadataRoute.Robots => ({
1179
+ rules: {
1180
+ userAgent: "*",
1181
+ allow: ["/"],
1182
+ disallow: getAllMultilingualUrls(["/dashboard"]),
1183
+ },
1184
+ host: "https://example.com",
1185
+ sitemap: "https://example.com/sitemap.xml",
1186
+ });
1187
+
1188
+ export default robots;
1189
+ ```
1190
+
1191
+ </TabItem>
1192
+ </Tab>
1193
+
1194
+ > Intlayer provides a `getMultilingualUrls` function to generate multilingual URLs for your sitemap.
1195
+
135
1196
  ---
136
1197
 
137
- ## When to choose which?
1198
+ ---
1199
+
1200
+ ## And the winner is…
1201
+
1202
+ It’s not simple. Each option has trade-offs. Here’s how I see it:
1203
+
1204
+ <Columns>
1205
+ <Column>
1206
+
1207
+ **next-intl**
1208
+
1209
+ - simplest, lightweight, fewer decisions forced on you. If you want a **minimal** solution, you’re comfortable with centralised catalogues, and your app is **small to mid-size**.
1210
+
1211
+ </Column>
1212
+ <Column>
1213
+
1214
+ **next-i18next**
1215
+
1216
+ - 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.
1217
+
1218
+ </Column>
1219
+ <Column>
1220
+
1221
+ **Intlayer**
1222
+
1223
+ - 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**.
1224
+
1225
+ </Column>
1226
+ </Columns>
1227
+
1228
+ If you prefer minimal setup and accept some manual wiring, next-intl is a good choice. If you need all the features and don’t mind complexity, next-i18next works. But if you want a modern, scalable, modular solution with built-in tools, Intlayer aims to provide that out of the box.
1229
+
1230
+ > **Alternative for enterprise teams**: If you need a well-proven solution that works perfectly with established localisation platforms like **Crowdin**, **Phrase**, or other professional translation management systems, consider **next-intl** or **next-i18next** for their mature ecosystem and proven integrations.
1231
+
1232
+ > **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.
1233
+
1234
+ ## GitHub STARs
1235
+
1236
+ 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.
138
1237
 
139
- - **Choose next-intl** if you want a **minimal** solution, you’re comfortable with centralised catalogues, and your app is **small to mid-size**.
140
- - **Choose next-i18next** 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.
141
- - **Choose Intlayer** 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**.
1238
+ [![Star History Chart](https://api.star-history.com/svg?repos=i18next/next-i18next&repos=amannn/next-intl&repos=aymericzip/intlayer&type=Date)](https://www.star-history.com/#i18next/next-i18next&amannn/next-intl&aymericzip/intlayer)
142
1239
 
143
1240
  ---
144
1241
 
@@ -147,6 +1244,6 @@ We focus on **Next.js 13+ App Router** (with **React Server Components**) and ev
147
1244
  All three libraries succeed at core localisation. The difference is **how much work you must do** to achieve a robust, scalable setup in **modern Next.js**:
148
1245
 
149
1246
  - With **Intlayer**, **modular content**, **strict TS**, **build-time safety**, **tree-shaken bundles**, and **first-class App Router + SEO tooling** are **defaults**, not chores.
150
- - If your team values **maintainability and speed** in a multi-locale, component-driven app, Intlayer offers the **most complete** experience today.
1247
+ - If your team prizes **maintainability and speed** in a multi-locale, component-driven app, Intlayer offers the **most complete** experience today.
151
1248
 
152
- Refer to the ['Why Intlayer?' doc](https://intlayer.org/doc/why) for more details.
1249
+ Refer to ['Why Intlayer?' doc](https://intlayer.org/doc/why) for more details.