@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.
Files changed (53) hide show
  1. package/blog/ar/next-i18next_vs_next-intl_vs_intlayer.md +404 -173
  2. package/blog/de/next-i18next_vs_next-intl_vs_intlayer.md +262 -113
  3. package/blog/en/intlayer_with_next-i18next.mdx +431 -0
  4. package/blog/en/intlayer_with_next-intl.mdx +335 -0
  5. package/blog/en/next-i18next_vs_next-intl_vs_intlayer.md +403 -140
  6. package/blog/en-GB/next-i18next_vs_next-intl_vs_intlayer.md +38 -28
  7. package/blog/es/next-i18next_vs_next-intl_vs_intlayer.md +185 -71
  8. package/blog/fr/next-i18next_vs_next-intl_vs_intlayer.md +38 -28
  9. package/blog/it/next-i18next_vs_next-intl_vs_intlayer.md +38 -28
  10. package/blog/ja/next-i18next_vs_next-intl_vs_intlayer.md +38 -28
  11. package/blog/ko/next-i18next_vs_next-intl_vs_intlayer.md +38 -28
  12. package/blog/pt/next-i18next_vs_next-intl_vs_intlayer.md +38 -28
  13. package/blog/ru/next-i18next_vs_next-intl_vs_intlayer.md +36 -28
  14. package/blog/tr/next-i18next_vs_next-intl_vs_intlayer.md +2 -0
  15. package/blog/zh/next-i18next_vs_next-intl_vs_intlayer.md +38 -28
  16. package/dist/cjs/generated/docs.entry.cjs +16 -0
  17. package/dist/cjs/generated/docs.entry.cjs.map +1 -1
  18. package/dist/esm/generated/docs.entry.mjs +16 -0
  19. package/dist/esm/generated/docs.entry.mjs.map +1 -1
  20. package/dist/types/generated/docs.entry.d.ts +1 -0
  21. package/dist/types/generated/docs.entry.d.ts.map +1 -1
  22. package/docs/ar/component_i18n.md +186 -0
  23. package/docs/ar/vs_code_extension.md +48 -109
  24. package/docs/de/component_i18n.md +186 -0
  25. package/docs/de/vs_code_extension.md +46 -107
  26. package/docs/en/component_i18n.md +186 -0
  27. package/docs/en/intlayer_with_nextjs_14.md +18 -1
  28. package/docs/en/intlayer_with_nextjs_15.md +18 -1
  29. package/docs/en/vs_code_extension.md +24 -114
  30. package/docs/en-GB/component_i18n.md +186 -0
  31. package/docs/en-GB/vs_code_extension.md +42 -103
  32. package/docs/es/component_i18n.md +182 -0
  33. package/docs/es/vs_code_extension.md +53 -114
  34. package/docs/fr/component_i18n.md +186 -0
  35. package/docs/fr/vs_code_extension.md +50 -111
  36. package/docs/hi/component_i18n.md +186 -0
  37. package/docs/hi/vs_code_extension.md +49 -110
  38. package/docs/it/component_i18n.md +186 -0
  39. package/docs/it/vs_code_extension.md +50 -111
  40. package/docs/ja/component_i18n.md +186 -0
  41. package/docs/ja/vs_code_extension.md +50 -111
  42. package/docs/ko/component_i18n.md +186 -0
  43. package/docs/ko/vs_code_extension.md +48 -109
  44. package/docs/pt/component_i18n.md +186 -0
  45. package/docs/pt/vs_code_extension.md +46 -107
  46. package/docs/ru/component_i18n.md +186 -0
  47. package/docs/ru/vs_code_extension.md +48 -109
  48. package/docs/tr/component_i18n.md +186 -0
  49. package/docs/tr/vs_code_extension.md +54 -115
  50. package/docs/zh/component_i18n.md +186 -0
  51. package/docs/zh/vs_code_extension.md +51 -105
  52. package/package.json +11 -11
  53. 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
+ ![next-i18next VS next-intl VS intlayer](https://github.com/aymericzip/intlayer/blob/main/docs/assets/i18next-next-intl-intlayer.png?raw=true)
23
+
22
24
  Let’s take a look into the similarities and differences between three i18n options for Next.js: next-i18next, next-intl, and Intlayer.
23
25
 
24
26
  This is not a full tutorial. It’s a comparison to help you pick.
@@ -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 | Bundle not optimized |
169
- | -------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
170
- | ![optimized bundle](https://github.com/aymericzip/intlayer/blob/main/docs/assets/bundle.png) | ![no optimized bundle](https://github.com/aymericzip/intlayer/blob/main/docs/assets/bundle_no_optimization.png) |
172
+ | Optimized bundle | Bundle not optimized |
173
+ | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
174
+ | ![optimized bundle](https://github.com/aymericzip/intlayer/blob/main/docs/assets/bundle.png?raw=true) | ![no optimized bundle](https://github.com/aymericzip/intlayer/blob/main/docs/assets/bundle_no_optimization.png?raw=true) |
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
- ├── public
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
- ├── middleware.ts
361
+ ├── locales
362
+ │ ├── en
363
+ │ │ ├── common.json
364
+ │ │ └── about.json
365
+ │ └── fr
366
+ │ ├── common.json
367
+ │ └── about.json
369
368
  ├── app
370
- └── home.tsx
369
+ ├── i18n
370
+ │ │ └── server.ts
371
+ │ └── [locale]
372
+ │ ├── layout.tsx
373
+ │ └── about.tsx
371
374
  └── components
372
- └── Navbar
373
- └── index.tsx
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
- └── home.tsx
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
- │ └── home
411
- └── index.tsx
412
- │ └── index.content.ts
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
- ```tsx fileName="next-i18next.config.js"
436
- module.exports = {
437
- i18n: {
438
- locales: ["en", "fr", "es"],
439
- defaultLocale: "en",
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
- ```tsx fileName="src/app/_app.tsx"
445
- import { appWithTranslation } from "next-i18next";
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
- const MyApp = ({ Component, pageProps }) => <Component {...pageProps} />;
454
+ export function localizedPath(locale: string, path: string) {
455
+ return locale === defaultLocale ? path : "/" + locale + path;
456
+ }
448
457
 
449
- export default appWithTranslation(MyApp);
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
- ```tsx fileName="src/app/[locale]/about/page.tsx"
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 { ClientComponent, ServerComponent } from "@components";
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
- export default function HomePage({ locale }: { locale: string }) {
461
- // Déclarez explicitement le namespace utilisé par ce composant
462
- const resources = await loadMessagesFor(locale); // your loader (JSON, etc.)
496
+ ```tsx fileName="src/components/I18nProvider.tsx"
497
+ "use client";
463
498
 
464
- const i18n = createInstance();
465
- i18n.use(initReactI18next).init({
466
- lng: locale,
467
- fallbackLng: "en",
468
- resources,
469
- ns: ["common", "about"],
470
- defaultNS: "common",
471
- interpolation: { escapeValue: false },
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
- const { t } = useTranslation("about");
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
- <I18nextProvider i18n={i18n}>
598
+ <I18nProvider locale={locale} namespaces={[...namespaces]}>
478
599
  <main>
479
- <h1>{t("title")}</h1>
600
+ <h1>{tAbout("title")}</h1>
601
+
480
602
  <ClientComponent />
481
- <ServerComponent />
603
+ <ServerComponent t={tAbout} locale={locale} count={0} />
482
604
  </main>
483
- </I18nextProvider>
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
- // Can be imported from a shared config
505
- const locales = ["en", "fr", "es"];
617
+ export const locales = ["en", "fr", "es"] as const;
618
+ export const defaultLocale = "en" as const;
619
+
620
+ async function loadMessages(locale: string) {
621
+ // Load only the namespaces your layout/pages need
622
+ const [common, about] = await Promise.all([
623
+ import(`../locales/${locale}/common.json`).then((m) => m.default),
624
+ import(`../locales/${locale}/about.json`).then((m) => m.default),
625
+ ]);
626
+
627
+ return { common, about } as const;
628
+ }
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: (await import(`../messages/${locale}.json`)).default,
634
+ messages: await loadMessages(locale),
513
635
  };
514
636
  });
515
637
  ```
516
638
 
517
- ```tsx fileName="src/app/[locale]/about/layout.tsx"
518
- import { NextIntlClientProvider } from "next-intl";
519
- import { getMessages, unstable_setRequestLocale } from "next-intl/server";
520
- import pick from "lodash/pick";
639
+ ```tsx fileName="src/app/[locale]/layout.tsx"
640
+ import type { ReactNode } from "react";
641
+ import { locales } from "@/i18n";
642
+ import {
643
+ getLocaleDirection,
644
+ unstable_setRequestLocale,
645
+ } from "next-intl/server";
646
+
647
+ export const dynamic = "force-static";
648
+
649
+ export function generateStaticParams() {
650
+ return locales.map((locale) => ({ locale }));
651
+ }
521
652
 
522
653
  export default async function LocaleLayout({
523
654
  children,
524
655
  params,
525
656
  }: {
526
- children: React.ReactNode;
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
- // Messages are loaded server-side via src/i18n/request.ts
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 { ClientComponent, ServerComponent } from "@components";
676
+ import { getTranslations, getMessages, getFormatter } from "next-intl/server";
677
+ import { NextIntlClientProvider } from "next-intl";
678
+ import pick from "lodash/pick";
679
+ import ServerComponent from "@/components/ServerComponent";
680
+ import ClientComponentExample from "@/components/ClientComponentExample";
681
+
682
+ export const dynamic = "force-static";
555
683
 
556
- export default async function LandingPage({
684
+ export default async function AboutPage({
557
685
  params,
558
686
  }: {
559
- params: { locale: string };
687
+ params: Promise<{ locale: string }>;
560
688
  }) {
561
- // Chargement strictement côté serveur (pas hydraté au client)
562
- const t = await getTranslations("about");
689
+ const { locale } = await params;
690
+
691
+ // Messages are loaded server-side. Push only what's needed to the client.
692
+ const messages = await getMessages();
693
+ const clientMessages = pick(messages, ["common", "about"]);
694
+
695
+ // Strictly server-side translations/formatting
696
+ const tAbout = await getTranslations("about");
697
+ const tCounter = await getTranslations("about.counter");
698
+ const format = await getFormatter();
699
+
700
+ const initialFormattedCount = format.number(0);
563
701
 
564
702
  return (
565
- <main>
566
- <h1>{t("title")}</h1>
567
- <ClientComponent />
568
- <ServerComponent />
569
- </main>
703
+ <NextIntlClientProvider locale={locale} messages={clientMessages}>
704
+ <main>
705
+ <h1>{tAbout("title")}</h1>
706
+ <ClientComponentExample />
707
+ <ServerComponent
708
+ formattedCount={initialFormattedCount}
709
+ label={tCounter("label")}
710
+ increment={tCounter("increment")}
711
+ />
712
+ </main>
713
+ </NextIntlClientProvider>
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
- export default {
722
+ import { type IntlayerConfig, Locales } from "intlayer";
723
+
724
+ const config: IntlayerConfig = {
579
725
  internationalization: {
580
- locales: ["en", "fr", "es"],
581
- defaultLocale: "en",
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 LandingLayout: NextLayoutIntlayer = async ({ children, params }) => {
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
- <IntlayerClientProvider locale={locale}>
602
- {children}
603
- </IntlayerClientProvider>
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 (must be real JSON in `public/locales/...`)**
805
+ **Translations (one JSON per namespace under `src/locales/...`)**
656
806
 
657
- ```json fileName="public/locales/en/about.json"
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="public/locales/fr/about.json"
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/ClientComponentExample.tsx"
831
+ ```tsx fileName="src/components/ClientComponent.tsx"
678
832
  "use client";
679
833
 
680
- import React, { useMemo, useState } from "react";
681
- import { useTranslation } from "next-i18next";
834
+ import React, { useState } from "react";
835
+ import { useTranslation } from "react-i18next";
682
836
 
683
- const ClientComponentExample = () => {
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((count) => count + 1)}
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
- > Don't forget to add "about" namespace on the page serverSideTranslations
705
- > We take here the version of react 19.x.x, but for lower versions, you will need to use useMemo to store the instance of the formatter as it's a heavy function
859
+ > Ensure the page/provider includes only the namespaces you need (e.g. `about`).
860
+ > If you use React < 19, memoize heavy formatters like `Intl.NumberFormat`.
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/pages/about.tsx"
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 { t, i18n } = useTranslation("about");
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
- count: number;
857
- t: (key: string) => string;
1011
+ formattedCount: string;
1012
+ label: string;
1013
+ increment: string;
858
1014
  };
859
1015
 
860
- const ServerComponent = ({ t, count }: ServerComponentProps) => {
861
- const formatted = new Intl.NumberFormat(i18n.language).format(count);
862
-
1016
+ const ServerComponent = ({
1017
+ formattedCount,
1018
+ label,
1019
+ increment,
1020
+ }: ServerComponentProps) => {
863
1021
  return (
864
1022
  <div>
865
- <p>{formatted}</p>
866
- <button aria-label={t("label")}>{t("increment")}</button>
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 format = await getFormatter();`
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
- // Dynamically import the correct JSON file
956
- const messages = (
957
- await import("@/../public/locales/" + locale + "/about.json")
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