@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
|
@@ -165,9 +165,9 @@ slugs:
|
|
|
165
165
|
|
|
166
166
|
فيما يلي مثال على تأثير تحسين حجم الحزمة باستخدام `intlayer` في تطبيق vite + react:
|
|
167
167
|
|
|
168
|
-
| الحزمة المحسنة
|
|
169
|
-
|
|
|
170
|
-
|  |  |
|
|
168
|
+
| الحزمة المحسنة | الحزمة غير المحسنة |
|
|
169
|
+
| ----------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
|
|
170
|
+
|  |  |
|
|
171
171
|
|
|
172
172
|
---
|
|
173
173
|
|
|
@@ -340,25 +340,25 @@ slugs:
|
|
|
340
340
|
|
|
341
341
|
```bash
|
|
342
342
|
.
|
|
343
|
-
├──
|
|
344
|
-
│ └── locales
|
|
345
|
-
│ ├── en
|
|
346
|
-
│ │ ├── home.json
|
|
347
|
-
│ │ └── navbar.json
|
|
348
|
-
│ ├── fr
|
|
349
|
-
│ │ ├── home.json
|
|
350
|
-
│ │ └── navbar.json
|
|
351
|
-
│ └── es
|
|
352
|
-
│ ├── home.json
|
|
353
|
-
│ └── navbar.json
|
|
354
|
-
├── next-i18next.config.js
|
|
343
|
+
├── i18n.config.ts
|
|
355
344
|
└── src
|
|
356
|
-
├──
|
|
345
|
+
├── locales
|
|
346
|
+
│ ├── en
|
|
347
|
+
│ │ ├── common.json
|
|
348
|
+
│ │ └── about.json
|
|
349
|
+
│ └── fr
|
|
350
|
+
│ ├── common.json
|
|
351
|
+
│ └── about.json
|
|
357
352
|
├── app
|
|
358
|
-
│
|
|
353
|
+
│ ├── i18n
|
|
354
|
+
│ │ └── server.ts
|
|
355
|
+
│ └── [locale]
|
|
356
|
+
│ ├── layout.tsx
|
|
357
|
+
│ └── about.tsx
|
|
359
358
|
└── components
|
|
360
|
-
|
|
361
|
-
|
|
359
|
+
├── I18nProvider.tsx
|
|
360
|
+
├── ClientComponent.tsx
|
|
361
|
+
└── ServerComponent.tsx
|
|
362
362
|
```
|
|
363
363
|
|
|
364
364
|
</TabItem>
|
|
@@ -366,6 +366,7 @@ slugs:
|
|
|
366
366
|
|
|
367
367
|
```bash
|
|
368
368
|
.
|
|
369
|
+
├── i18n.ts
|
|
369
370
|
├── locales
|
|
370
371
|
│ ├── en
|
|
371
372
|
│ │ ├── home.json
|
|
@@ -376,18 +377,20 @@ slugs:
|
|
|
376
377
|
│ └── es
|
|
377
378
|
│ ├── home.json
|
|
378
379
|
│ └── navbar.json
|
|
379
|
-
├── i18n.ts
|
|
380
380
|
└── src
|
|
381
381
|
├── middleware.ts
|
|
382
382
|
├── app
|
|
383
|
-
│
|
|
383
|
+
│ ├── i18n
|
|
384
|
+
│ │ └── server.ts
|
|
385
|
+
│ └── [locale]
|
|
386
|
+
│ └── home.tsx
|
|
384
387
|
└── components
|
|
385
388
|
└── Navbar
|
|
386
389
|
└── index.tsx
|
|
387
390
|
```
|
|
388
391
|
|
|
389
392
|
</TabItem>
|
|
390
|
-
<TabItem label="intlayer" value="intlayer">
|
|
393
|
+
<TabItem label="intlayer" value="intlayer">
|
|
391
394
|
|
|
392
395
|
```bash
|
|
393
396
|
.
|
|
@@ -395,9 +398,11 @@ slugs:
|
|
|
395
398
|
└── src
|
|
396
399
|
├── middleware.ts
|
|
397
400
|
├── app
|
|
398
|
-
│ └──
|
|
399
|
-
│
|
|
400
|
-
│ └──
|
|
401
|
+
│ └── [locale]
|
|
402
|
+
│ ├── layout.tsx
|
|
403
|
+
│ └── home
|
|
404
|
+
│ ├── index.tsx
|
|
405
|
+
│ └── index.content.ts
|
|
401
406
|
└── components
|
|
402
407
|
└── Navbar
|
|
403
408
|
├── index.tsx
|
|
@@ -420,155 +425,294 @@ slugs:
|
|
|
420
425
|
<Tab defaultTab="next-intl" group='techno'>
|
|
421
426
|
<TabItem label="next-i18next" value="next-i18next">
|
|
422
427
|
|
|
423
|
-
```
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
},
|
|
429
|
-
};
|
|
430
|
-
```
|
|
428
|
+
```ts fileName="i18n.config.ts"
|
|
429
|
+
export const locales = ["en", "fr"] as const;
|
|
430
|
+
export type Locale = (typeof locales)[number];
|
|
431
|
+
|
|
432
|
+
export const defaultLocale: Locale = "en";
|
|
431
433
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
+
export const rtlLocales = ["ar", "he", "fa", "ur"] as const;
|
|
435
|
+
export const isRtl = (locale: string) =>
|
|
436
|
+
(rtlLocales as readonly string[]).includes(locale);
|
|
434
437
|
|
|
435
|
-
|
|
438
|
+
export function localizedPath(locale: string, path: string) {
|
|
439
|
+
return locale === defaultLocale ? path : "/" + locale + path;
|
|
440
|
+
}
|
|
436
441
|
|
|
437
|
-
|
|
442
|
+
const ORIGIN = "https://example.com";
|
|
443
|
+
export function abs(locale: string, path: string) {
|
|
444
|
+
return ORIGIN + localizedPath(locale, path);
|
|
445
|
+
}
|
|
438
446
|
```
|
|
439
447
|
|
|
440
|
-
```
|
|
441
|
-
import type { GetStaticProps } from "next";
|
|
442
|
-
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
|
|
443
|
-
import { useTranslation } from "next-i18next";
|
|
444
|
-
import { I18nextProvider, initReactI18next } from "react-i18next";
|
|
448
|
+
```ts fileName="src/app/i18n/server.ts"
|
|
445
449
|
import { createInstance } from "i18next";
|
|
446
|
-
import {
|
|
450
|
+
import { initReactI18next } from "react-i18next/initReactI18next";
|
|
451
|
+
import resourcesToBackend from "i18next-resources-to-backend";
|
|
452
|
+
import { defaultLocale } from "@/i18n.config";
|
|
453
|
+
|
|
454
|
+
// Load JSON resources from src/locales/<locale>/<namespace>.json
|
|
455
|
+
const backend = resourcesToBackend(
|
|
456
|
+
(locale: string, namespace: string) =>
|
|
457
|
+
import(`../../locales/${locale}/${namespace}.json`)
|
|
458
|
+
);
|
|
459
|
+
|
|
460
|
+
export async function initI18next(
|
|
461
|
+
locale: string,
|
|
462
|
+
namespaces: string[] = ["common"]
|
|
463
|
+
) {
|
|
464
|
+
const i18n = createInstance();
|
|
465
|
+
await i18n
|
|
466
|
+
.use(initReactI18next)
|
|
467
|
+
.use(backend)
|
|
468
|
+
.init({
|
|
469
|
+
lng: locale,
|
|
470
|
+
fallbackLng: defaultLocale,
|
|
471
|
+
ns: namespaces,
|
|
472
|
+
defaultNS: "common",
|
|
473
|
+
interpolation: { escapeValue: false },
|
|
474
|
+
react: { useSuspense: false },
|
|
475
|
+
});
|
|
476
|
+
return i18n;
|
|
477
|
+
}
|
|
478
|
+
```
|
|
447
479
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
const resources = await loadMessagesFor(locale); // محملك (JSON، إلخ)
|
|
480
|
+
```tsx fileName="src/components/I18nProvider.tsx"
|
|
481
|
+
"use client";
|
|
451
482
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
483
|
+
import * as React from "react";
|
|
484
|
+
import { I18nextProvider } from "react-i18next";
|
|
485
|
+
import { createInstance } from "i18next";
|
|
486
|
+
import { initReactI18next } from "react-i18next/initReactI18next";
|
|
487
|
+
import resourcesToBackend from "i18next-resources-to-backend";
|
|
488
|
+
import { defaultLocale } from "@/i18n.config";
|
|
489
|
+
|
|
490
|
+
const backend = resourcesToBackend(
|
|
491
|
+
(locale: string, namespace: string) =>
|
|
492
|
+
import(`../../locales/${locale}/${namespace}.json`)
|
|
493
|
+
);
|
|
494
|
+
|
|
495
|
+
type Props = {
|
|
496
|
+
locale: string;
|
|
497
|
+
namespaces?: string[];
|
|
498
|
+
resources?: Record<string, any>; // { ns: bundle }
|
|
499
|
+
children: React.ReactNode;
|
|
500
|
+
};
|
|
501
|
+
|
|
502
|
+
export default function I18nProvider({
|
|
503
|
+
locale,
|
|
504
|
+
namespaces = ["common"],
|
|
505
|
+
resources,
|
|
506
|
+
children,
|
|
507
|
+
}: Props) {
|
|
508
|
+
const [i18n] = React.useState(() => {
|
|
509
|
+
const i = createInstance();
|
|
510
|
+
|
|
511
|
+
i.use(initReactI18next)
|
|
512
|
+
.use(backend)
|
|
513
|
+
.init({
|
|
514
|
+
lng: locale,
|
|
515
|
+
fallbackLng: defaultLocale,
|
|
516
|
+
ns: namespaces,
|
|
517
|
+
resources: resources ? { [locale]: resources } : undefined,
|
|
518
|
+
defaultNS: "common",
|
|
519
|
+
interpolation: { escapeValue: false },
|
|
520
|
+
react: { useSuspense: false },
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
return i;
|
|
460
524
|
});
|
|
461
525
|
|
|
462
|
-
|
|
526
|
+
return <I18nextProvider i18n={i18n}>{children}</I18nextProvider>;
|
|
527
|
+
}
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
```tsx fileName="src/app/[locale]/layout.tsx"
|
|
531
|
+
import type { ReactNode } from "react";
|
|
532
|
+
import { locales, defaultLocale, isRtl, type Locale } from "@/i18n.config";
|
|
533
|
+
|
|
534
|
+
export const dynamicParams = false;
|
|
535
|
+
|
|
536
|
+
export function generateStaticParams() {
|
|
537
|
+
return locales.map((locale) => ({ locale }));
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
export default function LocaleLayout({
|
|
541
|
+
children,
|
|
542
|
+
params,
|
|
543
|
+
}: {
|
|
544
|
+
children: ReactNode;
|
|
545
|
+
params: { locale: string };
|
|
546
|
+
}) {
|
|
547
|
+
const locale: Locale = (locales as readonly string[]).includes(params.locale)
|
|
548
|
+
? (params.locale as any)
|
|
549
|
+
: defaultLocale;
|
|
550
|
+
|
|
551
|
+
const dir = isRtl(locale) ? "rtl" : "ltr";
|
|
552
|
+
|
|
553
|
+
return (
|
|
554
|
+
<html lang={locale} dir={dir}>
|
|
555
|
+
<body>{children}</body>
|
|
556
|
+
</html>
|
|
557
|
+
);
|
|
558
|
+
}
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
```tsx fileName="src/app/[locale]/about.tsx"
|
|
562
|
+
import I18nProvider from "@/components/I18nProvider";
|
|
563
|
+
import { initI18next } from "@/app/i18n/server";
|
|
564
|
+
import type { Locale } from "@/i18n.config";
|
|
565
|
+
import ClientComponent from "@/components/ClientComponent";
|
|
566
|
+
import ServerComponent from "@/components/ServerComponent";
|
|
567
|
+
|
|
568
|
+
// Force static rendering for the page
|
|
569
|
+
export const dynamic = "force-static";
|
|
570
|
+
|
|
571
|
+
export default async function AboutPage({
|
|
572
|
+
params: { locale },
|
|
573
|
+
}: {
|
|
574
|
+
params: { locale: Locale };
|
|
575
|
+
}) {
|
|
576
|
+
const namespaces = ["common", "about"] as const;
|
|
577
|
+
|
|
578
|
+
const i18n = await initI18next(locale, [...namespaces]);
|
|
579
|
+
const tAbout = i18n.getFixedT(locale, "about");
|
|
463
580
|
|
|
464
581
|
return (
|
|
465
|
-
<
|
|
582
|
+
<I18nProvider locale={locale} namespaces={[...namespaces]}>
|
|
466
583
|
<main>
|
|
467
|
-
<h1>{
|
|
584
|
+
<h1>{tAbout("title")}</h1>
|
|
585
|
+
|
|
468
586
|
<ClientComponent />
|
|
469
|
-
<ServerComponent />
|
|
587
|
+
<ServerComponent t={tAbout} locale={locale} count={0} />
|
|
470
588
|
</main>
|
|
471
|
-
</
|
|
589
|
+
</I18nProvider>
|
|
472
590
|
);
|
|
473
591
|
}
|
|
474
|
-
|
|
475
|
-
export const getStaticProps: GetStaticProps = async ({ locale }) => {
|
|
476
|
-
// قم بتحميل مساحات الأسماء الضرورية فقط لهذه الصفحة
|
|
477
|
-
return {
|
|
478
|
-
props: {
|
|
479
|
-
...(await serverSideTranslations(locale ?? "en", ["common", "about"])),
|
|
480
|
-
},
|
|
481
|
-
};
|
|
482
|
-
};
|
|
483
592
|
```
|
|
484
593
|
|
|
485
594
|
</TabItem>
|
|
486
595
|
<TabItem label="next-intl" value="next-intl">
|
|
487
596
|
|
|
488
|
-
```tsx fileName="i18n.ts"
|
|
597
|
+
```tsx fileName="src/i18n.ts"
|
|
489
598
|
import { getRequestConfig } from "next-intl/server";
|
|
490
599
|
import { notFound } from "next/navigation";
|
|
491
600
|
|
|
492
|
-
|
|
493
|
-
const
|
|
601
|
+
export const locales = ["en", "fr", "es"] as const;
|
|
602
|
+
export const defaultLocale = "en" as const;
|
|
603
|
+
|
|
604
|
+
async function loadMessages(locale: string) {
|
|
605
|
+
// Load only the namespaces your layout/pages need
|
|
606
|
+
const [common, about] = await Promise.all([
|
|
607
|
+
import(`../locales/${locale}/common.json`).then((m) => m.default),
|
|
608
|
+
import(`../locales/${locale}/about.json`).then((m) => m.default),
|
|
609
|
+
]);
|
|
610
|
+
|
|
611
|
+
return { common, about } as const;
|
|
612
|
+
}
|
|
494
613
|
|
|
495
614
|
export default getRequestConfig(async ({ locale }) => {
|
|
496
|
-
// تحقق من أن معامل `locale` الوارد صالح
|
|
497
615
|
if (!locales.includes(locale as any)) notFound();
|
|
498
616
|
|
|
499
617
|
return {
|
|
500
|
-
messages:
|
|
618
|
+
messages: await loadMessages(locale),
|
|
501
619
|
};
|
|
502
620
|
});
|
|
503
621
|
```
|
|
504
622
|
|
|
505
|
-
```tsx fileName="src/app/[locale]/
|
|
506
|
-
import {
|
|
507
|
-
import {
|
|
508
|
-
import
|
|
623
|
+
```tsx fileName="src/app/[locale]/layout.tsx"
|
|
624
|
+
import type { ReactNode } from "react";
|
|
625
|
+
import { locales } from "@/i18n";
|
|
626
|
+
import {
|
|
627
|
+
getLocaleDirection,
|
|
628
|
+
unstable_setRequestLocale,
|
|
629
|
+
} from "next-intl/server";
|
|
630
|
+
|
|
631
|
+
export const dynamic = "force-static";
|
|
632
|
+
|
|
633
|
+
export function generateStaticParams() {
|
|
634
|
+
return locales.map((locale) => ({ locale }));
|
|
635
|
+
}
|
|
509
636
|
|
|
510
637
|
export default async function LocaleLayout({
|
|
511
638
|
children,
|
|
512
639
|
params,
|
|
513
640
|
}: {
|
|
514
|
-
children:
|
|
515
|
-
params: { locale: string }
|
|
641
|
+
children: ReactNode;
|
|
642
|
+
params: Promise<{ locale: string }>;
|
|
516
643
|
}) {
|
|
517
|
-
const { locale } = params;
|
|
644
|
+
const { locale } = await params;
|
|
518
645
|
|
|
519
|
-
//
|
|
646
|
+
// Set the active request locale for this server render (RSC)
|
|
520
647
|
unstable_setRequestLocale(locale);
|
|
521
648
|
|
|
522
|
-
|
|
523
|
-
// (انظر وثائق next-intl). هنا ندفع فقط جزءًا فرعيًا إلى العميل
|
|
524
|
-
// اللازم لمكونات العميل (تحسين الحمولة).
|
|
525
|
-
const messages = await getMessages();
|
|
526
|
-
const clientMessages = pick(messages, ["common", "about"]);
|
|
649
|
+
const dir = getLocaleDirection(locale);
|
|
527
650
|
|
|
528
651
|
return (
|
|
529
|
-
<html lang={locale}>
|
|
530
|
-
<body>
|
|
531
|
-
<NextIntlClientProvider locale={locale} messages={clientMessages}>
|
|
532
|
-
{children}
|
|
533
|
-
</NextIntlClientProvider>
|
|
534
|
-
</body>
|
|
652
|
+
<html lang={locale} dir={dir}>
|
|
653
|
+
<body>{children}</body>
|
|
535
654
|
</html>
|
|
536
655
|
);
|
|
537
656
|
}
|
|
538
657
|
```
|
|
539
658
|
|
|
540
659
|
```tsx fileName="src/app/[locale]/about/page.tsx"
|
|
541
|
-
import { getTranslations } from "next-intl/server";
|
|
542
|
-
import {
|
|
660
|
+
import { getTranslations, getMessages, getFormatter } from "next-intl/server";
|
|
661
|
+
import { NextIntlClientProvider } from "next-intl";
|
|
662
|
+
import pick from "lodash/pick";
|
|
663
|
+
import ServerComponent from "@/components/ServerComponent";
|
|
664
|
+
import ClientComponentExample from "@/components/ClientComponentExample";
|
|
543
665
|
|
|
544
|
-
export
|
|
666
|
+
export const dynamic = "force-static";
|
|
667
|
+
|
|
668
|
+
export default async function AboutPage({
|
|
545
669
|
params,
|
|
546
670
|
}: {
|
|
547
|
-
params: { locale: string }
|
|
671
|
+
params: Promise<{ locale: string }>;
|
|
548
672
|
}) {
|
|
549
|
-
|
|
550
|
-
|
|
673
|
+
const { locale } = await params;
|
|
674
|
+
|
|
675
|
+
// Messages are loaded server-side. Push only what's needed to the client.
|
|
676
|
+
const messages = await getMessages();
|
|
677
|
+
const clientMessages = pick(messages, ["common", "about"]);
|
|
678
|
+
|
|
679
|
+
// Strictly server-side translations/formatting
|
|
680
|
+
const tAbout = await getTranslations("about");
|
|
681
|
+
const tCounter = await getTranslations("about.counter");
|
|
682
|
+
const format = await getFormatter();
|
|
683
|
+
|
|
684
|
+
const initialFormattedCount = format.number(0);
|
|
551
685
|
|
|
552
686
|
return (
|
|
553
|
-
<
|
|
554
|
-
<
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
687
|
+
<NextIntlClientProvider locale={locale} messages={clientMessages}>
|
|
688
|
+
<main>
|
|
689
|
+
<h1>{tAbout("title")}</h1>
|
|
690
|
+
<ClientComponentExample />
|
|
691
|
+
<ServerComponent
|
|
692
|
+
formattedCount={initialFormattedCount}
|
|
693
|
+
label={tCounter("label")}
|
|
694
|
+
increment={tCounter("increment")}
|
|
695
|
+
/>
|
|
696
|
+
</main>
|
|
697
|
+
</NextIntlClientProvider>
|
|
558
698
|
);
|
|
559
699
|
}
|
|
560
700
|
```
|
|
561
701
|
|
|
562
702
|
</TabItem>
|
|
563
|
-
<TabItem label="intlayer" value="intlayer">
|
|
703
|
+
<TabItem label="intlayer" value="intlayer">
|
|
564
704
|
|
|
565
705
|
```tsx fileName="intlayer.config.ts"
|
|
566
|
-
|
|
706
|
+
import { type IntlayerConfig, Locales } from "intlayer";
|
|
707
|
+
|
|
708
|
+
const config: IntlayerConfig = {
|
|
567
709
|
internationalization: {
|
|
568
|
-
locales: [
|
|
569
|
-
defaultLocale:
|
|
710
|
+
locales: [Locales.ENGLISH, Locales.FRENCH, Locales.SPANISH],
|
|
711
|
+
defaultLocale: Locales.ENGLISH,
|
|
570
712
|
},
|
|
571
713
|
};
|
|
714
|
+
|
|
715
|
+
export default config;
|
|
572
716
|
```
|
|
573
717
|
|
|
574
718
|
```tsx fileName="src/app/[locale]/layout.tsx"
|
|
@@ -581,14 +725,16 @@ import {
|
|
|
581
725
|
|
|
582
726
|
export const dynamic = "force-static";
|
|
583
727
|
|
|
584
|
-
const
|
|
728
|
+
const LocaleLayout: NextLayoutIntlayer = async ({ children, params }) => {
|
|
585
729
|
const { locale } = await params;
|
|
586
730
|
|
|
587
731
|
return (
|
|
588
732
|
<html lang={locale} dir={getHTMLTextDir(locale)}>
|
|
589
|
-
<
|
|
590
|
-
{
|
|
591
|
-
|
|
733
|
+
<body>
|
|
734
|
+
<IntlayerClientProvider locale={locale}>
|
|
735
|
+
{children}
|
|
736
|
+
</IntlayerClientProvider>
|
|
737
|
+
</body>
|
|
592
738
|
</html>
|
|
593
739
|
);
|
|
594
740
|
};
|
|
@@ -640,10 +786,12 @@ export default LandingPage;
|
|
|
640
786
|
<Tab defaultTab="next-intl" group='techno'>
|
|
641
787
|
<TabItem label="next-i18next" value="next-i18next">
|
|
642
788
|
|
|
643
|
-
**الترجمات (
|
|
789
|
+
**الترجمات (one JSON per namespace under `src/locales/...`)**
|
|
644
790
|
|
|
645
|
-
```json fileName="
|
|
791
|
+
```json fileName="src/locales/en/about.json"
|
|
646
792
|
{
|
|
793
|
+
"title": "About",
|
|
794
|
+
"description": "About page description",
|
|
647
795
|
"counter": {
|
|
648
796
|
"label": "Counter",
|
|
649
797
|
"increment": "Increment"
|
|
@@ -651,28 +799,29 @@ export default LandingPage;
|
|
|
651
799
|
}
|
|
652
800
|
```
|
|
653
801
|
|
|
654
|
-
```json fileName="
|
|
802
|
+
```json fileName="src/locales/fr/about.json"
|
|
655
803
|
{
|
|
804
|
+
"title": "À propos",
|
|
805
|
+
"description": "Description de la page À propos",
|
|
656
806
|
"counter": {
|
|
657
|
-
"label": "
|
|
658
|
-
"increment": "
|
|
807
|
+
"label": "Compteur",
|
|
808
|
+
"increment": "Incrémenter"
|
|
659
809
|
}
|
|
660
810
|
}
|
|
661
811
|
```
|
|
662
812
|
|
|
663
|
-
**مكون
|
|
813
|
+
**مكون العميل (loads only the required namespace)**
|
|
664
814
|
|
|
665
|
-
```tsx fileName="src/components/
|
|
815
|
+
```tsx fileName="src/components/ClientComponent.tsx"
|
|
666
816
|
"use client";
|
|
667
817
|
|
|
668
|
-
import React, {
|
|
669
|
-
import { useTranslation } from "
|
|
818
|
+
import React, { useState } from "react";
|
|
819
|
+
import { useTranslation } from "react-i18next";
|
|
670
820
|
|
|
671
|
-
const
|
|
821
|
+
const ClientComponent = () => {
|
|
672
822
|
const { t, i18n } = useTranslation("about");
|
|
673
823
|
const [count, setCount] = useState(0);
|
|
674
824
|
|
|
675
|
-
// next-i18next لا يوفر useNumber؛ استخدم Intl.NumberFormat
|
|
676
825
|
const numberFormat = new Intl.NumberFormat(i18n.language);
|
|
677
826
|
|
|
678
827
|
return (
|
|
@@ -680,22 +829,24 @@ const ClientComponentExample = () => {
|
|
|
680
829
|
<p>{numberFormat.format(count)}</p>
|
|
681
830
|
<button
|
|
682
831
|
aria-label={t("counter.label")}
|
|
683
|
-
onClick={() => setCount((
|
|
832
|
+
onClick={() => setCount((c) => c + 1)}
|
|
684
833
|
>
|
|
685
834
|
{t("counter.increment")}
|
|
686
835
|
</button>
|
|
687
836
|
</div>
|
|
688
837
|
);
|
|
689
838
|
};
|
|
839
|
+
|
|
840
|
+
export default ClientComponent;
|
|
690
841
|
```
|
|
691
842
|
|
|
692
|
-
>
|
|
693
|
-
>
|
|
843
|
+
> Ensure the page/provider includes only the namespaces you need (e.g. `about`).
|
|
844
|
+
> If you use React < 19, memoize heavy formatters like `Intl.NumberFormat`.
|
|
694
845
|
|
|
695
846
|
</TabItem>
|
|
696
847
|
<TabItem label="next-intl" value="next-intl">
|
|
697
848
|
|
|
698
|
-
**الترجمات (
|
|
849
|
+
**الترجمات (shape reused; load them into next-intl messages as you prefer)**
|
|
699
850
|
|
|
700
851
|
```json fileName="locales/en/about.json"
|
|
701
852
|
{
|
|
@@ -743,7 +894,7 @@ const ClientComponentExample = () => {
|
|
|
743
894
|
};
|
|
744
895
|
```
|
|
745
896
|
|
|
746
|
-
>
|
|
897
|
+
> Don't forget to add "about" message on the page client message
|
|
747
898
|
|
|
748
899
|
</TabItem>
|
|
749
900
|
<TabItem label="intlayer" value="intlayer">
|
|
@@ -756,8 +907,8 @@ import { t, type Dictionary } from "intlayer";
|
|
|
756
907
|
const counterContent = {
|
|
757
908
|
key: "counter",
|
|
758
909
|
content: {
|
|
759
|
-
label: t({
|
|
760
|
-
increment: t({
|
|
910
|
+
label: t({ en: "Counter", fr: "Compteur" }),
|
|
911
|
+
increment: t({ en: "Increment", fr: "Incrémenter" }),
|
|
761
912
|
},
|
|
762
913
|
} satisfies Dictionary;
|
|
763
914
|
|
|
@@ -774,7 +925,7 @@ import { useNumber, useIntlayer } from "next-intlayer";
|
|
|
774
925
|
|
|
775
926
|
const ClientComponentExample = () => {
|
|
776
927
|
const [count, setCount] = useState(0);
|
|
777
|
-
const { label, increment } = useIntlayer("counter"); //
|
|
928
|
+
const { label, increment } = useIntlayer("counter"); // returns strings
|
|
778
929
|
const { number } = useNumber();
|
|
779
930
|
|
|
780
931
|
return (
|
|
@@ -802,30 +953,28 @@ const ClientComponentExample = () => {
|
|
|
802
953
|
- احتفظ ببنية متداخلة (`about.counter.label`) وحدد نطاق الخطاف الخاص بك وفقًا لذلك (`useTranslation("about")` + `t("counter.label")` أو `useTranslations("about.counter")` + `t("label")`).
|
|
803
954
|
|
|
804
955
|
- **مواقع الملفات**
|
|
805
|
-
- **next-i18next**
|
|
806
|
-
- **next-intl**
|
|
807
|
-
- **Intlayer**
|
|
956
|
+
- **next-i18next** expects JSON in `public/locales/{lng}/{ns}.json`.
|
|
957
|
+
- **next-intl** is flexible; load messages however you configure.
|
|
958
|
+
- **Intlayer** stores content in TS/JS dictionaries and resolves by key.
|
|
808
959
|
|
|
809
960
|
---
|
|
810
961
|
|
|
811
962
|
### الاستخدام في مكون الخادم
|
|
812
963
|
|
|
813
|
-
|
|
964
|
+
We will take the case of a UI component. This component is a server component, and should be able to be inserted as a child of a client component. (page (server component) -> client component -> server component). As this component can be inserted as a child of a client component, it cannot be async.
|
|
814
965
|
|
|
815
966
|
<Tab defaultTab="next-intl" group='techno'>
|
|
816
967
|
<TabItem label="next-i18next" value="next-i18next">
|
|
817
968
|
|
|
818
|
-
```tsx fileName="src/
|
|
819
|
-
import type { GetStaticProps } from "next";
|
|
820
|
-
import { useTranslation } from "next-i18next";
|
|
821
|
-
|
|
969
|
+
```tsx fileName="src/components/ServerComponent.tsx"
|
|
822
970
|
type ServerComponentProps = {
|
|
971
|
+
t: (key: string) => string;
|
|
972
|
+
locale: string;
|
|
823
973
|
count: number;
|
|
824
974
|
};
|
|
825
975
|
|
|
826
|
-
const ServerComponent = ({ count }: ServerComponentProps) => {
|
|
827
|
-
const
|
|
828
|
-
const formatted = new Intl.NumberFormat(i18n.language).format(count);
|
|
976
|
+
const ServerComponent = ({ t, locale, count }: ServerComponentProps) => {
|
|
977
|
+
const formatted = new Intl.NumberFormat(locale).format(count);
|
|
829
978
|
|
|
830
979
|
return (
|
|
831
980
|
<div>
|
|
@@ -834,35 +983,43 @@ const ServerComponent = ({ count }: ServerComponentProps) => {
|
|
|
834
983
|
</div>
|
|
835
984
|
);
|
|
836
985
|
};
|
|
837
|
-
```
|
|
838
986
|
|
|
839
|
-
|
|
987
|
+
export default ServerComponent;
|
|
988
|
+
```
|
|
840
989
|
|
|
841
990
|
</TabItem>
|
|
842
991
|
<TabItem label="next-intl" value="next-intl">
|
|
843
992
|
|
|
844
993
|
```tsx fileName="src/components/ServerComponent.tsx"
|
|
845
994
|
type ServerComponentProps = {
|
|
846
|
-
|
|
847
|
-
|
|
995
|
+
formattedCount: string;
|
|
996
|
+
label: string;
|
|
997
|
+
increment: string;
|
|
848
998
|
};
|
|
849
999
|
|
|
850
|
-
const ServerComponent = ({
|
|
851
|
-
|
|
852
|
-
|
|
1000
|
+
const ServerComponent = ({
|
|
1001
|
+
formattedCount,
|
|
1002
|
+
label,
|
|
1003
|
+
increment,
|
|
1004
|
+
}: ServerComponentProps) => {
|
|
853
1005
|
return (
|
|
854
1006
|
<div>
|
|
855
|
-
<p>{
|
|
856
|
-
<button aria-label={
|
|
1007
|
+
<p>{formattedCount}</p>
|
|
1008
|
+
<button aria-label={label}>{increment}</button>
|
|
857
1009
|
</div>
|
|
858
1010
|
);
|
|
859
1011
|
};
|
|
1012
|
+
|
|
1013
|
+
export default ServerComponent;
|
|
860
1014
|
```
|
|
861
1015
|
|
|
862
|
-
>
|
|
1016
|
+
> As the server component cannot be async, you need to pass the translations and formatter function as props.
|
|
1017
|
+
>
|
|
1018
|
+
> In your page / layout:
|
|
863
1019
|
>
|
|
1020
|
+
> - `import { getTranslations, getFormatter } from "next-intl/server";`
|
|
864
1021
|
> - `const t = await getTranslations("about.counter");`
|
|
865
|
-
> - `const
|
|
1022
|
+
> - `const formatter = await getFormatter().then((formatter) => formatter.number());`
|
|
866
1023
|
|
|
867
1024
|
</TabItem>
|
|
868
1025
|
<TabItem label="intlayer" value="intlayer">
|
|
@@ -870,7 +1027,11 @@ const ServerComponent = ({ t, count }: ServerComponentProps) => {
|
|
|
870
1027
|
```tsx fileName="src/components/ServerComponent.tsx"
|
|
871
1028
|
import { useIntlayer, useNumber } from "next-intlayer/server";
|
|
872
1029
|
|
|
873
|
-
|
|
1030
|
+
type ServerComponentProps = {
|
|
1031
|
+
count: number;
|
|
1032
|
+
};
|
|
1033
|
+
|
|
1034
|
+
const ServerComponent = ({ count }: ServerComponentProps) => {
|
|
874
1035
|
const { label, increment } = useIntlayer("counter");
|
|
875
1036
|
const { number } = useNumber();
|
|
876
1037
|
|
|
@@ -886,24 +1047,24 @@ const ServerComponent = ({ count }: { count: number }) => {
|
|
|
886
1047
|
</TabItem>
|
|
887
1048
|
</Tab>
|
|
888
1049
|
|
|
889
|
-
>
|
|
1050
|
+
> Intlayer exposes **server-safe** hooks via `next-intlayer/server`. To work, `useIntlayer` and `useNumber` use hooks-like syntax, similar to the client hooks, but depend under the hood on the server context (`IntlayerServerProvider`).
|
|
890
1051
|
|
|
891
1052
|
### البيانات الوصفية / خريطة الموقع / روبوتات البحث
|
|
892
1053
|
|
|
893
1054
|
ترجمة المحتوى أمر رائع. لكن الناس عادةً ما ينسون أن الهدف الرئيسي من التدويل هو جعل موقعك الإلكتروني أكثر ظهورًا للعالم. التدويل هو رافعة مذهلة لتحسين ظهور موقعك الإلكتروني.
|
|
894
1055
|
|
|
895
|
-
|
|
1056
|
+
Here's a list of good practices regarding multilingual SEO.
|
|
896
1057
|
|
|
897
|
-
-
|
|
898
|
-
>
|
|
899
|
-
-
|
|
1058
|
+
- set hreflang meta tags in the `<head>` tag
|
|
1059
|
+
> It helps search engines to understand what languages are available on the page
|
|
1060
|
+
- list all pages translations in the sitemap.xml using `http://www.w3.org/1999/xhtml` XML schema
|
|
900
1061
|
>
|
|
901
|
-
-
|
|
1062
|
+
- do not forget to exclude prefixed pages from the robots.txt (e.g. `/dashboard`, and `/fr/dashboard`, `/es/dashboard`)
|
|
902
1063
|
>
|
|
903
|
-
-
|
|
1064
|
+
- use custom Link component to redirect to the most localized page (e.g. in french `<a href="/fr/about">A propos</a>` )
|
|
904
1065
|
>
|
|
905
1066
|
|
|
906
|
-
|
|
1067
|
+
Developers often forget to properly reference their pages across locales.
|
|
907
1068
|
|
|
908
1069
|
<Tab defaultTab="next-intl" group='techno'>
|
|
909
1070
|
|
|
@@ -935,10 +1096,9 @@ export async function generateMetadata({
|
|
|
935
1096
|
}): Promise<Metadata> {
|
|
936
1097
|
const { locale } = params;
|
|
937
1098
|
|
|
938
|
-
//
|
|
939
|
-
const messages = (
|
|
940
|
-
|
|
941
|
-
).default;
|
|
1099
|
+
// Import the correct JSON bundle from src/locales
|
|
1100
|
+
const messages = (await import("@/locales/" + locale + "/about.json"))
|
|
1101
|
+
.default;
|
|
942
1102
|
|
|
943
1103
|
const languages = Object.fromEntries(
|
|
944
1104
|
locales.map((locale) => [locale, localizedPath(locale, "/about")])
|
|
@@ -955,7 +1115,7 @@ export async function generateMetadata({
|
|
|
955
1115
|
}
|
|
956
1116
|
|
|
957
1117
|
export default async function AboutPage() {
|
|
958
|
-
return <h1
|
|
1118
|
+
return <h1>About</h1>;
|
|
959
1119
|
}
|
|
960
1120
|
```
|
|
961
1121
|
|
|
@@ -971,7 +1131,7 @@ export default function sitemap(): MetadataRoute.Sitemap {
|
|
|
971
1131
|
{
|
|
972
1132
|
url: abs(defaultLocale, "/about"),
|
|
973
1133
|
lastModified: new Date(),
|
|
974
|
-
changeFrequency: "
|
|
1134
|
+
changeFrequency: "monthly",
|
|
975
1135
|
priority: 0.7,
|
|
976
1136
|
alternates: { languages },
|
|
977
1137
|
},
|
|
@@ -1041,7 +1201,7 @@ export async function generateMetadata({
|
|
|
1041
1201
|
};
|
|
1042
1202
|
}
|
|
1043
1203
|
|
|
1044
|
-
// ...
|
|
1204
|
+
// ... Rest of the page code
|
|
1045
1205
|
```
|
|
1046
1206
|
|
|
1047
1207
|
```tsx fileName="src/app/sitemap.ts"
|
|
@@ -1122,7 +1282,7 @@ export const generateMetadata = async ({
|
|
|
1122
1282
|
};
|
|
1123
1283
|
};
|
|
1124
1284
|
|
|
1125
|
-
// ...
|
|
1285
|
+
// ... Rest of the page code
|
|
1126
1286
|
```
|
|
1127
1287
|
|
|
1128
1288
|
```tsx fileName="src/app/sitemap.ts"
|
|
@@ -1143,16 +1303,14 @@ const sitemap = (): MetadataRoute.Sitemap => [
|
|
|
1143
1303
|
import { getMultilingualUrls } from "intlayer";
|
|
1144
1304
|
import type { MetadataRoute } from "next";
|
|
1145
1305
|
|
|
1146
|
-
// دالة لجلب جميع الروابط متعددة اللغات من قائمة الروابط
|
|
1147
1306
|
const getAllMultilingualUrls = (urls: string[]) =>
|
|
1148
1307
|
urls.flatMap((url) => Object.values(getMultilingualUrls(url)) as string[]);
|
|
1149
1308
|
|
|
1150
|
-
// إعدادات ملف robots.txt مع قواعد السماح والمنع للزواحف
|
|
1151
1309
|
const robots = (): MetadataRoute.Robots => ({
|
|
1152
1310
|
rules: {
|
|
1153
1311
|
userAgent: "*",
|
|
1154
1312
|
allow: ["/"],
|
|
1155
|
-
disallow: getAllMultilingualUrls(["/dashboard"]),
|
|
1313
|
+
disallow: getAllMultilingualUrls(["/dashboard"]),
|
|
1156
1314
|
},
|
|
1157
1315
|
host: "https://example.com",
|
|
1158
1316
|
sitemap: "https://example.com/sitemap.xml",
|
|
@@ -1164,10 +1322,83 @@ export default robots;
|
|
|
1164
1322
|
</TabItem>
|
|
1165
1323
|
</Tab>
|
|
1166
1324
|
|
|
1167
|
-
>
|
|
1325
|
+
> Intlayer provides a `getMultilingualUrls` function to generate multilingual URLs for your sitemap.
|
|
1168
1326
|
|
|
1169
1327
|
---
|
|
1170
1328
|
|
|
1329
|
+
### Middleware for locale routing
|
|
1330
|
+
|
|
1331
|
+
<Tab defaultTab="next-intl" group='techno'>
|
|
1332
|
+
<TabItem label="next-i18next" value="next-i18next">
|
|
1333
|
+
|
|
1334
|
+
Add a middleware to handle locale detection and routing:
|
|
1335
|
+
|
|
1336
|
+
```ts fileName="src/middleware.ts"
|
|
1337
|
+
import { NextResponse, type NextRequest } from "next/server";
|
|
1338
|
+
import { defaultLocale, locales } from "@/i18n.config";
|
|
1339
|
+
|
|
1340
|
+
const PUBLIC_FILE = /\.[^/]+$/; // exclude files with extensions
|
|
1341
|
+
|
|
1342
|
+
export function middleware(request: NextRequest) {
|
|
1343
|
+
const { pathname } = request.nextUrl;
|
|
1344
|
+
|
|
1345
|
+
if (
|
|
1346
|
+
pathname.startsWith("/_next") ||
|
|
1347
|
+
pathname.startsWith("/api") ||
|
|
1348
|
+
pathname.startsWith("/static") ||
|
|
1349
|
+
PUBLIC_FILE.test(pathname)
|
|
1350
|
+
) {
|
|
1351
|
+
return;
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
const hasLocale = locales.some(
|
|
1355
|
+
(l) => pathname === "/" + l || pathname.startsWith("/" + l + "/")
|
|
1356
|
+
);
|
|
1357
|
+
if (!hasLocale) {
|
|
1358
|
+
const locale = defaultLocale;
|
|
1359
|
+
const url = request.nextUrl.clone();
|
|
1360
|
+
url.pathname = "/" + locale + (pathname === "/" ? "" : pathname);
|
|
1361
|
+
return NextResponse.redirect(url);
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
export const config = {
|
|
1366
|
+
matcher: [
|
|
1367
|
+
// Match all paths except the ones starting with these and files with an extension
|
|
1368
|
+
"/((?!api|_next|static|.*\\..*).*)",
|
|
1369
|
+
],
|
|
1370
|
+
};
|
|
1371
|
+
```
|
|
1372
|
+
|
|
1373
|
+
</TabItem>
|
|
1374
|
+
<TabItem label="next-intl" value="next-intl">
|
|
1375
|
+
|
|
1376
|
+
Add a middleware to handle locale detection and routing:
|
|
1377
|
+
|
|
1378
|
+
```ts fileName="src/middleware.ts"
|
|
1379
|
+
import createMiddleware from "next-intl/middleware";
|
|
1380
|
+
import { locales, defaultLocale } from "@/i18n";
|
|
1381
|
+
|
|
1382
|
+
export default createMiddleware({
|
|
1383
|
+
locales: [...locales],
|
|
1384
|
+
defaultLocale,
|
|
1385
|
+
localeDetection: true,
|
|
1386
|
+
});
|
|
1387
|
+
|
|
1388
|
+
export const config = {
|
|
1389
|
+
// Skip API, Next internals and static assets
|
|
1390
|
+
matcher: ["/((?!api|_next|.*\\..*).*)"],
|
|
1391
|
+
};
|
|
1392
|
+
```
|
|
1393
|
+
|
|
1394
|
+
</TabItem>
|
|
1395
|
+
<TabItem label="intlayer" value="intlayer">
|
|
1396
|
+
|
|
1397
|
+
Intlayer provides built-in middleware handling through the `next-intlayer` package configuration.
|
|
1398
|
+
|
|
1399
|
+
</TabItem>
|
|
1400
|
+
</Tab>
|
|
1401
|
+
|
|
1171
1402
|
---
|
|
1172
1403
|
|
|
1173
1404
|
## والفائز هو…
|