@intlayer/docs 6.1.5 → 6.1.6
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 +404 -173
- package/blog/de/next-i18next_vs_next-intl_vs_intlayer.md +262 -113
- 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 +463 -209
- package/blog/en-GB/next-i18next_vs_next-intl_vs_intlayer.md +38 -28
- package/blog/es/next-i18next_vs_next-intl_vs_intlayer.md +185 -71
- package/blog/fr/next-i18next_vs_next-intl_vs_intlayer.md +38 -28
- package/blog/it/next-i18next_vs_next-intl_vs_intlayer.md +38 -28
- package/blog/ja/next-i18next_vs_next-intl_vs_intlayer.md +38 -28
- package/blog/ko/next-i18next_vs_next-intl_vs_intlayer.md +38 -28
- package/blog/pt/next-i18next_vs_next-intl_vs_intlayer.md +38 -28
- package/blog/ru/next-i18next_vs_next-intl_vs_intlayer.md +36 -28
- package/blog/tr/next-i18next_vs_next-intl_vs_intlayer.md +2 -0
- package/blog/zh/next-i18next_vs_next-intl_vs_intlayer.md +38 -28
- package/dist/cjs/generated/docs.entry.cjs +32 -0
- package/dist/cjs/generated/docs.entry.cjs.map +1 -1
- package/dist/esm/generated/docs.entry.mjs +32 -0
- package/dist/esm/generated/docs.entry.mjs.map +1 -1
- package/dist/types/generated/docs.entry.d.ts +2 -0
- package/dist/types/generated/docs.entry.d.ts.map +1 -1
- package/docs/ar/component_i18n.md +186 -0
- package/docs/ar/intlayer_with_angular.md +2 -2
- package/docs/ar/intlayer_with_astro.md +246 -0
- package/docs/ar/intlayer_with_create_react_app.md +3 -2
- package/docs/ar/intlayer_with_express.md +2 -2
- package/docs/ar/intlayer_with_nestjs.md +2 -2
- package/docs/ar/intlayer_with_nextjs_14.md +2 -2
- package/docs/ar/intlayer_with_nextjs_15.md +2 -2
- package/docs/ar/intlayer_with_nextjs_page_router.md +2 -2
- package/docs/ar/intlayer_with_nuxt.md +2 -2
- package/docs/ar/intlayer_with_react_native+expo.md +11 -20
- package/docs/ar/intlayer_with_react_router_v7.md +195 -241
- package/docs/ar/intlayer_with_tanstack.md +198 -272
- package/docs/ar/intlayer_with_vite+preact.md +9 -9
- package/docs/ar/intlayer_with_vite+react.md +7 -7
- package/docs/ar/intlayer_with_vite+vue.md +9 -9
- package/docs/ar/vs_code_extension.md +48 -109
- package/docs/de/component_i18n.md +186 -0
- package/docs/de/intlayer_with_angular.md +2 -2
- package/docs/de/intlayer_with_astro.md +246 -0
- package/docs/de/intlayer_with_create_react_app.md +2 -2
- package/docs/de/intlayer_with_express.md +2 -2
- package/docs/de/intlayer_with_nestjs.md +2 -2
- package/docs/de/intlayer_with_nextjs_14.md +2 -2
- package/docs/de/intlayer_with_nextjs_15.md +2 -2
- package/docs/de/intlayer_with_nextjs_page_router.md +2 -2
- package/docs/de/intlayer_with_nuxt.md +2 -2
- package/docs/de/intlayer_with_react_native+expo.md +11 -20
- package/docs/de/intlayer_with_react_router_v7.md +193 -242
- package/docs/de/intlayer_with_tanstack.md +194 -266
- package/docs/de/intlayer_with_vite+preact.md +9 -9
- package/docs/de/intlayer_with_vite+react.md +9 -9
- package/docs/de/intlayer_with_vite+vue.md +11 -11
- package/docs/de/packages/vite-intlayer/index.md +3 -3
- package/docs/de/vs_code_extension.md +46 -107
- package/docs/en/component_i18n.md +186 -0
- package/docs/en/how_works_intlayer.md +1 -1
- package/docs/en/index.md +1 -1
- package/docs/en/intlayer_cli.md +1 -1
- package/docs/en/intlayer_with_angular.md +4 -4
- package/docs/en/intlayer_with_astro.md +246 -0
- package/docs/en/intlayer_with_create_react_app.md +4 -4
- package/docs/en/intlayer_with_express.md +3 -3
- package/docs/en/intlayer_with_lynx+react.md +1 -1
- package/docs/en/intlayer_with_nestjs.md +2 -2
- package/docs/en/intlayer_with_nextjs_14.md +31 -5
- package/docs/en/intlayer_with_nextjs_15.md +31 -5
- package/docs/en/intlayer_with_nextjs_page_router.md +5 -5
- package/docs/en/intlayer_with_nuxt.md +4 -4
- package/docs/en/intlayer_with_react_native+expo.md +46 -24
- package/docs/en/intlayer_with_react_router_v7.md +164 -211
- package/docs/en/intlayer_with_tanstack.md +166 -241
- package/docs/en/intlayer_with_vite+preact.md +12 -12
- package/docs/en/intlayer_with_vite+react.md +12 -12
- package/docs/en/intlayer_with_vite+solid.md +2 -2
- package/docs/en/intlayer_with_vite+svelte.md +2 -2
- package/docs/en/intlayer_with_vite+vue.md +12 -12
- package/docs/en/introduction.md +1 -1
- package/docs/en/packages/next-intlayer/useDictionary.md +1 -1
- package/docs/en/packages/next-intlayer/useIntlayer.md +1 -1
- package/docs/en/packages/react-intlayer/useDictionary.md +1 -1
- package/docs/en/packages/react-intlayer/useI18n.md +1 -1
- package/docs/en/packages/react-intlayer/useIntlayer.md +1 -1
- package/docs/en/releases/v6.md +1 -0
- package/docs/en/roadmap.md +1 -1
- package/docs/en/vs_code_extension.md +24 -114
- package/docs/en-GB/component_i18n.md +186 -0
- package/docs/en-GB/intlayer_with_angular.md +3 -3
- package/docs/en-GB/intlayer_with_astro.md +246 -0
- package/docs/en-GB/intlayer_with_create_react_app.md +5 -4
- package/docs/en-GB/intlayer_with_express.md +2 -2
- package/docs/en-GB/intlayer_with_nestjs.md +2 -2
- package/docs/en-GB/intlayer_with_nextjs_14.md +4 -4
- package/docs/en-GB/intlayer_with_nextjs_15.md +2 -2
- package/docs/en-GB/intlayer_with_nextjs_page_router.md +2 -2
- package/docs/en-GB/intlayer_with_nuxt.md +2 -2
- package/docs/en-GB/intlayer_with_react_native+expo.md +11 -20
- package/docs/en-GB/intlayer_with_react_router_v7.md +171 -220
- package/docs/en-GB/intlayer_with_tanstack.md +174 -248
- package/docs/en-GB/intlayer_with_vite+preact.md +9 -9
- package/docs/en-GB/intlayer_with_vite+react.md +9 -9
- package/docs/en-GB/intlayer_with_vite+vue.md +11 -11
- package/docs/en-GB/packages/next-intlayer/useIntlayer.md +1 -1
- package/docs/en-GB/packages/react-intlayer/useIntlayer.md +1 -1
- package/docs/en-GB/vs_code_extension.md +42 -103
- package/docs/es/component_i18n.md +182 -0
- package/docs/es/intlayer_with_angular.md +2 -2
- package/docs/es/intlayer_with_astro.md +246 -0
- package/docs/es/intlayer_with_create_react_app.md +3 -2
- package/docs/es/intlayer_with_express.md +2 -2
- package/docs/es/intlayer_with_nextjs_14.md +2 -2
- package/docs/es/intlayer_with_nextjs_15.md +2 -2
- package/docs/es/intlayer_with_react_native+expo.md +11 -20
- package/docs/es/intlayer_with_react_router_v7.md +188 -232
- package/docs/es/intlayer_with_tanstack.md +203 -273
- package/docs/es/intlayer_with_vite+preact.md +7 -7
- package/docs/es/intlayer_with_vite+react.md +7 -7
- package/docs/es/intlayer_with_vite+vue.md +9 -9
- package/docs/es/vs_code_extension.md +53 -114
- package/docs/fr/component_i18n.md +186 -0
- package/docs/fr/intlayer_with_angular.md +2 -2
- package/docs/fr/intlayer_with_astro.md +246 -0
- package/docs/fr/intlayer_with_create_react_app.md +3 -2
- package/docs/fr/intlayer_with_express.md +2 -2
- package/docs/fr/intlayer_with_nestjs.md +2 -2
- package/docs/fr/intlayer_with_nextjs_14.md +2 -2
- package/docs/fr/intlayer_with_react_native+expo.md +11 -20
- package/docs/fr/intlayer_with_react_router_v7.md +188 -248
- package/docs/fr/intlayer_with_tanstack.md +192 -265
- package/docs/fr/intlayer_with_vite+preact.md +7 -7
- package/docs/fr/intlayer_with_vite+react.md +7 -7
- package/docs/fr/intlayer_with_vite+vue.md +9 -9
- package/docs/fr/vs_code_extension.md +50 -111
- package/docs/hi/component_i18n.md +186 -0
- package/docs/hi/intlayer_cli.md +1 -4
- package/docs/hi/intlayer_with_angular.md +2 -2
- package/docs/hi/intlayer_with_astro.md +246 -0
- package/docs/hi/intlayer_with_create_react_app.md +2 -2
- package/docs/hi/intlayer_with_express.md +2 -2
- package/docs/hi/intlayer_with_nestjs.md +2 -2
- package/docs/hi/intlayer_with_nextjs_14.md +2 -2
- package/docs/hi/intlayer_with_nextjs_15.md +2 -2
- package/docs/hi/intlayer_with_nextjs_page_router.md +2 -2
- package/docs/hi/intlayer_with_nuxt.md +2 -2
- package/docs/hi/intlayer_with_react_native+expo.md +11 -20
- package/docs/hi/intlayer_with_react_router_v7.md +199 -243
- package/docs/hi/intlayer_with_tanstack.md +210 -285
- package/docs/hi/intlayer_with_vite+preact.md +9 -9
- package/docs/hi/intlayer_with_vite+react.md +9 -9
- package/docs/hi/intlayer_with_vite+solid.md +1 -1
- package/docs/hi/intlayer_with_vite+vue.md +11 -11
- package/docs/hi/vs_code_extension.md +49 -110
- package/docs/it/component_i18n.md +186 -0
- package/docs/it/intlayer_with_angular.md +2 -2
- package/docs/it/intlayer_with_astro.md +246 -0
- package/docs/it/intlayer_with_create_react_app.md +3 -2
- package/docs/it/intlayer_with_express.md +2 -2
- package/docs/it/intlayer_with_nestjs.md +2 -2
- package/docs/it/intlayer_with_nextjs_14.md +2 -2
- package/docs/it/intlayer_with_nextjs_15.md +2 -2
- package/docs/it/intlayer_with_nextjs_page_router.md +2 -2
- package/docs/it/intlayer_with_nuxt.md +2 -2
- package/docs/it/intlayer_with_react_native+expo.md +11 -21
- package/docs/it/intlayer_with_react_router_v7.md +195 -242
- package/docs/it/intlayer_with_tanstack.md +203 -267
- package/docs/it/intlayer_with_vite+preact.md +9 -9
- package/docs/it/intlayer_with_vite+react.md +13 -11
- package/docs/it/intlayer_with_vite+vue.md +11 -11
- package/docs/it/vs_code_extension.md +50 -111
- package/docs/ja/component_i18n.md +186 -0
- package/docs/ja/intlayer_with_angular.md +2 -2
- package/docs/ja/intlayer_with_astro.md +246 -0
- package/docs/ja/intlayer_with_create_react_app.md +3 -2
- package/docs/ja/intlayer_with_express.md +2 -2
- package/docs/ja/intlayer_with_nestjs.md +2 -2
- package/docs/ja/intlayer_with_nextjs_14.md +2 -2
- package/docs/ja/intlayer_with_nextjs_15.md +2 -2
- package/docs/ja/intlayer_with_nextjs_page_router.md +2 -2
- package/docs/ja/intlayer_with_nuxt.md +2 -2
- package/docs/ja/intlayer_with_react_native+expo.md +18 -29
- package/docs/ja/intlayer_with_react_router_v7.md +204 -250
- package/docs/ja/intlayer_with_tanstack.md +218 -286
- package/docs/ja/intlayer_with_vite+preact.md +9 -9
- package/docs/ja/intlayer_with_vite+react.md +11 -11
- package/docs/ja/intlayer_with_vite+vue.md +11 -11
- package/docs/ja/vs_code_extension.md +50 -111
- package/docs/ko/component_i18n.md +186 -0
- package/docs/ko/intlayer_with_angular.md +2 -2
- package/docs/ko/intlayer_with_astro.md +246 -0
- package/docs/ko/intlayer_with_create_react_app.md +3 -2
- package/docs/ko/intlayer_with_express.md +2 -2
- package/docs/ko/intlayer_with_nestjs.md +2 -2
- package/docs/ko/intlayer_with_nextjs_14.md +2 -2
- package/docs/ko/intlayer_with_nextjs_15.md +2 -2
- package/docs/ko/intlayer_with_nextjs_page_router.md +2 -2
- package/docs/ko/intlayer_with_nuxt.md +2 -2
- package/docs/ko/intlayer_with_react_native+expo.md +19 -28
- package/docs/ko/intlayer_with_react_router_v7.md +190 -244
- package/docs/ko/intlayer_with_tanstack.md +200 -270
- package/docs/ko/intlayer_with_vite+preact.md +9 -9
- package/docs/ko/intlayer_with_vite+react.md +9 -9
- package/docs/ko/intlayer_with_vite+vue.md +11 -11
- package/docs/ko/vs_code_extension.md +48 -109
- package/docs/pt/component_i18n.md +186 -0
- package/docs/pt/intlayer_with_angular.md +2 -2
- package/docs/pt/intlayer_with_astro.md +246 -0
- package/docs/pt/intlayer_with_create_react_app.md +3 -2
- package/docs/pt/intlayer_with_express.md +2 -2
- package/docs/pt/intlayer_with_nestjs.md +2 -2
- package/docs/pt/intlayer_with_nextjs_14.md +2 -2
- package/docs/pt/intlayer_with_nextjs_15.md +2 -2
- package/docs/pt/intlayer_with_nextjs_page_router.md +2 -2
- package/docs/pt/intlayer_with_nuxt.md +2 -2
- package/docs/pt/intlayer_with_react_native+expo.md +11 -20
- package/docs/pt/intlayer_with_react_router_v7.md +7 -13
- package/docs/pt/intlayer_with_tanstack.md +183 -258
- package/docs/pt/intlayer_with_vite+preact.md +9 -9
- package/docs/pt/intlayer_with_vite+react.md +9 -9
- package/docs/pt/intlayer_with_vite+vue.md +9 -9
- package/docs/pt/vs_code_extension.md +46 -107
- package/docs/ru/component_i18n.md +186 -0
- package/docs/ru/intlayer_with_angular.md +2 -2
- package/docs/ru/intlayer_with_astro.md +246 -0
- package/docs/ru/intlayer_with_create_react_app.md +3 -2
- package/docs/ru/intlayer_with_express.md +2 -2
- package/docs/ru/intlayer_with_nestjs.md +2 -2
- package/docs/ru/intlayer_with_nextjs_14.md +2 -2
- package/docs/ru/intlayer_with_nextjs_15.md +2 -2
- package/docs/ru/intlayer_with_nextjs_page_router.md +2 -2
- package/docs/ru/intlayer_with_nuxt.md +2 -2
- package/docs/ru/intlayer_with_react_native+expo.md +11 -20
- package/docs/ru/intlayer_with_react_router_v7.md +192 -238
- package/docs/ru/intlayer_with_tanstack.md +197 -269
- package/docs/ru/intlayer_with_vite+preact.md +9 -9
- package/docs/ru/intlayer_with_vite+react.md +9 -9
- package/docs/ru/intlayer_with_vite+vue.md +11 -11
- package/docs/ru/vs_code_extension.md +48 -109
- package/docs/tr/component_i18n.md +186 -0
- package/docs/tr/how_works_intlayer.md +1 -1
- package/docs/tr/index.md +1 -1
- package/docs/tr/intlayer_cli.md +1 -1
- package/docs/tr/intlayer_with_angular.md +4 -4
- package/docs/tr/intlayer_with_astro.md +246 -0
- package/docs/tr/intlayer_with_create_react_app.md +4 -4
- package/docs/tr/intlayer_with_express.md +3 -3
- package/docs/tr/intlayer_with_lynx+react.md +1 -1
- package/docs/tr/intlayer_with_nestjs.md +2 -2
- package/docs/tr/intlayer_with_nextjs_14.md +2 -2
- package/docs/tr/intlayer_with_nextjs_15.md +4 -4
- package/docs/tr/intlayer_with_nextjs_page_router.md +5 -5
- package/docs/tr/intlayer_with_nuxt.md +4 -4
- package/docs/tr/intlayer_with_react_native+expo.md +12 -21
- package/docs/tr/intlayer_with_react_router_v7.md +222 -267
- package/docs/tr/intlayer_with_tanstack.md +400 -303
- package/docs/tr/intlayer_with_vite+preact.md +12 -12
- package/docs/tr/intlayer_with_vite+react.md +12 -12
- package/docs/tr/intlayer_with_vite+solid.md +2 -2
- package/docs/tr/intlayer_with_vite+svelte.md +2 -2
- package/docs/tr/intlayer_with_vite+vue.md +12 -12
- package/docs/tr/introduction.md +1 -1
- package/docs/tr/packages/react-intlayer/useDictionary.md +1 -1
- package/docs/tr/packages/react-intlayer/useI18n.md +1 -1
- package/docs/tr/roadmap.md +1 -1
- package/docs/tr/vs_code_extension.md +54 -115
- package/docs/zh/component_i18n.md +186 -0
- package/docs/zh/intlayer_with_angular.md +2 -2
- package/docs/zh/intlayer_with_astro.md +246 -0
- package/docs/zh/intlayer_with_create_react_app.md +3 -2
- package/docs/zh/intlayer_with_express.md +2 -2
- package/docs/zh/intlayer_with_nestjs.md +2 -2
- package/docs/zh/intlayer_with_nextjs_14.md +2 -2
- package/docs/zh/intlayer_with_nextjs_15.md +2 -2
- package/docs/zh/intlayer_with_nextjs_page_router.md +2 -2
- package/docs/zh/intlayer_with_nuxt.md +2 -2
- package/docs/zh/intlayer_with_react_native+expo.md +19 -28
- package/docs/zh/intlayer_with_react_router_v7.md +200 -248
- package/docs/zh/intlayer_with_tanstack.md +208 -283
- package/docs/zh/intlayer_with_vite+preact.md +9 -9
- package/docs/zh/intlayer_with_vite+react.md +9 -9
- package/docs/zh/intlayer_with_vite+vue.md +9 -9
- package/docs/zh/vs_code_extension.md +51 -105
- package/package.json +10 -10
- package/src/generated/docs.entry.ts +32 -0
|
@@ -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.
|
|
@@ -35,7 +37,7 @@ We focus on **Next.js 13+ App Router** (with **React Server Components**) and ev
|
|
|
35
37
|
|
|
36
38
|
> **tl;dr**: All three can localize 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.
|
|
37
39
|
|
|
38
|
-
> One confusion often made by developers is to think that `next-intl` is the Next.js version of `react-intl`. It's not
|
|
40
|
+
> 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
41
|
|
|
40
42
|
---
|
|
41
43
|
|
|
@@ -163,11 +165,13 @@ How the library handles fallbacks is also important. Let's consider that the app
|
|
|
163
165
|
|
|
164
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.**
|
|
165
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
|
+
|
|
166
170
|
Here an example of the impact of bundle size optimization using `intlayer` in a vite + react application:
|
|
167
171
|
|
|
168
|
-
| Optimized bundle
|
|
169
|
-
|
|
|
170
|
-
|  |  |
|
|
172
|
+
| Optimized bundle | Bundle not optimized |
|
|
173
|
+
| ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
|
|
174
|
+
|  |  |
|
|
171
175
|
|
|
172
176
|
---
|
|
173
177
|
|
|
@@ -176,16 +180,16 @@ Here an example of the impact of bundle size optimization using `intlayer` in a
|
|
|
176
180
|
<Columns>
|
|
177
181
|
<Column>
|
|
178
182
|
|
|
179
|
-
**next-
|
|
183
|
+
**next-i18next**
|
|
180
184
|
|
|
181
|
-
-
|
|
185
|
+
- Base typings for hooks. **strict key typing requires extra tooling/config**.
|
|
182
186
|
|
|
183
187
|
</Column>
|
|
184
188
|
<Column>
|
|
185
189
|
|
|
186
|
-
**next-
|
|
190
|
+
**next-intl**
|
|
187
191
|
|
|
188
|
-
-
|
|
192
|
+
- Solid TypeScript support, but **keys aren’t strictly typed by default**. you’ll maintain safety patterns manually.
|
|
189
193
|
|
|
190
194
|
</Column>
|
|
191
195
|
<Column>
|
|
@@ -206,16 +210,16 @@ Here an example of the impact of bundle size optimization using `intlayer` in a
|
|
|
206
210
|
<Columns>
|
|
207
211
|
<Column>
|
|
208
212
|
|
|
209
|
-
**next-
|
|
213
|
+
**next-i18next**
|
|
210
214
|
|
|
211
|
-
- Relies on **runtime fallbacks
|
|
215
|
+
- Relies on **runtime fallbacks**. Build doesn’t fail.
|
|
212
216
|
|
|
213
217
|
</Column>
|
|
214
218
|
<Column>
|
|
215
219
|
|
|
216
|
-
**next-
|
|
220
|
+
**next-intl**
|
|
217
221
|
|
|
218
|
-
- Relies on **runtime fallbacks
|
|
222
|
+
- Relies on **runtime fallbacks**. Build doesn’t fail.
|
|
219
223
|
|
|
220
224
|
</Column>
|
|
221
225
|
<Column>
|
|
@@ -227,7 +231,7 @@ Here an example of the impact of bundle size optimization using `intlayer` in a
|
|
|
227
231
|
</Column>
|
|
228
232
|
</Columns>
|
|
229
233
|
|
|
230
|
-
**Why it matters:** Catching gaps during build prevents
|
|
234
|
+
**Why it matters:** Catching gaps during build prevents 'undefined' strings in production.
|
|
231
235
|
|
|
232
236
|
---
|
|
233
237
|
|
|
@@ -236,28 +240,30 @@ Here an example of the impact of bundle size optimization using `intlayer` in a
|
|
|
236
240
|
<Columns>
|
|
237
241
|
<Column>
|
|
238
242
|
|
|
239
|
-
**next-
|
|
243
|
+
**next-i18next**
|
|
240
244
|
|
|
241
|
-
-
|
|
245
|
+
- Allows localized routing. But middleware is not built-in.
|
|
242
246
|
|
|
243
247
|
</Column>
|
|
244
248
|
<Column>
|
|
245
249
|
|
|
246
|
-
**next-
|
|
250
|
+
**next-intl**
|
|
247
251
|
|
|
248
|
-
-
|
|
252
|
+
- Allows localized routing.
|
|
253
|
+
- Provides middleware.
|
|
249
254
|
|
|
250
255
|
</Column>
|
|
251
256
|
<Column>
|
|
252
257
|
|
|
253
258
|
**intlayer**
|
|
254
259
|
|
|
255
|
-
-
|
|
260
|
+
- Allows localized routing.
|
|
261
|
+
- Provides middleware.
|
|
256
262
|
|
|
257
263
|
</Column>
|
|
258
264
|
</Columns>
|
|
259
265
|
|
|
260
|
-
**Why it matters:**
|
|
266
|
+
**Why it matters:** Helps for SEO and discovery, as well as user experience.
|
|
261
267
|
|
|
262
268
|
---
|
|
263
269
|
|
|
@@ -266,58 +272,33 @@ Here an example of the impact of bundle size optimization using `intlayer` in a
|
|
|
266
272
|
<Columns>
|
|
267
273
|
<Column>
|
|
268
274
|
|
|
269
|
-
**next-intl**
|
|
270
|
-
|
|
271
|
-
- Supports Next.js 13+. Often requires passing t-functions/formatters through component trees in hybrid setups.
|
|
272
|
-
|
|
273
|
-
</Column>
|
|
274
|
-
<Column>
|
|
275
|
-
|
|
276
275
|
**next-i18next**
|
|
277
276
|
|
|
278
|
-
-
|
|
277
|
+
- Support page and layout server components.
|
|
278
|
+
- Do not provide synchronous API for children server components.
|
|
279
279
|
|
|
280
280
|
</Column>
|
|
281
281
|
<Column>
|
|
282
282
|
|
|
283
|
-
**intlayer**
|
|
284
|
-
|
|
285
|
-
- Supports Next.js 13+ and smooths the **server/client boundary** with a consistent API and RSC-oriented providers, avoiding shuttling formatters or t-functions.
|
|
286
|
-
|
|
287
|
-
</Column>
|
|
288
|
-
</Columns>
|
|
289
|
-
|
|
290
|
-
**Why it matters:** Cleaner mental model and fewer edge cases in hybrid trees.
|
|
291
|
-
|
|
292
|
-
---
|
|
293
|
-
|
|
294
|
-
## DX, tooling & maintenance
|
|
295
|
-
|
|
296
|
-
<Columns>
|
|
297
|
-
<Column>
|
|
298
|
-
|
|
299
283
|
**next-intl**
|
|
300
284
|
|
|
301
|
-
-
|
|
302
|
-
|
|
303
|
-
</Column>
|
|
304
|
-
<Column>
|
|
305
|
-
|
|
306
|
-
**next-i18next**
|
|
307
|
-
|
|
308
|
-
- Commonly paired with external localization platforms and editorial workflows.
|
|
285
|
+
- Support page and layout server components.
|
|
286
|
+
- Do not provide synchronous API for children server components.
|
|
309
287
|
|
|
310
288
|
</Column>
|
|
311
289
|
<Column>
|
|
312
290
|
|
|
313
291
|
**intlayer**
|
|
314
292
|
|
|
315
|
-
-
|
|
293
|
+
- Support page and layout server components.
|
|
294
|
+
- Provide synchronous API for children server components.
|
|
316
295
|
|
|
317
296
|
</Column>
|
|
318
297
|
</Columns>
|
|
319
298
|
|
|
320
|
-
**Why it matters:**
|
|
299
|
+
**Why it matters:** Server component suport is a key feature of Next.js 13+, helping for performance. Passing props the locale or the `t` function from the parent to the child server components make your components less reusable.
|
|
300
|
+
|
|
301
|
+
---
|
|
321
302
|
|
|
322
303
|
## Integration with localization platforms (TMS)
|
|
323
304
|
|
|
@@ -338,6 +319,8 @@ Large organizations often rely on Translation Management Systems (TMS) like **Cr
|
|
|
338
319
|
|
|
339
320
|
> 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.
|
|
340
321
|
|
|
322
|
+
---
|
|
323
|
+
|
|
341
324
|
## Developer Experience
|
|
342
325
|
|
|
343
326
|
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.
|
|
@@ -352,25 +335,25 @@ The app structure is important to ensure good maintainability for your codebase.
|
|
|
352
335
|
|
|
353
336
|
```bash
|
|
354
337
|
.
|
|
355
|
-
├──
|
|
356
|
-
│ └── locales
|
|
357
|
-
│ ├── en
|
|
358
|
-
│ │ ├── home.json
|
|
359
|
-
│ │ └── navbar.json
|
|
360
|
-
│ ├── fr
|
|
361
|
-
│ │ ├── home.json
|
|
362
|
-
│ │ └── navbar.json
|
|
363
|
-
│ └── es
|
|
364
|
-
│ ├── home.json
|
|
365
|
-
│ └── navbar.json
|
|
366
|
-
├── next-i18next.config.js
|
|
338
|
+
├── i18n.config.ts
|
|
367
339
|
└── src
|
|
368
|
-
├──
|
|
340
|
+
├── locales
|
|
341
|
+
│ ├── en
|
|
342
|
+
│ │ ├── common.json
|
|
343
|
+
│ │ └── about.json
|
|
344
|
+
│ └── fr
|
|
345
|
+
│ ├── common.json
|
|
346
|
+
│ └── about.json
|
|
369
347
|
├── app
|
|
370
|
-
│
|
|
348
|
+
│ ├── i18n
|
|
349
|
+
│ │ └── server.ts
|
|
350
|
+
│ └── [locale]
|
|
351
|
+
│ ├── layout.tsx
|
|
352
|
+
│ └── about.tsx
|
|
371
353
|
└── components
|
|
372
|
-
|
|
373
|
-
|
|
354
|
+
├── I18nProvider.tsx
|
|
355
|
+
├── ClientComponent.tsx
|
|
356
|
+
└── ServerComponent.tsx
|
|
374
357
|
```
|
|
375
358
|
|
|
376
359
|
</TabItem>
|
|
@@ -378,6 +361,7 @@ The app structure is important to ensure good maintainability for your codebase.
|
|
|
378
361
|
|
|
379
362
|
```bash
|
|
380
363
|
.
|
|
364
|
+
├── i18n.ts
|
|
381
365
|
├── locales
|
|
382
366
|
│ ├── en
|
|
383
367
|
│ │ ├── home.json
|
|
@@ -388,11 +372,13 @@ The app structure is important to ensure good maintainability for your codebase.
|
|
|
388
372
|
│ └── es
|
|
389
373
|
│ ├── home.json
|
|
390
374
|
│ └── navbar.json
|
|
391
|
-
├── i18n.ts
|
|
392
375
|
└── src
|
|
393
376
|
├── middleware.ts
|
|
394
377
|
├── app
|
|
395
|
-
│
|
|
378
|
+
│ ├── i18n
|
|
379
|
+
│ │ └── server.ts
|
|
380
|
+
│ └── [locale]
|
|
381
|
+
│ └── home.tsx
|
|
396
382
|
└── components
|
|
397
383
|
└── Navbar
|
|
398
384
|
└── index.tsx
|
|
@@ -407,9 +393,11 @@ The app structure is important to ensure good maintainability for your codebase.
|
|
|
407
393
|
└── src
|
|
408
394
|
├── middleware.ts
|
|
409
395
|
├── app
|
|
410
|
-
│ └──
|
|
411
|
-
│
|
|
412
|
-
│ └──
|
|
396
|
+
│ └── [locale]
|
|
397
|
+
│ ├── layout.tsx
|
|
398
|
+
│ └── home
|
|
399
|
+
│ ├── index.tsx
|
|
400
|
+
│ └── index.content.ts
|
|
413
401
|
└── components
|
|
414
402
|
└── Navbar
|
|
415
403
|
├── index.tsx
|
|
@@ -432,141 +420,276 @@ How the library handles content loading is important.
|
|
|
432
420
|
<Tab defaultTab="next-intl" group='techno'>
|
|
433
421
|
<TabItem label="next-i18next" value="next-i18next">
|
|
434
422
|
|
|
435
|
-
```
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
locales: ["en", "fr", "es"],
|
|
439
|
-
defaultLocale: "en",
|
|
440
|
-
},
|
|
441
|
-
};
|
|
442
|
-
```
|
|
423
|
+
```ts fileName="i18n.config.ts"
|
|
424
|
+
export const locales = ["en", "fr"] as const;
|
|
425
|
+
export type Locale = (typeof locales)[number];
|
|
443
426
|
|
|
444
|
-
|
|
445
|
-
import { appWithTranslation } from "next-i18next";
|
|
427
|
+
export const defaultLocale: Locale = "en";
|
|
446
428
|
|
|
447
|
-
const
|
|
429
|
+
export const rtlLocales = ["ar", "he", "fa", "ur"] as const;
|
|
430
|
+
export const isRtl = (locale: string) =>
|
|
431
|
+
(rtlLocales as readonly string[]).includes(locale);
|
|
448
432
|
|
|
449
|
-
export
|
|
433
|
+
export function localizedPath(locale: string, path: string) {
|
|
434
|
+
return locale === defaultLocale ? path : "/" + locale + path;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const ORIGIN = "https://example.com";
|
|
438
|
+
export function abs(locale: string, path: string) {
|
|
439
|
+
return ORIGIN + localizedPath(locale, path);
|
|
440
|
+
}
|
|
450
441
|
```
|
|
451
442
|
|
|
452
|
-
```
|
|
453
|
-
import type { GetStaticProps } from "next";
|
|
454
|
-
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
|
|
455
|
-
import { useTranslation } from "next-i18next";
|
|
456
|
-
import { I18nextProvider, initReactI18next } from "react-i18next";
|
|
443
|
+
```ts fileName="src/app/i18n/server.ts"
|
|
457
444
|
import { createInstance } from "i18next";
|
|
458
|
-
import {
|
|
445
|
+
import { initReactI18next } from "react-i18next/initReactI18next";
|
|
446
|
+
import resourcesToBackend from "i18next-resources-to-backend";
|
|
447
|
+
import { defaultLocale } from "@/i18n.config";
|
|
448
|
+
|
|
449
|
+
// Load JSON resources from src/locales/<locale>/<namespace>.json
|
|
450
|
+
const backend = resourcesToBackend(
|
|
451
|
+
(locale: string, namespace: string) =>
|
|
452
|
+
import(`../../locales/${locale}/${namespace}.json`)
|
|
453
|
+
);
|
|
454
|
+
|
|
455
|
+
export async function initI18next(
|
|
456
|
+
locale: string,
|
|
457
|
+
namespaces: string[] = ["common"]
|
|
458
|
+
) {
|
|
459
|
+
const i18n = createInstance();
|
|
460
|
+
await i18n
|
|
461
|
+
.use(initReactI18next)
|
|
462
|
+
.use(backend)
|
|
463
|
+
.init({
|
|
464
|
+
lng: locale,
|
|
465
|
+
fallbackLng: defaultLocale,
|
|
466
|
+
ns: namespaces,
|
|
467
|
+
defaultNS: "common",
|
|
468
|
+
interpolation: { escapeValue: false },
|
|
469
|
+
react: { useSuspense: false },
|
|
470
|
+
});
|
|
471
|
+
return i18n;
|
|
472
|
+
}
|
|
473
|
+
```
|
|
459
474
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
const resources = await loadMessagesFor(locale); // your loader (JSON, etc.)
|
|
475
|
+
```tsx fileName="src/components/I18nProvider.tsx"
|
|
476
|
+
"use client";
|
|
463
477
|
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
478
|
+
import * as React from "react";
|
|
479
|
+
import { I18nextProvider } from "react-i18next";
|
|
480
|
+
import { createInstance } from "i18next";
|
|
481
|
+
import { initReactI18next } from "react-i18next/initReactI18next";
|
|
482
|
+
import resourcesToBackend from "i18next-resources-to-backend";
|
|
483
|
+
import { defaultLocale } from "@/i18n.config";
|
|
484
|
+
|
|
485
|
+
const backend = resourcesToBackend(
|
|
486
|
+
(locale: string, namespace: string) =>
|
|
487
|
+
import(`../../locales/${locale}/${namespace}.json`)
|
|
488
|
+
);
|
|
489
|
+
|
|
490
|
+
type Props = {
|
|
491
|
+
locale: string;
|
|
492
|
+
namespaces?: string[];
|
|
493
|
+
resources?: Record<string, any>; // { ns: bundle }
|
|
494
|
+
children: React.ReactNode;
|
|
495
|
+
};
|
|
496
|
+
|
|
497
|
+
export default function I18nProvider({
|
|
498
|
+
locale,
|
|
499
|
+
namespaces = ["common"],
|
|
500
|
+
resources,
|
|
501
|
+
children,
|
|
502
|
+
}: Props) {
|
|
503
|
+
const [i18n] = React.useState(() => {
|
|
504
|
+
const i = createInstance();
|
|
505
|
+
|
|
506
|
+
i.use(initReactI18next)
|
|
507
|
+
.use(backend)
|
|
508
|
+
.init({
|
|
509
|
+
lng: locale,
|
|
510
|
+
fallbackLng: defaultLocale,
|
|
511
|
+
ns: namespaces,
|
|
512
|
+
resources: resources ? { [locale]: resources } : undefined,
|
|
513
|
+
defaultNS: "common",
|
|
514
|
+
interpolation: { escapeValue: false },
|
|
515
|
+
react: { useSuspense: false },
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
return i;
|
|
472
519
|
});
|
|
473
520
|
|
|
474
|
-
|
|
521
|
+
return <I18nextProvider i18n={i18n}>{children}</I18nextProvider>;
|
|
522
|
+
}
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
```tsx fileName="src/app/[locale]/layout.tsx"
|
|
526
|
+
import type { ReactNode } from "react";
|
|
527
|
+
import { locales, defaultLocale, isRtl, type Locale } from "@/i18n.config";
|
|
528
|
+
|
|
529
|
+
export const dynamicParams = false;
|
|
530
|
+
|
|
531
|
+
export function generateStaticParams() {
|
|
532
|
+
return locales.map((locale) => ({ locale }));
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
export default function LocaleLayout({
|
|
536
|
+
children,
|
|
537
|
+
params,
|
|
538
|
+
}: {
|
|
539
|
+
children: ReactNode;
|
|
540
|
+
params: { locale: string };
|
|
541
|
+
}) {
|
|
542
|
+
const locale: Locale = (locales as readonly string[]).includes(params.locale)
|
|
543
|
+
? (params.locale as any)
|
|
544
|
+
: defaultLocale;
|
|
545
|
+
|
|
546
|
+
const dir = isRtl(locale) ? "rtl" : "ltr";
|
|
547
|
+
|
|
548
|
+
return (
|
|
549
|
+
<html lang={locale} dir={dir}>
|
|
550
|
+
<body>{children}</body>
|
|
551
|
+
</html>
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
```
|
|
555
|
+
|
|
556
|
+
```tsx fileName="src/app/[locale]/about.tsx"
|
|
557
|
+
import I18nProvider from "@/components/I18nProvider";
|
|
558
|
+
import { initI18next } from "@/app/i18n/server";
|
|
559
|
+
import type { Locale } from "@/i18n.config";
|
|
560
|
+
import ClientComponent from "@/components/ClientComponent";
|
|
561
|
+
import ServerComponent from "@/components/ServerComponent";
|
|
562
|
+
|
|
563
|
+
// Force static rendering for the page
|
|
564
|
+
export const dynamic = "force-static";
|
|
565
|
+
|
|
566
|
+
export default async function AboutPage({
|
|
567
|
+
params: { locale },
|
|
568
|
+
}: {
|
|
569
|
+
params: { locale: Locale };
|
|
570
|
+
}) {
|
|
571
|
+
const namespaces = ["common", "about"] as const;
|
|
572
|
+
|
|
573
|
+
const i18n = await initI18next(locale, [...namespaces]);
|
|
574
|
+
const tAbout = i18n.getFixedT(locale, "about");
|
|
475
575
|
|
|
476
576
|
return (
|
|
477
|
-
<
|
|
577
|
+
<I18nProvider locale={locale} namespaces={[...namespaces]}>
|
|
478
578
|
<main>
|
|
479
|
-
<h1>{
|
|
579
|
+
<h1>{tAbout("title")}</h1>
|
|
580
|
+
|
|
480
581
|
<ClientComponent />
|
|
481
|
-
<ServerComponent />
|
|
582
|
+
<ServerComponent t={tAbout} locale={locale} count={0} />
|
|
482
583
|
</main>
|
|
483
|
-
</
|
|
584
|
+
</I18nProvider>
|
|
484
585
|
);
|
|
485
586
|
}
|
|
486
|
-
|
|
487
|
-
export const getStaticProps: GetStaticProps = async ({ locale }) => {
|
|
488
|
-
// Ne préchargez que les namespaces nécessaires à CETTE page
|
|
489
|
-
return {
|
|
490
|
-
props: {
|
|
491
|
-
...(await serverSideTranslations(locale ?? "en", ["common", "about"])),
|
|
492
|
-
},
|
|
493
|
-
};
|
|
494
|
-
};
|
|
495
587
|
```
|
|
496
588
|
|
|
497
589
|
</TabItem>
|
|
498
590
|
<TabItem label="next-intl" value="next-intl">
|
|
499
591
|
|
|
500
|
-
```tsx fileName="i18n.ts"
|
|
592
|
+
```tsx fileName="src/i18n.ts"
|
|
501
593
|
import { getRequestConfig } from "next-intl/server";
|
|
502
594
|
import { notFound } from "next/navigation";
|
|
503
595
|
|
|
504
|
-
|
|
505
|
-
const
|
|
596
|
+
export const locales = ["en", "fr", "es"] as const;
|
|
597
|
+
export const defaultLocale = "en" as const;
|
|
598
|
+
|
|
599
|
+
async function loadMessages(locale: string) {
|
|
600
|
+
// Load only the namespaces your layout/pages need
|
|
601
|
+
const [common, about] = await Promise.all([
|
|
602
|
+
import(`../locales/${locale}/common.json`).then((m) => m.default),
|
|
603
|
+
import(`../locales/${locale}/about.json`).then((m) => m.default),
|
|
604
|
+
]);
|
|
605
|
+
|
|
606
|
+
return { common, about } as const;
|
|
607
|
+
}
|
|
506
608
|
|
|
507
609
|
export default getRequestConfig(async ({ locale }) => {
|
|
508
|
-
// Validate that the incoming `locale` parameter is valid
|
|
509
610
|
if (!locales.includes(locale as any)) notFound();
|
|
510
611
|
|
|
511
612
|
return {
|
|
512
|
-
messages:
|
|
613
|
+
messages: await loadMessages(locale),
|
|
513
614
|
};
|
|
514
615
|
});
|
|
515
616
|
```
|
|
516
617
|
|
|
517
|
-
```tsx fileName="src/app/[locale]/
|
|
518
|
-
import {
|
|
519
|
-
import {
|
|
520
|
-
import
|
|
618
|
+
```tsx fileName="src/app/[locale]/layout.tsx"
|
|
619
|
+
import type { ReactNode } from "react";
|
|
620
|
+
import { locales } from "@/i18n";
|
|
621
|
+
import {
|
|
622
|
+
getLocaleDirection,
|
|
623
|
+
unstable_setRequestLocale,
|
|
624
|
+
} from "next-intl/server";
|
|
625
|
+
|
|
626
|
+
export const dynamic = "force-static";
|
|
627
|
+
|
|
628
|
+
export function generateStaticParams() {
|
|
629
|
+
return locales.map((locale) => ({ locale }));
|
|
630
|
+
}
|
|
521
631
|
|
|
522
632
|
export default async function LocaleLayout({
|
|
523
633
|
children,
|
|
524
634
|
params,
|
|
525
635
|
}: {
|
|
526
|
-
children:
|
|
527
|
-
params: { locale: string }
|
|
636
|
+
children: ReactNode;
|
|
637
|
+
params: Promise<{ locale: string }>;
|
|
528
638
|
}) {
|
|
529
|
-
const { locale } = params;
|
|
639
|
+
const { locale } = await params;
|
|
530
640
|
|
|
531
641
|
// Set the active request locale for this server render (RSC)
|
|
532
642
|
unstable_setRequestLocale(locale);
|
|
533
643
|
|
|
534
|
-
|
|
535
|
-
// (see next-intl docs). Here we only push a subset to the client
|
|
536
|
-
// that's needed for client components (payload optimization).
|
|
537
|
-
const messages = await getMessages();
|
|
538
|
-
const clientMessages = pick(messages, ["common", "about"]);
|
|
644
|
+
const dir = getLocaleDirection(locale);
|
|
539
645
|
|
|
540
646
|
return (
|
|
541
|
-
<html lang={locale}>
|
|
542
|
-
<body>
|
|
543
|
-
<NextIntlClientProvider locale={locale} messages={clientMessages}>
|
|
544
|
-
{children}
|
|
545
|
-
</NextIntlClientProvider>
|
|
546
|
-
</body>
|
|
647
|
+
<html lang={locale} dir={dir}>
|
|
648
|
+
<body>{children}</body>
|
|
547
649
|
</html>
|
|
548
650
|
);
|
|
549
651
|
}
|
|
550
652
|
```
|
|
551
653
|
|
|
552
654
|
```tsx fileName="src/app/[locale]/about/page.tsx"
|
|
553
|
-
import { getTranslations } from "next-intl/server";
|
|
554
|
-
import {
|
|
655
|
+
import { getTranslations, getMessages, getFormatter } from "next-intl/server";
|
|
656
|
+
import { NextIntlClientProvider } from "next-intl";
|
|
657
|
+
import pick from "lodash/pick";
|
|
658
|
+
import ServerComponent from "@/components/ServerComponent";
|
|
659
|
+
import ClientComponentExample from "@/components/ClientComponentExample";
|
|
555
660
|
|
|
556
|
-
export
|
|
661
|
+
export const dynamic = "force-static";
|
|
662
|
+
|
|
663
|
+
export default async function AboutPage({
|
|
557
664
|
params,
|
|
558
665
|
}: {
|
|
559
|
-
params: { locale: string }
|
|
666
|
+
params: Promise<{ locale: string }>;
|
|
560
667
|
}) {
|
|
561
|
-
|
|
562
|
-
|
|
668
|
+
const { locale } = await params;
|
|
669
|
+
|
|
670
|
+
// Messages are loaded server-side. Push only what's needed to the client.
|
|
671
|
+
const messages = await getMessages();
|
|
672
|
+
const clientMessages = pick(messages, ["common", "about"]);
|
|
673
|
+
|
|
674
|
+
// Strictly server-side translations/formatting
|
|
675
|
+
const tAbout = await getTranslations("about");
|
|
676
|
+
const tCounter = await getTranslations("about.counter");
|
|
677
|
+
const format = await getFormatter();
|
|
678
|
+
|
|
679
|
+
const initialFormattedCount = format.number(0);
|
|
563
680
|
|
|
564
681
|
return (
|
|
565
|
-
<
|
|
566
|
-
<
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
682
|
+
<NextIntlClientProvider locale={locale} messages={clientMessages}>
|
|
683
|
+
<main>
|
|
684
|
+
<h1>{tAbout("title")}</h1>
|
|
685
|
+
<ClientComponentExample />
|
|
686
|
+
<ServerComponent
|
|
687
|
+
formattedCount={initialFormattedCount}
|
|
688
|
+
label={tCounter("label")}
|
|
689
|
+
increment={tCounter("increment")}
|
|
690
|
+
/>
|
|
691
|
+
</main>
|
|
692
|
+
</NextIntlClientProvider>
|
|
570
693
|
);
|
|
571
694
|
}
|
|
572
695
|
```
|
|
@@ -575,12 +698,16 @@ export default async function LandingPage({
|
|
|
575
698
|
<TabItem label="intlayer" value="intlayer">
|
|
576
699
|
|
|
577
700
|
```tsx fileName="intlayer.config.ts"
|
|
578
|
-
|
|
701
|
+
import { type IntlayerConfig, Locales } from "intlayer";
|
|
702
|
+
|
|
703
|
+
const config: IntlayerConfig = {
|
|
579
704
|
internationalization: {
|
|
580
|
-
locales: [
|
|
581
|
-
defaultLocale:
|
|
705
|
+
locales: [Locales.ENGLISH, Locales.FRENCH, Locales.SPANISH],
|
|
706
|
+
defaultLocale: Locales.ENGLISH,
|
|
582
707
|
},
|
|
583
708
|
};
|
|
709
|
+
|
|
710
|
+
export default config;
|
|
584
711
|
```
|
|
585
712
|
|
|
586
713
|
```tsx fileName="src/app/[locale]/layout.tsx"
|
|
@@ -593,14 +720,16 @@ import {
|
|
|
593
720
|
|
|
594
721
|
export const dynamic = "force-static";
|
|
595
722
|
|
|
596
|
-
const
|
|
723
|
+
const LocaleLayout: NextLayoutIntlayer = async ({ children, params }) => {
|
|
597
724
|
const { locale } = await params;
|
|
598
725
|
|
|
599
726
|
return (
|
|
600
727
|
<html lang={locale} dir={getHTMLTextDir(locale)}>
|
|
601
|
-
<
|
|
602
|
-
{
|
|
603
|
-
|
|
728
|
+
<body>
|
|
729
|
+
<IntlayerClientProvider locale={locale}>
|
|
730
|
+
{children}
|
|
731
|
+
</IntlayerClientProvider>
|
|
732
|
+
</body>
|
|
604
733
|
</html>
|
|
605
734
|
);
|
|
606
735
|
};
|
|
@@ -652,10 +781,12 @@ Let's take an example of a client component rendering a counter.
|
|
|
652
781
|
<Tab defaultTab="next-intl" group='techno'>
|
|
653
782
|
<TabItem label="next-i18next" value="next-i18next">
|
|
654
783
|
|
|
655
|
-
**Translations (
|
|
784
|
+
**Translations (one JSON per namespace under `src/locales/...`)**
|
|
656
785
|
|
|
657
|
-
```json fileName="
|
|
786
|
+
```json fileName="src/locales/en/about.json"
|
|
658
787
|
{
|
|
788
|
+
"title": "About",
|
|
789
|
+
"description": "About page description",
|
|
659
790
|
"counter": {
|
|
660
791
|
"label": "Counter",
|
|
661
792
|
"increment": "Increment"
|
|
@@ -663,8 +794,10 @@ Let's take an example of a client component rendering a counter.
|
|
|
663
794
|
}
|
|
664
795
|
```
|
|
665
796
|
|
|
666
|
-
```json fileName="
|
|
797
|
+
```json fileName="src/locales/fr/about.json"
|
|
667
798
|
{
|
|
799
|
+
"title": "À propos",
|
|
800
|
+
"description": "Description de la page À propos",
|
|
668
801
|
"counter": {
|
|
669
802
|
"label": "Compteur",
|
|
670
803
|
"increment": "Incrémenter"
|
|
@@ -672,19 +805,18 @@ Let's take an example of a client component rendering a counter.
|
|
|
672
805
|
}
|
|
673
806
|
```
|
|
674
807
|
|
|
675
|
-
**Client component**
|
|
808
|
+
**Client component (loads only the required namespace)**
|
|
676
809
|
|
|
677
|
-
```tsx fileName="src/components/
|
|
810
|
+
```tsx fileName="src/components/ClientComponent.tsx"
|
|
678
811
|
"use client";
|
|
679
812
|
|
|
680
|
-
import React, {
|
|
681
|
-
import { useTranslation } from "
|
|
813
|
+
import React, { useState } from "react";
|
|
814
|
+
import { useTranslation } from "react-i18next";
|
|
682
815
|
|
|
683
|
-
const
|
|
816
|
+
const ClientComponent = () => {
|
|
684
817
|
const { t, i18n } = useTranslation("about");
|
|
685
818
|
const [count, setCount] = useState(0);
|
|
686
819
|
|
|
687
|
-
// next-i18next doesn't expose useNumber; use Intl.NumberFormat
|
|
688
820
|
const numberFormat = new Intl.NumberFormat(i18n.language);
|
|
689
821
|
|
|
690
822
|
return (
|
|
@@ -692,17 +824,19 @@ const ClientComponentExample = () => {
|
|
|
692
824
|
<p>{numberFormat.format(count)}</p>
|
|
693
825
|
<button
|
|
694
826
|
aria-label={t("counter.label")}
|
|
695
|
-
onClick={() => setCount((
|
|
827
|
+
onClick={() => setCount((c) => c + 1)}
|
|
696
828
|
>
|
|
697
829
|
{t("counter.increment")}
|
|
698
830
|
</button>
|
|
699
831
|
</div>
|
|
700
832
|
);
|
|
701
833
|
};
|
|
834
|
+
|
|
835
|
+
export default ClientComponent;
|
|
702
836
|
```
|
|
703
837
|
|
|
704
|
-
>
|
|
705
|
-
>
|
|
838
|
+
> Ensure the page/provider includes only the namespaces you need (e.g. `about`).
|
|
839
|
+
> If you use React < 19, memoize heavy formatters like `Intl.NumberFormat`.
|
|
706
840
|
|
|
707
841
|
</TabItem>
|
|
708
842
|
<TabItem label="next-intl" value="next-intl">
|
|
@@ -827,17 +961,15 @@ We will take the case of a UI component. This component is a server component, a
|
|
|
827
961
|
<Tab defaultTab="next-intl" group='techno'>
|
|
828
962
|
<TabItem label="next-i18next" value="next-i18next">
|
|
829
963
|
|
|
830
|
-
```tsx fileName="src/
|
|
831
|
-
import type { GetStaticProps } from "next";
|
|
832
|
-
import { useTranslation } from "next-i18next";
|
|
833
|
-
|
|
964
|
+
```tsx fileName="src/components/ServerComponent.tsx"
|
|
834
965
|
type ServerComponentProps = {
|
|
966
|
+
t: (key: string) => string;
|
|
967
|
+
locale: string;
|
|
835
968
|
count: number;
|
|
836
969
|
};
|
|
837
970
|
|
|
838
|
-
const ServerComponent = ({ count }: ServerComponentProps) => {
|
|
839
|
-
const
|
|
840
|
-
const formatted = new Intl.NumberFormat(i18n.language).format(count);
|
|
971
|
+
const ServerComponent = ({ t, locale, count }: ServerComponentProps) => {
|
|
972
|
+
const formatted = new Intl.NumberFormat(locale).format(count);
|
|
841
973
|
|
|
842
974
|
return (
|
|
843
975
|
<div>
|
|
@@ -846,6 +978,8 @@ const ServerComponent = ({ count }: ServerComponentProps) => {
|
|
|
846
978
|
</div>
|
|
847
979
|
);
|
|
848
980
|
};
|
|
981
|
+
|
|
982
|
+
export default ServerComponent;
|
|
849
983
|
```
|
|
850
984
|
|
|
851
985
|
</TabItem>
|
|
@@ -853,20 +987,29 @@ const ServerComponent = ({ count }: ServerComponentProps) => {
|
|
|
853
987
|
|
|
854
988
|
```tsx fileName="src/components/ServerComponent.tsx"
|
|
855
989
|
type ServerComponentProps = {
|
|
856
|
-
count: number;
|
|
857
990
|
t: (key: string) => string;
|
|
991
|
+
locale: string;
|
|
992
|
+
count: number;
|
|
993
|
+
formatter: Intl.NumberFormat;
|
|
858
994
|
};
|
|
859
995
|
|
|
860
|
-
const ServerComponent = ({
|
|
861
|
-
|
|
996
|
+
const ServerComponent = ({
|
|
997
|
+
t,
|
|
998
|
+
locale,
|
|
999
|
+
count,
|
|
1000
|
+
formatter,
|
|
1001
|
+
}: ServerComponentProps) => {
|
|
1002
|
+
const formatted = formatter.format(count);
|
|
862
1003
|
|
|
863
1004
|
return (
|
|
864
1005
|
<div>
|
|
865
1006
|
<p>{formatted}</p>
|
|
866
|
-
<button aria-label={t("label")}>{t("increment")}</button>
|
|
1007
|
+
<button aria-label={t("counter.label")}>{t("counter.increment")}</button>
|
|
867
1008
|
</div>
|
|
868
1009
|
);
|
|
869
1010
|
};
|
|
1011
|
+
|
|
1012
|
+
export default ServerComponent;
|
|
870
1013
|
```
|
|
871
1014
|
|
|
872
1015
|
> As the server component cannot be async, you need to pass the translations and formatter function as props.
|
|
@@ -875,7 +1018,7 @@ const ServerComponent = ({ t, count }: ServerComponentProps) => {
|
|
|
875
1018
|
>
|
|
876
1019
|
> - `import { getTranslations, getFormatter } from "next-intl/server";`
|
|
877
1020
|
> - `const t = await getTranslations("about.counter");`
|
|
878
|
-
> - `const
|
|
1021
|
+
> - `const formatter = await getFormatter().then((formatter) => formatter.number());`
|
|
879
1022
|
|
|
880
1023
|
</TabItem>
|
|
881
1024
|
<TabItem label="intlayer" value="intlayer">
|
|
@@ -945,17 +1088,20 @@ export function abs(locale: string, path: string) {
|
|
|
945
1088
|
import type { Metadata } from "next";
|
|
946
1089
|
import { locales, defaultLocale, localizedPath } from "@/i18n.config";
|
|
947
1090
|
|
|
948
|
-
|
|
1091
|
+
type GenerateMetadataParams = {
|
|
1092
|
+
params: Promise<{
|
|
1093
|
+
locale: string;
|
|
1094
|
+
}>;
|
|
1095
|
+
};
|
|
1096
|
+
|
|
1097
|
+
export const generateMetadata = async ({
|
|
949
1098
|
params,
|
|
950
|
-
}: {
|
|
951
|
-
|
|
952
|
-
}): Promise<Metadata> {
|
|
953
|
-
const { locale } = params;
|
|
1099
|
+
}: GenerateMetadataParams): Promise<Metadata> => {
|
|
1100
|
+
const { locale } = await params;
|
|
954
1101
|
|
|
955
|
-
//
|
|
956
|
-
const messages = (
|
|
957
|
-
|
|
958
|
-
).default;
|
|
1102
|
+
// Import the correct JSON bundle from src/locales
|
|
1103
|
+
const messages = (await import("@/locales/" + locale + "/about.json"))
|
|
1104
|
+
.default;
|
|
959
1105
|
|
|
960
1106
|
const languages = Object.fromEntries(
|
|
961
1107
|
locales.map((locale) => [locale, localizedPath(locale, "/about")])
|
|
@@ -969,7 +1115,7 @@ export async function generateMetadata({
|
|
|
969
1115
|
languages: { ...languages, "x-default": "/about" },
|
|
970
1116
|
},
|
|
971
1117
|
};
|
|
972
|
-
}
|
|
1118
|
+
};
|
|
973
1119
|
|
|
974
1120
|
export default async function AboutPage() {
|
|
975
1121
|
return <h1>About</h1>;
|
|
@@ -980,7 +1126,7 @@ export default async function AboutPage() {
|
|
|
980
1126
|
import type { MetadataRoute } from "next";
|
|
981
1127
|
import { locales, defaultLocale, abs } from "@/i18n.config";
|
|
982
1128
|
|
|
983
|
-
export
|
|
1129
|
+
export const sitemap = (): MetadataRoute.Sitemap => {
|
|
984
1130
|
const languages = Object.fromEntries(
|
|
985
1131
|
locales.map((locale) => [locale, abs(locale, "/about")])
|
|
986
1132
|
);
|
|
@@ -993,7 +1139,7 @@ export default function sitemap(): MetadataRoute.Sitemap {
|
|
|
993
1139
|
alternates: { languages },
|
|
994
1140
|
},
|
|
995
1141
|
];
|
|
996
|
-
}
|
|
1142
|
+
};
|
|
997
1143
|
```
|
|
998
1144
|
|
|
999
1145
|
```ts fileName="src/app/robots.ts"
|
|
@@ -1009,7 +1155,7 @@ const expandAllLocales = (path: string) => [
|
|
|
1009
1155
|
.map((locale) => localizedPath(locale, path)),
|
|
1010
1156
|
];
|
|
1011
1157
|
|
|
1012
|
-
export
|
|
1158
|
+
export const robots = (): MetadataRoute.Robots => {
|
|
1013
1159
|
const disallow = [
|
|
1014
1160
|
...expandAllLocales("/dashboard"),
|
|
1015
1161
|
...expandAllLocales("/admin"),
|
|
@@ -1020,7 +1166,7 @@ export default function robots(): MetadataRoute.Robots {
|
|
|
1020
1166
|
host: ORIGIN,
|
|
1021
1167
|
sitemap: ORIGIN + "/sitemap.xml",
|
|
1022
1168
|
};
|
|
1023
|
-
}
|
|
1169
|
+
};
|
|
1024
1170
|
```
|
|
1025
1171
|
|
|
1026
1172
|
</TabItem>
|
|
@@ -1031,16 +1177,20 @@ import type { Metadata } from "next";
|
|
|
1031
1177
|
import { locales, defaultLocale } from "@/i18n";
|
|
1032
1178
|
import { getTranslations } from "next-intl/server";
|
|
1033
1179
|
|
|
1034
|
-
|
|
1180
|
+
const localizedPath = (locale: string, path: string) => {
|
|
1035
1181
|
return locale === defaultLocale ? path : "/" + locale + path;
|
|
1036
|
-
}
|
|
1182
|
+
};
|
|
1037
1183
|
|
|
1038
|
-
|
|
1184
|
+
type GenerateMetadataParams = {
|
|
1185
|
+
params: Promise<{
|
|
1186
|
+
locale: string;
|
|
1187
|
+
}>;
|
|
1188
|
+
};
|
|
1189
|
+
|
|
1190
|
+
export const generateMetadata = async ({
|
|
1039
1191
|
params,
|
|
1040
|
-
}: {
|
|
1041
|
-
|
|
1042
|
-
}): Promise<Metadata> {
|
|
1043
|
-
const { locale } = params;
|
|
1192
|
+
}: GenerateMetadataParams): Promise<Metadata> => {
|
|
1193
|
+
const { locale } = await params;
|
|
1044
1194
|
const t = await getTranslations({ locale, namespace: "about" });
|
|
1045
1195
|
|
|
1046
1196
|
const url = "/about";
|
|
@@ -1056,7 +1206,7 @@ export async function generateMetadata({
|
|
|
1056
1206
|
languages: { ...languages, "x-default": url },
|
|
1057
1207
|
},
|
|
1058
1208
|
};
|
|
1059
|
-
}
|
|
1209
|
+
};
|
|
1060
1210
|
|
|
1061
1211
|
// ... Rest of the page code
|
|
1062
1212
|
```
|
|
@@ -1070,7 +1220,7 @@ const origin = "https://example.com";
|
|
|
1070
1220
|
const formatterLocalizedPath = (locale: string, path: string) =>
|
|
1071
1221
|
locale === defaultLocale ? origin + path : origin + "/" + locale + path;
|
|
1072
1222
|
|
|
1073
|
-
export
|
|
1223
|
+
export const sitemap = (): MetadataRoute.Sitemap => {
|
|
1074
1224
|
const aboutLanguages = Object.fromEntries(
|
|
1075
1225
|
locales.map((l) => [l, formatterLocalizedPath(l, "/about")])
|
|
1076
1226
|
);
|
|
@@ -1084,7 +1234,7 @@ export default function sitemap(): MetadataRoute.Sitemap {
|
|
|
1084
1234
|
alternates: { languages: aboutLanguages },
|
|
1085
1235
|
},
|
|
1086
1236
|
];
|
|
1087
|
-
}
|
|
1237
|
+
};
|
|
1088
1238
|
```
|
|
1089
1239
|
|
|
1090
1240
|
```tsx fileName="src/app/robots.ts"
|
|
@@ -1099,7 +1249,7 @@ const withAllLocales = (path: string) => [
|
|
|
1099
1249
|
.map((locale) => "/" + locale + path),
|
|
1100
1250
|
];
|
|
1101
1251
|
|
|
1102
|
-
export
|
|
1252
|
+
export const robots = (): MetadataRoute.Robots => {
|
|
1103
1253
|
const disallow = [
|
|
1104
1254
|
...withAllLocales("/dashboard"),
|
|
1105
1255
|
...withAllLocales("/admin"),
|
|
@@ -1110,7 +1260,7 @@ export default function robots(): MetadataRoute.Robots {
|
|
|
1110
1260
|
host: origin,
|
|
1111
1261
|
sitemap: origin + "/sitemap.xml",
|
|
1112
1262
|
};
|
|
1113
|
-
}
|
|
1263
|
+
};
|
|
1114
1264
|
```
|
|
1115
1265
|
|
|
1116
1266
|
</TabItem>
|
|
@@ -1181,7 +1331,111 @@ export default robots;
|
|
|
1181
1331
|
|
|
1182
1332
|
> Intlayer provides a `getMultilingualUrls` function to generate multilingual URLs for your sitemap.
|
|
1183
1333
|
|
|
1184
|
-
|
|
1334
|
+
### Middleware for locale routing
|
|
1335
|
+
|
|
1336
|
+
<Tab defaultTab="next-intl" group='techno'>
|
|
1337
|
+
<TabItem label="next-i18next" value="next-i18next">
|
|
1338
|
+
|
|
1339
|
+
Add a middleware to handle locale detection and routing:
|
|
1340
|
+
|
|
1341
|
+
```ts fileName="src/middleware.ts"
|
|
1342
|
+
import { NextResponse, type NextRequest } from "next/server";
|
|
1343
|
+
import { defaultLocale, locales } from "@/i18n.config";
|
|
1344
|
+
|
|
1345
|
+
const PUBLIC_FILE = /\.[^/]+$/; // exclude files with extensions
|
|
1346
|
+
|
|
1347
|
+
export function middleware(request: NextRequest) {
|
|
1348
|
+
const { pathname } = request.nextUrl;
|
|
1349
|
+
|
|
1350
|
+
if (
|
|
1351
|
+
pathname.startsWith("/_next") ||
|
|
1352
|
+
pathname.startsWith("/api") ||
|
|
1353
|
+
pathname.startsWith("/static") ||
|
|
1354
|
+
PUBLIC_FILE.test(pathname)
|
|
1355
|
+
) {
|
|
1356
|
+
return;
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
const hasLocale = locales.some(
|
|
1360
|
+
(l) => pathname === "/" + l || pathname.startsWith("/" + l + "/")
|
|
1361
|
+
);
|
|
1362
|
+
if (!hasLocale) {
|
|
1363
|
+
const locale = defaultLocale;
|
|
1364
|
+
const url = request.nextUrl.clone();
|
|
1365
|
+
url.pathname = "/" + locale + (pathname === "/" ? "" : pathname);
|
|
1366
|
+
return NextResponse.redirect(url);
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
export const config = {
|
|
1371
|
+
matcher: [
|
|
1372
|
+
// Match all paths except the ones starting with these and files with an extension
|
|
1373
|
+
"/((?!api|_next|static|.*\\..*).*)",
|
|
1374
|
+
],
|
|
1375
|
+
};
|
|
1376
|
+
```
|
|
1377
|
+
|
|
1378
|
+
</TabItem>
|
|
1379
|
+
<TabItem label="next-intl" value="next-intl">
|
|
1380
|
+
|
|
1381
|
+
Add a middleware to handle locale detection and routing:
|
|
1382
|
+
|
|
1383
|
+
```ts fileName="src/middleware.ts"
|
|
1384
|
+
import createMiddleware from "next-intl/middleware";
|
|
1385
|
+
import { locales, defaultLocale } from "@/i18n";
|
|
1386
|
+
|
|
1387
|
+
export default createMiddleware({
|
|
1388
|
+
locales: [...locales],
|
|
1389
|
+
defaultLocale,
|
|
1390
|
+
localeDetection: true,
|
|
1391
|
+
});
|
|
1392
|
+
|
|
1393
|
+
export const config = {
|
|
1394
|
+
// Skip API, Next internals and static assets
|
|
1395
|
+
matcher: ["/((?!api|_next|.*\\..*).*)"],
|
|
1396
|
+
};
|
|
1397
|
+
```
|
|
1398
|
+
|
|
1399
|
+
</TabItem>
|
|
1400
|
+
<TabItem label="intlayer" value="intlayer">
|
|
1401
|
+
|
|
1402
|
+
Intlayer provides built-in middleware handling through the `next-intlayer` package configuration.
|
|
1403
|
+
|
|
1404
|
+
</TabItem>
|
|
1405
|
+
</Tab>
|
|
1406
|
+
|
|
1407
|
+
### Setup checklist and good practices
|
|
1408
|
+
|
|
1409
|
+
<Tab defaultTab="next-intl" group='techno'>
|
|
1410
|
+
<TabItem label="next-i18next" value="next-i18next">
|
|
1411
|
+
|
|
1412
|
+
- Ensure `lang` and `dir` are set on the root `<html>` in `src/app/[locale]/layout.tsx`.
|
|
1413
|
+
- Split translations into namespaces (for example `common.json`, `about.json`) under `src/locales/<locale>/`.
|
|
1414
|
+
- Only load required namespaces in client components using `useTranslation('<ns>')` and by scoping `I18nProvider` with the same namespaces.
|
|
1415
|
+
- Keep pages static when possible: export `export const dynamic = 'force-static'` on pages; set `dynamicParams = false` and implement `generateStaticParams`.
|
|
1416
|
+
- Use sync server components nested under client boundaries by passing already-computed strings or the `t` function and the `locale`.
|
|
1417
|
+
- For SEO, set `alternates.languages` in metadata, list localized URLs in `sitemap.ts`, and disallow duplicate localized routes in `robots.ts`.
|
|
1418
|
+
- Prefer locale-aware formatters (e.g., `Intl.NumberFormat(locale)`) and memoize them on the client if using React < 19.
|
|
1419
|
+
|
|
1420
|
+
</TabItem>
|
|
1421
|
+
<TabItem label="next-intl" value="next-intl">
|
|
1422
|
+
|
|
1423
|
+
- **Set html `lang` and `dir`**: In `src/app/[locale]/layout.tsx`, compute `dir` via `getLocaleDirection(locale)` and set `<html lang={locale} dir={dir}>`.
|
|
1424
|
+
- **Split messages by namespace**: Organize JSON per locale and namespace (e.g., `common.json`, `about.json`).
|
|
1425
|
+
- **Minimize client payload**: On pages, send only required namespaces to `NextIntlClientProvider` (e.g., `pick(messages, ['common', 'about'])`).
|
|
1426
|
+
- **Prefer static pages**: Export `export const dynamic = 'force-static'` and generate static params for all `locales`.
|
|
1427
|
+
- **Synchronous server components**: Keep server components sync by passing precomputed strings (translated labels, formatted numbers) rather than async calls or non-serializable functions.
|
|
1428
|
+
|
|
1429
|
+
</TabItem>
|
|
1430
|
+
<TabItem label="intlayer" value="intlayer">
|
|
1431
|
+
|
|
1432
|
+
- **Modular content**: Co-locate content dictionaries with components using `.content.{ts|js|json}` files.
|
|
1433
|
+
- **Type safety**: Leverage TypeScript integration for compile-time content validation.
|
|
1434
|
+
- **Build-time optimization**: Use Intlayer's build tools for automatic tree-shaking and bundle optimization.
|
|
1435
|
+
- **Integrated tooling**: Take advantage of built-in routing, SEO helpers, and visual editor support.
|
|
1436
|
+
|
|
1437
|
+
</TabItem>
|
|
1438
|
+
</Tab>
|
|
1185
1439
|
|
|
1186
1440
|
---
|
|
1187
1441
|
|