@intlayer/docs 6.1.5 → 6.1.6-canary.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/blog/ar/next-i18next_vs_next-intl_vs_intlayer.md +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 +403 -140
- 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 +16 -0
- package/dist/cjs/generated/docs.entry.cjs.map +1 -1
- package/dist/esm/generated/docs.entry.mjs +16 -0
- package/dist/esm/generated/docs.entry.mjs.map +1 -1
- package/dist/types/generated/docs.entry.d.ts +1 -0
- package/dist/types/generated/docs.entry.d.ts.map +1 -1
- package/docs/ar/component_i18n.md +186 -0
- package/docs/ar/vs_code_extension.md +48 -109
- package/docs/de/component_i18n.md +186 -0
- package/docs/de/vs_code_extension.md +46 -107
- package/docs/en/component_i18n.md +186 -0
- package/docs/en/intlayer_with_nextjs_14.md +18 -1
- package/docs/en/intlayer_with_nextjs_15.md +18 -1
- package/docs/en/vs_code_extension.md +24 -114
- package/docs/en-GB/component_i18n.md +186 -0
- package/docs/en-GB/vs_code_extension.md +42 -103
- package/docs/es/component_i18n.md +182 -0
- package/docs/es/vs_code_extension.md +53 -114
- package/docs/fr/component_i18n.md +186 -0
- package/docs/fr/vs_code_extension.md +50 -111
- package/docs/hi/component_i18n.md +186 -0
- package/docs/hi/vs_code_extension.md +49 -110
- package/docs/it/component_i18n.md +186 -0
- package/docs/it/vs_code_extension.md +50 -111
- package/docs/ja/component_i18n.md +186 -0
- package/docs/ja/vs_code_extension.md +50 -111
- package/docs/ko/component_i18n.md +186 -0
- package/docs/ko/vs_code_extension.md +48 -109
- package/docs/pt/component_i18n.md +186 -0
- package/docs/pt/vs_code_extension.md +46 -107
- package/docs/ru/component_i18n.md +186 -0
- package/docs/ru/vs_code_extension.md +48 -109
- package/docs/tr/component_i18n.md +186 -0
- package/docs/tr/vs_code_extension.md +54 -115
- package/docs/zh/component_i18n.md +186 -0
- package/docs/zh/vs_code_extension.md +51 -105
- package/package.json +11 -11
- package/src/generated/docs.entry.ts +16 -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.
|
|
@@ -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
|
|
|
@@ -352,25 +356,25 @@ The app structure is important to ensure good maintainability for your codebase.
|
|
|
352
356
|
|
|
353
357
|
```bash
|
|
354
358
|
.
|
|
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
|
|
359
|
+
├── i18n.config.ts
|
|
367
360
|
└── src
|
|
368
|
-
├──
|
|
361
|
+
├── locales
|
|
362
|
+
│ ├── en
|
|
363
|
+
│ │ ├── common.json
|
|
364
|
+
│ │ └── about.json
|
|
365
|
+
│ └── fr
|
|
366
|
+
│ ├── common.json
|
|
367
|
+
│ └── about.json
|
|
369
368
|
├── app
|
|
370
|
-
│
|
|
369
|
+
│ ├── i18n
|
|
370
|
+
│ │ └── server.ts
|
|
371
|
+
│ └── [locale]
|
|
372
|
+
│ ├── layout.tsx
|
|
373
|
+
│ └── about.tsx
|
|
371
374
|
└── components
|
|
372
|
-
|
|
373
|
-
|
|
375
|
+
├── I18nProvider.tsx
|
|
376
|
+
├── ClientComponent.tsx
|
|
377
|
+
└── ServerComponent.tsx
|
|
374
378
|
```
|
|
375
379
|
|
|
376
380
|
</TabItem>
|
|
@@ -378,6 +382,7 @@ The app structure is important to ensure good maintainability for your codebase.
|
|
|
378
382
|
|
|
379
383
|
```bash
|
|
380
384
|
.
|
|
385
|
+
├── i18n.ts
|
|
381
386
|
├── locales
|
|
382
387
|
│ ├── en
|
|
383
388
|
│ │ ├── home.json
|
|
@@ -388,11 +393,13 @@ The app structure is important to ensure good maintainability for your codebase.
|
|
|
388
393
|
│ └── es
|
|
389
394
|
│ ├── home.json
|
|
390
395
|
│ └── navbar.json
|
|
391
|
-
├── i18n.ts
|
|
392
396
|
└── src
|
|
393
397
|
├── middleware.ts
|
|
394
398
|
├── app
|
|
395
|
-
│
|
|
399
|
+
│ ├── i18n
|
|
400
|
+
│ │ └── server.ts
|
|
401
|
+
│ └── [locale]
|
|
402
|
+
│ └── home.tsx
|
|
396
403
|
└── components
|
|
397
404
|
└── Navbar
|
|
398
405
|
└── index.tsx
|
|
@@ -407,9 +414,11 @@ The app structure is important to ensure good maintainability for your codebase.
|
|
|
407
414
|
└── src
|
|
408
415
|
├── middleware.ts
|
|
409
416
|
├── app
|
|
410
|
-
│ └──
|
|
411
|
-
│
|
|
412
|
-
│ └──
|
|
417
|
+
│ └── [locale]
|
|
418
|
+
│ ├── layout.tsx
|
|
419
|
+
│ └── home
|
|
420
|
+
│ ├── index.tsx
|
|
421
|
+
│ └── index.content.ts
|
|
413
422
|
└── components
|
|
414
423
|
└── Navbar
|
|
415
424
|
├── index.tsx
|
|
@@ -432,141 +441,276 @@ How the library handles content loading is important.
|
|
|
432
441
|
<Tab defaultTab="next-intl" group='techno'>
|
|
433
442
|
<TabItem label="next-i18next" value="next-i18next">
|
|
434
443
|
|
|
435
|
-
```
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
},
|
|
441
|
-
};
|
|
442
|
-
```
|
|
444
|
+
```ts fileName="i18n.config.ts"
|
|
445
|
+
export const locales = ["en", "fr"] as const;
|
|
446
|
+
export type Locale = (typeof locales)[number];
|
|
447
|
+
|
|
448
|
+
export const defaultLocale: Locale = "en";
|
|
443
449
|
|
|
444
|
-
|
|
445
|
-
|
|
450
|
+
export const rtlLocales = ["ar", "he", "fa", "ur"] as const;
|
|
451
|
+
export const isRtl = (locale: string) =>
|
|
452
|
+
(rtlLocales as readonly string[]).includes(locale);
|
|
446
453
|
|
|
447
|
-
|
|
454
|
+
export function localizedPath(locale: string, path: string) {
|
|
455
|
+
return locale === defaultLocale ? path : "/" + locale + path;
|
|
456
|
+
}
|
|
448
457
|
|
|
449
|
-
|
|
458
|
+
const ORIGIN = "https://example.com";
|
|
459
|
+
export function abs(locale: string, path: string) {
|
|
460
|
+
return ORIGIN + localizedPath(locale, path);
|
|
461
|
+
}
|
|
450
462
|
```
|
|
451
463
|
|
|
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";
|
|
464
|
+
```ts fileName="src/app/i18n/server.ts"
|
|
457
465
|
import { createInstance } from "i18next";
|
|
458
|
-
import {
|
|
466
|
+
import { initReactI18next } from "react-i18next/initReactI18next";
|
|
467
|
+
import resourcesToBackend from "i18next-resources-to-backend";
|
|
468
|
+
import { defaultLocale } from "@/i18n.config";
|
|
469
|
+
|
|
470
|
+
// Load JSON resources from src/locales/<locale>/<namespace>.json
|
|
471
|
+
const backend = resourcesToBackend(
|
|
472
|
+
(locale: string, namespace: string) =>
|
|
473
|
+
import(`../../locales/${locale}/${namespace}.json`)
|
|
474
|
+
);
|
|
475
|
+
|
|
476
|
+
export async function initI18next(
|
|
477
|
+
locale: string,
|
|
478
|
+
namespaces: string[] = ["common"]
|
|
479
|
+
) {
|
|
480
|
+
const i18n = createInstance();
|
|
481
|
+
await i18n
|
|
482
|
+
.use(initReactI18next)
|
|
483
|
+
.use(backend)
|
|
484
|
+
.init({
|
|
485
|
+
lng: locale,
|
|
486
|
+
fallbackLng: defaultLocale,
|
|
487
|
+
ns: namespaces,
|
|
488
|
+
defaultNS: "common",
|
|
489
|
+
interpolation: { escapeValue: false },
|
|
490
|
+
react: { useSuspense: false },
|
|
491
|
+
});
|
|
492
|
+
return i18n;
|
|
493
|
+
}
|
|
494
|
+
```
|
|
459
495
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
const resources = await loadMessagesFor(locale); // your loader (JSON, etc.)
|
|
496
|
+
```tsx fileName="src/components/I18nProvider.tsx"
|
|
497
|
+
"use client";
|
|
463
498
|
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
499
|
+
import * as React from "react";
|
|
500
|
+
import { I18nextProvider } from "react-i18next";
|
|
501
|
+
import { createInstance } from "i18next";
|
|
502
|
+
import { initReactI18next } from "react-i18next/initReactI18next";
|
|
503
|
+
import resourcesToBackend from "i18next-resources-to-backend";
|
|
504
|
+
import { defaultLocale } from "@/i18n.config";
|
|
505
|
+
|
|
506
|
+
const backend = resourcesToBackend(
|
|
507
|
+
(locale: string, namespace: string) =>
|
|
508
|
+
import(`../../locales/${locale}/${namespace}.json`)
|
|
509
|
+
);
|
|
510
|
+
|
|
511
|
+
type Props = {
|
|
512
|
+
locale: string;
|
|
513
|
+
namespaces?: string[];
|
|
514
|
+
resources?: Record<string, any>; // { ns: bundle }
|
|
515
|
+
children: React.ReactNode;
|
|
516
|
+
};
|
|
517
|
+
|
|
518
|
+
export default function I18nProvider({
|
|
519
|
+
locale,
|
|
520
|
+
namespaces = ["common"],
|
|
521
|
+
resources,
|
|
522
|
+
children,
|
|
523
|
+
}: Props) {
|
|
524
|
+
const [i18n] = React.useState(() => {
|
|
525
|
+
const i = createInstance();
|
|
526
|
+
|
|
527
|
+
i.use(initReactI18next)
|
|
528
|
+
.use(backend)
|
|
529
|
+
.init({
|
|
530
|
+
lng: locale,
|
|
531
|
+
fallbackLng: defaultLocale,
|
|
532
|
+
ns: namespaces,
|
|
533
|
+
resources: resources ? { [locale]: resources } : undefined,
|
|
534
|
+
defaultNS: "common",
|
|
535
|
+
interpolation: { escapeValue: false },
|
|
536
|
+
react: { useSuspense: false },
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
return i;
|
|
472
540
|
});
|
|
473
541
|
|
|
474
|
-
|
|
542
|
+
return <I18nextProvider i18n={i18n}>{children}</I18nextProvider>;
|
|
543
|
+
}
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
```tsx fileName="src/app/[locale]/layout.tsx"
|
|
547
|
+
import type { ReactNode } from "react";
|
|
548
|
+
import { locales, defaultLocale, isRtl, type Locale } from "@/i18n.config";
|
|
549
|
+
|
|
550
|
+
export const dynamicParams = false;
|
|
551
|
+
|
|
552
|
+
export function generateStaticParams() {
|
|
553
|
+
return locales.map((locale) => ({ locale }));
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
export default function LocaleLayout({
|
|
557
|
+
children,
|
|
558
|
+
params,
|
|
559
|
+
}: {
|
|
560
|
+
children: ReactNode;
|
|
561
|
+
params: { locale: string };
|
|
562
|
+
}) {
|
|
563
|
+
const locale: Locale = (locales as readonly string[]).includes(params.locale)
|
|
564
|
+
? (params.locale as any)
|
|
565
|
+
: defaultLocale;
|
|
566
|
+
|
|
567
|
+
const dir = isRtl(locale) ? "rtl" : "ltr";
|
|
568
|
+
|
|
569
|
+
return (
|
|
570
|
+
<html lang={locale} dir={dir}>
|
|
571
|
+
<body>{children}</body>
|
|
572
|
+
</html>
|
|
573
|
+
);
|
|
574
|
+
}
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
```tsx fileName="src/app/[locale]/about.tsx"
|
|
578
|
+
import I18nProvider from "@/components/I18nProvider";
|
|
579
|
+
import { initI18next } from "@/app/i18n/server";
|
|
580
|
+
import type { Locale } from "@/i18n.config";
|
|
581
|
+
import ClientComponent from "@/components/ClientComponent";
|
|
582
|
+
import ServerComponent from "@/components/ServerComponent";
|
|
583
|
+
|
|
584
|
+
// Force static rendering for the page
|
|
585
|
+
export const dynamic = "force-static";
|
|
586
|
+
|
|
587
|
+
export default async function AboutPage({
|
|
588
|
+
params: { locale },
|
|
589
|
+
}: {
|
|
590
|
+
params: { locale: Locale };
|
|
591
|
+
}) {
|
|
592
|
+
const namespaces = ["common", "about"] as const;
|
|
593
|
+
|
|
594
|
+
const i18n = await initI18next(locale, [...namespaces]);
|
|
595
|
+
const tAbout = i18n.getFixedT(locale, "about");
|
|
475
596
|
|
|
476
597
|
return (
|
|
477
|
-
<
|
|
598
|
+
<I18nProvider locale={locale} namespaces={[...namespaces]}>
|
|
478
599
|
<main>
|
|
479
|
-
<h1>{
|
|
600
|
+
<h1>{tAbout("title")}</h1>
|
|
601
|
+
|
|
480
602
|
<ClientComponent />
|
|
481
|
-
<ServerComponent />
|
|
603
|
+
<ServerComponent t={tAbout} locale={locale} count={0} />
|
|
482
604
|
</main>
|
|
483
|
-
</
|
|
605
|
+
</I18nProvider>
|
|
484
606
|
);
|
|
485
607
|
}
|
|
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
608
|
```
|
|
496
609
|
|
|
497
610
|
</TabItem>
|
|
498
611
|
<TabItem label="next-intl" value="next-intl">
|
|
499
612
|
|
|
500
|
-
```tsx fileName="i18n.ts"
|
|
613
|
+
```tsx fileName="src/i18n.ts"
|
|
501
614
|
import { getRequestConfig } from "next-intl/server";
|
|
502
615
|
import { notFound } from "next/navigation";
|
|
503
616
|
|
|
504
|
-
|
|
505
|
-
const
|
|
617
|
+
export const locales = ["en", "fr", "es"] as const;
|
|
618
|
+
export const defaultLocale = "en" as const;
|
|
619
|
+
|
|
620
|
+
async function loadMessages(locale: string) {
|
|
621
|
+
// Load only the namespaces your layout/pages need
|
|
622
|
+
const [common, about] = await Promise.all([
|
|
623
|
+
import(`../locales/${locale}/common.json`).then((m) => m.default),
|
|
624
|
+
import(`../locales/${locale}/about.json`).then((m) => m.default),
|
|
625
|
+
]);
|
|
626
|
+
|
|
627
|
+
return { common, about } as const;
|
|
628
|
+
}
|
|
506
629
|
|
|
507
630
|
export default getRequestConfig(async ({ locale }) => {
|
|
508
|
-
// Validate that the incoming `locale` parameter is valid
|
|
509
631
|
if (!locales.includes(locale as any)) notFound();
|
|
510
632
|
|
|
511
633
|
return {
|
|
512
|
-
messages:
|
|
634
|
+
messages: await loadMessages(locale),
|
|
513
635
|
};
|
|
514
636
|
});
|
|
515
637
|
```
|
|
516
638
|
|
|
517
|
-
```tsx fileName="src/app/[locale]/
|
|
518
|
-
import {
|
|
519
|
-
import {
|
|
520
|
-
import
|
|
639
|
+
```tsx fileName="src/app/[locale]/layout.tsx"
|
|
640
|
+
import type { ReactNode } from "react";
|
|
641
|
+
import { locales } from "@/i18n";
|
|
642
|
+
import {
|
|
643
|
+
getLocaleDirection,
|
|
644
|
+
unstable_setRequestLocale,
|
|
645
|
+
} from "next-intl/server";
|
|
646
|
+
|
|
647
|
+
export const dynamic = "force-static";
|
|
648
|
+
|
|
649
|
+
export function generateStaticParams() {
|
|
650
|
+
return locales.map((locale) => ({ locale }));
|
|
651
|
+
}
|
|
521
652
|
|
|
522
653
|
export default async function LocaleLayout({
|
|
523
654
|
children,
|
|
524
655
|
params,
|
|
525
656
|
}: {
|
|
526
|
-
children:
|
|
527
|
-
params: { locale: string }
|
|
657
|
+
children: ReactNode;
|
|
658
|
+
params: Promise<{ locale: string }>;
|
|
528
659
|
}) {
|
|
529
|
-
const { locale } = params;
|
|
660
|
+
const { locale } = await params;
|
|
530
661
|
|
|
531
662
|
// Set the active request locale for this server render (RSC)
|
|
532
663
|
unstable_setRequestLocale(locale);
|
|
533
664
|
|
|
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"]);
|
|
665
|
+
const dir = getLocaleDirection(locale);
|
|
539
666
|
|
|
540
667
|
return (
|
|
541
|
-
<html lang={locale}>
|
|
542
|
-
<body>
|
|
543
|
-
<NextIntlClientProvider locale={locale} messages={clientMessages}>
|
|
544
|
-
{children}
|
|
545
|
-
</NextIntlClientProvider>
|
|
546
|
-
</body>
|
|
668
|
+
<html lang={locale} dir={dir}>
|
|
669
|
+
<body>{children}</body>
|
|
547
670
|
</html>
|
|
548
671
|
);
|
|
549
672
|
}
|
|
550
673
|
```
|
|
551
674
|
|
|
552
675
|
```tsx fileName="src/app/[locale]/about/page.tsx"
|
|
553
|
-
import { getTranslations } from "next-intl/server";
|
|
554
|
-
import {
|
|
676
|
+
import { getTranslations, getMessages, getFormatter } from "next-intl/server";
|
|
677
|
+
import { NextIntlClientProvider } from "next-intl";
|
|
678
|
+
import pick from "lodash/pick";
|
|
679
|
+
import ServerComponent from "@/components/ServerComponent";
|
|
680
|
+
import ClientComponentExample from "@/components/ClientComponentExample";
|
|
681
|
+
|
|
682
|
+
export const dynamic = "force-static";
|
|
555
683
|
|
|
556
|
-
export default async function
|
|
684
|
+
export default async function AboutPage({
|
|
557
685
|
params,
|
|
558
686
|
}: {
|
|
559
|
-
params: { locale: string }
|
|
687
|
+
params: Promise<{ locale: string }>;
|
|
560
688
|
}) {
|
|
561
|
-
|
|
562
|
-
|
|
689
|
+
const { locale } = await params;
|
|
690
|
+
|
|
691
|
+
// Messages are loaded server-side. Push only what's needed to the client.
|
|
692
|
+
const messages = await getMessages();
|
|
693
|
+
const clientMessages = pick(messages, ["common", "about"]);
|
|
694
|
+
|
|
695
|
+
// Strictly server-side translations/formatting
|
|
696
|
+
const tAbout = await getTranslations("about");
|
|
697
|
+
const tCounter = await getTranslations("about.counter");
|
|
698
|
+
const format = await getFormatter();
|
|
699
|
+
|
|
700
|
+
const initialFormattedCount = format.number(0);
|
|
563
701
|
|
|
564
702
|
return (
|
|
565
|
-
<
|
|
566
|
-
<
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
703
|
+
<NextIntlClientProvider locale={locale} messages={clientMessages}>
|
|
704
|
+
<main>
|
|
705
|
+
<h1>{tAbout("title")}</h1>
|
|
706
|
+
<ClientComponentExample />
|
|
707
|
+
<ServerComponent
|
|
708
|
+
formattedCount={initialFormattedCount}
|
|
709
|
+
label={tCounter("label")}
|
|
710
|
+
increment={tCounter("increment")}
|
|
711
|
+
/>
|
|
712
|
+
</main>
|
|
713
|
+
</NextIntlClientProvider>
|
|
570
714
|
);
|
|
571
715
|
}
|
|
572
716
|
```
|
|
@@ -575,12 +719,16 @@ export default async function LandingPage({
|
|
|
575
719
|
<TabItem label="intlayer" value="intlayer">
|
|
576
720
|
|
|
577
721
|
```tsx fileName="intlayer.config.ts"
|
|
578
|
-
|
|
722
|
+
import { type IntlayerConfig, Locales } from "intlayer";
|
|
723
|
+
|
|
724
|
+
const config: IntlayerConfig = {
|
|
579
725
|
internationalization: {
|
|
580
|
-
locales: [
|
|
581
|
-
defaultLocale:
|
|
726
|
+
locales: [Locales.ENGLISH, Locales.FRENCH, Locales.SPANISH],
|
|
727
|
+
defaultLocale: Locales.ENGLISH,
|
|
582
728
|
},
|
|
583
729
|
};
|
|
730
|
+
|
|
731
|
+
export default config;
|
|
584
732
|
```
|
|
585
733
|
|
|
586
734
|
```tsx fileName="src/app/[locale]/layout.tsx"
|
|
@@ -593,14 +741,16 @@ import {
|
|
|
593
741
|
|
|
594
742
|
export const dynamic = "force-static";
|
|
595
743
|
|
|
596
|
-
const
|
|
744
|
+
const LocaleLayout: NextLayoutIntlayer = async ({ children, params }) => {
|
|
597
745
|
const { locale } = await params;
|
|
598
746
|
|
|
599
747
|
return (
|
|
600
748
|
<html lang={locale} dir={getHTMLTextDir(locale)}>
|
|
601
|
-
<
|
|
602
|
-
{
|
|
603
|
-
|
|
749
|
+
<body>
|
|
750
|
+
<IntlayerClientProvider locale={locale}>
|
|
751
|
+
{children}
|
|
752
|
+
</IntlayerClientProvider>
|
|
753
|
+
</body>
|
|
604
754
|
</html>
|
|
605
755
|
);
|
|
606
756
|
};
|
|
@@ -652,10 +802,12 @@ Let's take an example of a client component rendering a counter.
|
|
|
652
802
|
<Tab defaultTab="next-intl" group='techno'>
|
|
653
803
|
<TabItem label="next-i18next" value="next-i18next">
|
|
654
804
|
|
|
655
|
-
**Translations (
|
|
805
|
+
**Translations (one JSON per namespace under `src/locales/...`)**
|
|
656
806
|
|
|
657
|
-
```json fileName="
|
|
807
|
+
```json fileName="src/locales/en/about.json"
|
|
658
808
|
{
|
|
809
|
+
"title": "About",
|
|
810
|
+
"description": "About page description",
|
|
659
811
|
"counter": {
|
|
660
812
|
"label": "Counter",
|
|
661
813
|
"increment": "Increment"
|
|
@@ -663,8 +815,10 @@ Let's take an example of a client component rendering a counter.
|
|
|
663
815
|
}
|
|
664
816
|
```
|
|
665
817
|
|
|
666
|
-
```json fileName="
|
|
818
|
+
```json fileName="src/locales/fr/about.json"
|
|
667
819
|
{
|
|
820
|
+
"title": "À propos",
|
|
821
|
+
"description": "Description de la page À propos",
|
|
668
822
|
"counter": {
|
|
669
823
|
"label": "Compteur",
|
|
670
824
|
"increment": "Incrémenter"
|
|
@@ -672,19 +826,18 @@ Let's take an example of a client component rendering a counter.
|
|
|
672
826
|
}
|
|
673
827
|
```
|
|
674
828
|
|
|
675
|
-
**Client component**
|
|
829
|
+
**Client component (loads only the required namespace)**
|
|
676
830
|
|
|
677
|
-
```tsx fileName="src/components/
|
|
831
|
+
```tsx fileName="src/components/ClientComponent.tsx"
|
|
678
832
|
"use client";
|
|
679
833
|
|
|
680
|
-
import React, {
|
|
681
|
-
import { useTranslation } from "
|
|
834
|
+
import React, { useState } from "react";
|
|
835
|
+
import { useTranslation } from "react-i18next";
|
|
682
836
|
|
|
683
|
-
const
|
|
837
|
+
const ClientComponent = () => {
|
|
684
838
|
const { t, i18n } = useTranslation("about");
|
|
685
839
|
const [count, setCount] = useState(0);
|
|
686
840
|
|
|
687
|
-
// next-i18next doesn't expose useNumber; use Intl.NumberFormat
|
|
688
841
|
const numberFormat = new Intl.NumberFormat(i18n.language);
|
|
689
842
|
|
|
690
843
|
return (
|
|
@@ -692,17 +845,19 @@ const ClientComponentExample = () => {
|
|
|
692
845
|
<p>{numberFormat.format(count)}</p>
|
|
693
846
|
<button
|
|
694
847
|
aria-label={t("counter.label")}
|
|
695
|
-
onClick={() => setCount((
|
|
848
|
+
onClick={() => setCount((c) => c + 1)}
|
|
696
849
|
>
|
|
697
850
|
{t("counter.increment")}
|
|
698
851
|
</button>
|
|
699
852
|
</div>
|
|
700
853
|
);
|
|
701
854
|
};
|
|
855
|
+
|
|
856
|
+
export default ClientComponent;
|
|
702
857
|
```
|
|
703
858
|
|
|
704
|
-
>
|
|
705
|
-
>
|
|
859
|
+
> Ensure the page/provider includes only the namespaces you need (e.g. `about`).
|
|
860
|
+
> If you use React < 19, memoize heavy formatters like `Intl.NumberFormat`.
|
|
706
861
|
|
|
707
862
|
</TabItem>
|
|
708
863
|
<TabItem label="next-intl" value="next-intl">
|
|
@@ -827,17 +982,15 @@ We will take the case of a UI component. This component is a server component, a
|
|
|
827
982
|
<Tab defaultTab="next-intl" group='techno'>
|
|
828
983
|
<TabItem label="next-i18next" value="next-i18next">
|
|
829
984
|
|
|
830
|
-
```tsx fileName="src/
|
|
831
|
-
import type { GetStaticProps } from "next";
|
|
832
|
-
import { useTranslation } from "next-i18next";
|
|
833
|
-
|
|
985
|
+
```tsx fileName="src/components/ServerComponent.tsx"
|
|
834
986
|
type ServerComponentProps = {
|
|
987
|
+
t: (key: string) => string;
|
|
988
|
+
locale: string;
|
|
835
989
|
count: number;
|
|
836
990
|
};
|
|
837
991
|
|
|
838
|
-
const ServerComponent = ({ count }: ServerComponentProps) => {
|
|
839
|
-
const
|
|
840
|
-
const formatted = new Intl.NumberFormat(i18n.language).format(count);
|
|
992
|
+
const ServerComponent = ({ t, locale, count }: ServerComponentProps) => {
|
|
993
|
+
const formatted = new Intl.NumberFormat(locale).format(count);
|
|
841
994
|
|
|
842
995
|
return (
|
|
843
996
|
<div>
|
|
@@ -846,6 +999,8 @@ const ServerComponent = ({ count }: ServerComponentProps) => {
|
|
|
846
999
|
</div>
|
|
847
1000
|
);
|
|
848
1001
|
};
|
|
1002
|
+
|
|
1003
|
+
export default ServerComponent;
|
|
849
1004
|
```
|
|
850
1005
|
|
|
851
1006
|
</TabItem>
|
|
@@ -853,20 +1008,25 @@ const ServerComponent = ({ count }: ServerComponentProps) => {
|
|
|
853
1008
|
|
|
854
1009
|
```tsx fileName="src/components/ServerComponent.tsx"
|
|
855
1010
|
type ServerComponentProps = {
|
|
856
|
-
|
|
857
|
-
|
|
1011
|
+
formattedCount: string;
|
|
1012
|
+
label: string;
|
|
1013
|
+
increment: string;
|
|
858
1014
|
};
|
|
859
1015
|
|
|
860
|
-
const ServerComponent = ({
|
|
861
|
-
|
|
862
|
-
|
|
1016
|
+
const ServerComponent = ({
|
|
1017
|
+
formattedCount,
|
|
1018
|
+
label,
|
|
1019
|
+
increment,
|
|
1020
|
+
}: ServerComponentProps) => {
|
|
863
1021
|
return (
|
|
864
1022
|
<div>
|
|
865
|
-
<p>{
|
|
866
|
-
<button aria-label={
|
|
1023
|
+
<p>{formattedCount}</p>
|
|
1024
|
+
<button aria-label={label}>{increment}</button>
|
|
867
1025
|
</div>
|
|
868
1026
|
);
|
|
869
1027
|
};
|
|
1028
|
+
|
|
1029
|
+
export default ServerComponent;
|
|
870
1030
|
```
|
|
871
1031
|
|
|
872
1032
|
> As the server component cannot be async, you need to pass the translations and formatter function as props.
|
|
@@ -875,7 +1035,7 @@ const ServerComponent = ({ t, count }: ServerComponentProps) => {
|
|
|
875
1035
|
>
|
|
876
1036
|
> - `import { getTranslations, getFormatter } from "next-intl/server";`
|
|
877
1037
|
> - `const t = await getTranslations("about.counter");`
|
|
878
|
-
> - `const
|
|
1038
|
+
> - `const formatter = await getFormatter().then((formatter) => formatter.number());`
|
|
879
1039
|
|
|
880
1040
|
</TabItem>
|
|
881
1041
|
<TabItem label="intlayer" value="intlayer">
|
|
@@ -952,10 +1112,9 @@ export async function generateMetadata({
|
|
|
952
1112
|
}): Promise<Metadata> {
|
|
953
1113
|
const { locale } = params;
|
|
954
1114
|
|
|
955
|
-
//
|
|
956
|
-
const messages = (
|
|
957
|
-
|
|
958
|
-
).default;
|
|
1115
|
+
// Import the correct JSON bundle from src/locales
|
|
1116
|
+
const messages = (await import("@/locales/" + locale + "/about.json"))
|
|
1117
|
+
.default;
|
|
959
1118
|
|
|
960
1119
|
const languages = Object.fromEntries(
|
|
961
1120
|
locales.map((locale) => [locale, localizedPath(locale, "/about")])
|
|
@@ -1181,7 +1340,111 @@ export default robots;
|
|
|
1181
1340
|
|
|
1182
1341
|
> Intlayer provides a `getMultilingualUrls` function to generate multilingual URLs for your sitemap.
|
|
1183
1342
|
|
|
1184
|
-
|
|
1343
|
+
### Middleware for locale routing
|
|
1344
|
+
|
|
1345
|
+
<Tab defaultTab="next-intl" group='techno'>
|
|
1346
|
+
<TabItem label="next-i18next" value="next-i18next">
|
|
1347
|
+
|
|
1348
|
+
Add a middleware to handle locale detection and routing:
|
|
1349
|
+
|
|
1350
|
+
```ts fileName="src/middleware.ts"
|
|
1351
|
+
import { NextResponse, type NextRequest } from "next/server";
|
|
1352
|
+
import { defaultLocale, locales } from "@/i18n.config";
|
|
1353
|
+
|
|
1354
|
+
const PUBLIC_FILE = /\.[^/]+$/; // exclude files with extensions
|
|
1355
|
+
|
|
1356
|
+
export function middleware(request: NextRequest) {
|
|
1357
|
+
const { pathname } = request.nextUrl;
|
|
1358
|
+
|
|
1359
|
+
if (
|
|
1360
|
+
pathname.startsWith("/_next") ||
|
|
1361
|
+
pathname.startsWith("/api") ||
|
|
1362
|
+
pathname.startsWith("/static") ||
|
|
1363
|
+
PUBLIC_FILE.test(pathname)
|
|
1364
|
+
) {
|
|
1365
|
+
return;
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
const hasLocale = locales.some(
|
|
1369
|
+
(l) => pathname === "/" + l || pathname.startsWith("/" + l + "/")
|
|
1370
|
+
);
|
|
1371
|
+
if (!hasLocale) {
|
|
1372
|
+
const locale = defaultLocale;
|
|
1373
|
+
const url = request.nextUrl.clone();
|
|
1374
|
+
url.pathname = "/" + locale + (pathname === "/" ? "" : pathname);
|
|
1375
|
+
return NextResponse.redirect(url);
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
export const config = {
|
|
1380
|
+
matcher: [
|
|
1381
|
+
// Match all paths except the ones starting with these and files with an extension
|
|
1382
|
+
"/((?!api|_next|static|.*\\..*).*)",
|
|
1383
|
+
],
|
|
1384
|
+
};
|
|
1385
|
+
```
|
|
1386
|
+
|
|
1387
|
+
</TabItem>
|
|
1388
|
+
<TabItem label="next-intl" value="next-intl">
|
|
1389
|
+
|
|
1390
|
+
Add a middleware to handle locale detection and routing:
|
|
1391
|
+
|
|
1392
|
+
```ts fileName="src/middleware.ts"
|
|
1393
|
+
import createMiddleware from "next-intl/middleware";
|
|
1394
|
+
import { locales, defaultLocale } from "@/i18n";
|
|
1395
|
+
|
|
1396
|
+
export default createMiddleware({
|
|
1397
|
+
locales: [...locales],
|
|
1398
|
+
defaultLocale,
|
|
1399
|
+
localeDetection: true,
|
|
1400
|
+
});
|
|
1401
|
+
|
|
1402
|
+
export const config = {
|
|
1403
|
+
// Skip API, Next internals and static assets
|
|
1404
|
+
matcher: ["/((?!api|_next|.*\\..*).*)"],
|
|
1405
|
+
};
|
|
1406
|
+
```
|
|
1407
|
+
|
|
1408
|
+
</TabItem>
|
|
1409
|
+
<TabItem label="intlayer" value="intlayer">
|
|
1410
|
+
|
|
1411
|
+
Intlayer provides built-in middleware handling through the `next-intlayer` package configuration.
|
|
1412
|
+
|
|
1413
|
+
</TabItem>
|
|
1414
|
+
</Tab>
|
|
1415
|
+
|
|
1416
|
+
### Setup checklist and good practices
|
|
1417
|
+
|
|
1418
|
+
<Tab defaultTab="next-intl" group='techno'>
|
|
1419
|
+
<TabItem label="next-i18next" value="next-i18next">
|
|
1420
|
+
|
|
1421
|
+
- Ensure `lang` and `dir` are set on the root `<html>` in `src/app/[locale]/layout.tsx`.
|
|
1422
|
+
- Split translations into namespaces (for example `common.json`, `about.json`) under `src/locales/<locale>/`.
|
|
1423
|
+
- Only load required namespaces in client components using `useTranslation('<ns>')` and by scoping `I18nProvider` with the same namespaces.
|
|
1424
|
+
- Keep pages static when possible: export `export const dynamic = 'force-static'` on pages; set `dynamicParams = false` and implement `generateStaticParams`.
|
|
1425
|
+
- Use sync server components nested under client boundaries by passing already-computed strings or the `t` function and the `locale`.
|
|
1426
|
+
- For SEO, set `alternates.languages` in metadata, list localized URLs in `sitemap.ts`, and disallow duplicate localized routes in `robots.ts`.
|
|
1427
|
+
- Prefer locale-aware formatters (e.g., `Intl.NumberFormat(locale)`) and memoize them on the client if using React < 19.
|
|
1428
|
+
|
|
1429
|
+
</TabItem>
|
|
1430
|
+
<TabItem label="next-intl" value="next-intl">
|
|
1431
|
+
|
|
1432
|
+
- **Set html `lang` and `dir`**: In `src/app/[locale]/layout.tsx`, compute `dir` via `getLocaleDirection(locale)` and set `<html lang={locale} dir={dir}>`.
|
|
1433
|
+
- **Split messages by namespace**: Organize JSON per locale and namespace (e.g., `common.json`, `about.json`).
|
|
1434
|
+
- **Minimize client payload**: On pages, send only required namespaces to `NextIntlClientProvider` (e.g., `pick(messages, ['common', 'about'])`).
|
|
1435
|
+
- **Prefer static pages**: Export `export const dynamic = 'force-static'` and generate static params for all `locales`.
|
|
1436
|
+
- **Synchronous server components**: Keep server components sync by passing precomputed strings (translated labels, formatted numbers) rather than async calls or non-serializable functions.
|
|
1437
|
+
|
|
1438
|
+
</TabItem>
|
|
1439
|
+
<TabItem label="intlayer" value="intlayer">
|
|
1440
|
+
|
|
1441
|
+
- **Modular content**: Co-locate content dictionaries with components using `.content.{ts|js|json}` files.
|
|
1442
|
+
- **Type safety**: Leverage TypeScript integration for compile-time content validation.
|
|
1443
|
+
- **Build-time optimization**: Use Intlayer's build tools for automatic tree-shaking and bundle optimization.
|
|
1444
|
+
- **Integrated tooling**: Take advantage of built-in routing, SEO helpers, and visual editor support.
|
|
1445
|
+
|
|
1446
|
+
</TabItem>
|
|
1447
|
+
</Tab>
|
|
1185
1448
|
|
|
1186
1449
|
---
|
|
1187
1450
|
|