@intlayer/docs 6.1.4 → 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 +1366 -75
- package/blog/ar/nextjs-multilingual-seo-comparison.md +364 -0
- package/blog/de/next-i18next_vs_next-intl_vs_intlayer.md +1288 -72
- package/blog/de/nextjs-multilingual-seo-comparison.md +362 -0
- 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 +583 -336
- package/blog/en/nextjs-multilingual-seo-comparison.md +360 -0
- package/blog/en-GB/next-i18next_vs_next-intl_vs_intlayer.md +1144 -37
- package/blog/en-GB/nextjs-multilingual-seo-comparison.md +360 -0
- package/blog/es/next-i18next_vs_next-intl_vs_intlayer.md +1236 -64
- package/blog/es/nextjs-multilingual-seo-comparison.md +363 -0
- package/blog/fr/next-i18next_vs_next-intl_vs_intlayer.md +1142 -75
- package/blog/fr/nextjs-multilingual-seo-comparison.md +362 -0
- package/blog/hi/nextjs-multilingual-seo-comparison.md +363 -0
- package/blog/it/next-i18next_vs_next-intl_vs_intlayer.md +1130 -55
- package/blog/it/nextjs-multilingual-seo-comparison.md +363 -0
- package/blog/ja/next-i18next_vs_next-intl_vs_intlayer.md +1150 -76
- package/blog/ja/nextjs-multilingual-seo-comparison.md +362 -0
- package/blog/ko/next-i18next_vs_next-intl_vs_intlayer.md +1139 -73
- package/blog/ko/nextjs-multilingual-seo-comparison.md +362 -0
- package/blog/pt/next-i18next_vs_next-intl_vs_intlayer.md +1143 -76
- package/blog/pt/nextjs-multilingual-seo-comparison.md +362 -0
- package/blog/ru/next-i18next_vs_next-intl_vs_intlayer.md +1150 -74
- package/blog/ru/nextjs-multilingual-seo-comparison.md +370 -0
- package/blog/tr/next-i18next_vs_next-intl_vs_intlayer.md +2 -0
- package/blog/tr/nextjs-multilingual-seo-comparison.md +362 -0
- package/blog/zh/next-i18next_vs_next-intl_vs_intlayer.md +1152 -75
- package/blog/zh/nextjs-multilingual-seo-comparison.md +394 -0
- package/dist/cjs/generated/blog.entry.cjs +16 -0
- package/dist/cjs/generated/blog.entry.cjs.map +1 -1
- package/dist/cjs/generated/docs.entry.cjs +16 -0
- package/dist/cjs/generated/docs.entry.cjs.map +1 -1
- package/dist/esm/generated/blog.entry.mjs +16 -0
- package/dist/esm/generated/blog.entry.mjs.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/blog.entry.d.ts +1 -0
- package/dist/types/generated/blog.entry.d.ts.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/interest_of_intlayer.md +2 -2
- 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/blog.entry.ts +16 -0
- package/src/generated/docs.entry.ts +16 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
---
|
|
2
2
|
createdAt: 2025-08-23
|
|
3
|
-
updatedAt: 2025-
|
|
3
|
+
updatedAt: 2025-09-29
|
|
4
4
|
title: next-i18next مقابل next-intl مقابل Intlayer
|
|
5
|
-
description: مقارنة بين next-i18next و next-intl و Intlayer لتدويل
|
|
5
|
+
description: مقارنة بين next-i18next و next-intl و Intlayer لتدويل تطبيق Next.js
|
|
6
6
|
keywords:
|
|
7
7
|
- next-intl
|
|
8
8
|
- next-i18next
|
|
@@ -10,7 +10,7 @@ keywords:
|
|
|
10
10
|
- التدويل
|
|
11
11
|
- مدونة
|
|
12
12
|
- Next.js
|
|
13
|
-
-
|
|
13
|
+
- جافا سكريبت
|
|
14
14
|
- React
|
|
15
15
|
slugs:
|
|
16
16
|
- blog
|
|
@@ -19,144 +19,1435 @@ slugs:
|
|
|
19
19
|
|
|
20
20
|
# next-i18next مقابل next-intl مقابل intlayer | التدويل في Next.js (i18n)
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
لنلقي نظرة على أوجه التشابه والاختلاف بين ثلاثة خيارات للتدويل في Next.js: next-i18next، next-intl، و Intlayer.
|
|
23
|
+
|
|
24
|
+
هذا ليس درسًا كاملاً. إنها مقارنة لمساعدتك في الاختيار.
|
|
25
|
+
|
|
26
|
+
نركز على **موجه التطبيقات في Next.js 13+** (مع **مكونات خادم React**) ونقيم:
|
|
24
27
|
|
|
25
28
|
1. **البنية والتنظيم المحتوى**
|
|
26
29
|
2. **TypeScript والأمان**
|
|
27
30
|
3. **معالجة الترجمات المفقودة**
|
|
28
|
-
4. **التوجيه
|
|
31
|
+
4. **التوجيه والوسيطات**
|
|
29
32
|
5. **الأداء وسلوك التحميل**
|
|
30
33
|
6. **تجربة المطور (DX)، الأدوات والصيانة**
|
|
31
34
|
7. **تحسين محركات البحث (SEO) وقابلية التوسع في المشاريع الكبيرة**
|
|
32
35
|
|
|
33
|
-
> **ملخص**: يمكن لجميع الثلاثة تعريب تطبيق Next.js. إذا كنت تريد **محتوى مخصص للمكونات**، **أنواع TypeScript صارمة**، **فحوصات
|
|
36
|
+
> **ملخص**: يمكن لجميع الثلاثة تعريب تطبيق Next.js. إذا كنت تريد **محتوى مخصص للمكونات**، **أنواع TypeScript صارمة**، **فحوصات المفاتيح المفقودة أثناء البناء**، **قواميس مُحسّنة بالتخلص من الشجر غير المستخدم**، و**موجه تطبيقات من الدرجة الأولى + مساعدات SEO**، فإن **Intlayer** هو الخيار الأكثر اكتمالًا وحداثة.
|
|
37
|
+
|
|
38
|
+
> من الالتباسات الشائعة بين المطورين هو الاعتقاد بأن `next-intl` هو نسخة Next.js من `react-intl`. هذا غير صحيح — فـ `next-intl` تتم صيانته بواسطة [Amann](https://github.com/amannn)، بينما `react-intl` تتم صيانته بواسطة [FormatJS](https://github.com/formatjs/formatjs).
|
|
34
39
|
|
|
35
40
|
---
|
|
36
41
|
|
|
37
|
-
##
|
|
42
|
+
## باختصار
|
|
38
43
|
|
|
39
44
|
- **next-intl** - تنسيق رسائل خفيف الوزن وبسيط مع دعم قوي لـ Next.js. الكتالوجات المركزية شائعة؛ تجربة المطور بسيطة، لكن الأمان والصيانة على نطاق واسع تبقى في الغالب مسؤوليتك.
|
|
40
|
-
- **next-i18next** - i18next في
|
|
41
|
-
- **Intlayer** - نموذج محتوى يركز على المكونات لـ Next.js،
|
|
45
|
+
- **next-i18next** - i18next في هيئة Next.js. نظام بيئي ناضج وميزات عبر الإضافات (مثل ICU)، لكن التهيئة قد تكون مطولة وتميل الكتالوجات إلى المركزية مع نمو المشاريع.
|
|
46
|
+
- **Intlayer** - نموذج محتوى يركز على المكونات لـ Next.js، **كتابة صارمة بـ TS**، **فحوصات وقت البناء**، **إزالة الشيفرة غير المستخدمة (tree-shaking)**، **وسائط مدمجة ومساعدات SEO**، محرر/نظام إدارة محتوى بصري اختياري، وترجمات بمساعدة الذكاء الاصطناعي.
|
|
42
47
|
|
|
43
48
|
---
|
|
44
49
|
|
|
45
|
-
|
|
50
|
+
| Library | GitHub Stars | Total Commits | Last Commit | First Version | NPM Version | NPM Downloads |
|
|
51
|
+
| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- | ------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
|
|
52
|
+
| `aymericzip/intlayer` | [](https://github.com/aymericzip/intlayer/stargazers) | [](https://github.com/aymericzip/intlayer/commits) | [](https://github.com/aymericzip/intlayer/commits) | April 2024 | [](https://www.npmjs.com/package/intlayer) | [](https://www.npmjs.com/package/intlayer) |
|
|
53
|
+
| `amannn/next-intl` | [](https://github.com/amannn/next-intl/stargazers) | [](https://github.com/amannn/next-intl/commits) | [](https://github.com/amannn/next-intl/commits) | Nov 2020 | [](https://www.npmjs.com/package/next-intl) | [](https://www.npmjs.com/package/next-intl) |
|
|
54
|
+
| `i18next/i18next` | [](https://github.com/i18next/i18next/stargazers) | [](https://github.com/i18next/i18next/commits) | [](https://github.com/i18next/i18next/commits) | Jan 2012 | [](https://www.npmjs.com/package/i18next) | [](https://www.npmjs.com/package/i18next) |
|
|
55
|
+
| `i18next/next-i18next` | [](https://github.com/i18next/next-i18next/stargazers) | [](https://github.com/i18next/next-i18next/commits) | [](https://github.com/i18next/next-i18next/commits) | Nov 2018 | [](https://www.npmjs.com/package/next-i18next) | [](https://www.npmjs.com/package/next-i18next) |
|
|
46
56
|
|
|
47
|
-
|
|
48
|
-
| ------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
|
|
49
|
-
| **الترجمات بالقرب من المكونات** | ✅ نعم، المحتوى موضوع بجانب كل مكون | ❌ لا | ❌ لا |
|
|
50
|
-
| **تكامل TypeScript** | ✅ متقدم، أنواع صارمة مولدة تلقائيًا | ✅ جيد | ⚠️ أساسي |
|
|
51
|
-
| **كشف الترجمات المفقودة** | ✅ تمييز أخطاء TypeScript وتحذير/خطأ أثناء وقت البناء | ⚠️ استرجاع وقت التشغيل | ⚠️ استرجاع وقت التشغيل |
|
|
52
|
-
| **المحتوى الغني (JSX/Markdown/المكونات)** | ✅ دعم مباشر | ❌ غير مصمم للعقد الغنية | ⚠️ محدود |
|
|
53
|
-
| **الترجمة المدعومة بالذكاء الاصطناعي** | ✅ نعم، يدعم عدة مزودي ذكاء اصطناعي. يمكن استخدامه باستخدام مفاتيح API الخاصة بك. يأخذ في الاعتبار سياق تطبيقك ونطاق المحتوى | ❌ لا | ❌ لا |
|
|
54
|
-
| **المحرر المرئي** | ✅ نعم، محرر مرئي محلي + نظام إدارة محتوى اختياري؛ يمكنه إخراج محتوى قاعدة الشيفرة؛ قابل للتضمين | ❌ لا / متوفر عبر منصات التوطين الخارجية | ❌ لا / متوفر عبر منصات التوطين الخارجية |
|
|
55
|
-
| **التوجيه المحلي** | ✅ نعم، يدعم المسارات المحلية مباشرة (يعمل مع Next.js و Vite) | ✅ مدمج، يدعم App Router جزء `[locale]` | ✅ مدمج |
|
|
56
|
-
| **توليد المسارات الديناميكية** | ✅ نعم | ✅ نعم | ✅ نعم |
|
|
57
|
-
| **التصريف الجمعي** | ✅ أنماط قائمة على التعداد | ✅ جيد | ✅ جيد |
|
|
58
|
-
| **التنسيق (التواريخ، الأرقام، العملات)** | ✅ منسقات محسّنة (Intl في الخلفية) | ✅ جيد (مساعدات Intl) | ✅ جيد (مساعدات Intl) |
|
|
59
|
-
| **تنسيق المحتوى** | ✅ .tsx, .ts, .js, .json, .md, .txt, (.yaml قيد العمل) | ✅ .json, .js, .ts | ⚠️ .json |
|
|
60
|
-
| **دعم ICU** | ⚠️ قيد العمل | ✅ نعم | ⚠️ عبر الإضافة (`i18next-icu`) |
|
|
61
|
-
| **مساعدو تحسين محركات البحث (hreflang، خريطة الموقع)** | ✅ أدوات مدمجة: مساعدات لخريطة الموقع، robots.txt، البيانات الوصفية | ✅ جيد | ✅ جيد |
|
|
62
|
-
| **النظام البيئي / المجتمع** | ⚠️ أصغر حجماً لكنه ينمو بسرعة ويتسم بالتفاعل | ✅ متوسط الحجم، يركز على Next.js | ✅ متوسط الحجم، يركز على Next.js |
|
|
63
|
-
| **التصيير على جانب الخادم ومكونات الخادم** | ✅ نعم، مُبسّط للتصيير على جانب الخادم / مكونات React Server | ⚠️ مدعوم على مستوى الصفحة ولكن يحتاج إلى تمرير دوال t على شجرة المكونات لمكونات الخادم الفرعية | ⚠️ مدعوم على مستوى الصفحة ولكن يحتاج إلى تمرير دوال t على شجرة المكونات لمكونات الخادم الفرعية |
|
|
64
|
-
| **Tree-shaking (تحميل المحتوى المستخدم فقط)** | ✅ نعم، لكل مكون أثناء وقت البناء عبر إضافات Babel/SWC | ⚠️ جزئي | ⚠️ جزئي |
|
|
65
|
-
| **التحميل الكسول** | ✅ نعم، لكل لغة / لكل قاموس | ✅ نعم (لكل مسار/لكل لغة)، يحتاج إلى إدارة مساحة الأسماء | ✅ نعم (لكل مسار/لكل لغة)، يحتاج إلى إدارة مساحة الأسماء |
|
|
66
|
-
| **تنظيف المحتوى غير المستخدم** | ✅ نعم، لكل قاموس أثناء وقت البناء | ❌ لا، يمكن إدارته يدويًا باستخدام إدارة مساحة الأسماء | ❌ لا، يمكن إدارته يدويًا باستخدام إدارة مساحة الأسماء |
|
|
67
|
-
| **إدارة المشاريع الكبيرة** | ✅ يشجع على التكوين المعياري، مناسب لأنظمة التصميم | ✅ معياري مع الإعداد | ✅ معياري مع الإعداد |
|
|
57
|
+
> يتم تحديث الشارات تلقائيًا. ستختلف اللقطات مع مرور الوقت.
|
|
68
58
|
|
|
69
59
|
---
|
|
70
60
|
|
|
71
|
-
## مقارنة
|
|
61
|
+
## مقارنة الميزات جنبًا إلى جنب (مركزة على Next.js)
|
|
62
|
+
|
|
63
|
+
| الميزة | `next-intlayer` (Intlayer) | `next-intl` | `next-i18next` |
|
|
64
|
+
| ------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
|
|
65
|
+
| **الترجمات بالقرب من المكونات** | ✅ نعم، المحتوى موضوع بجانب كل مكون | ❌ لا | ❌ لا |
|
|
66
|
+
| **تكامل TypeScript** | ✅ متقدم، أنواع صارمة مولدة تلقائيًا | ✅ جيد | ⚠️ أساسي |
|
|
67
|
+
| **كشف الترجمات المفقودة** | ✅ تمييز أخطاء TypeScript وتحذير/خطأ أثناء وقت البناء | ⚠️ تراجع في وقت التشغيل | ⚠️ تراجع في وقت التشغيل |
|
|
68
|
+
| **المحتوى الغني (JSX/Markdown/المكونات)** | ✅ دعم مباشر | ❌ غير مصمم للعقد الغنية | ⚠️ محدود |
|
|
69
|
+
| **الترجمة المدعومة بالذكاء الاصطناعي** | ✅ نعم، يدعم عدة مزودي ذكاء اصطناعي. يمكن استخدامه باستخدام مفاتيح API الخاصة بك. يأخذ في الاعتبار سياق تطبيقك ونطاق المحتوى | ❌ لا | ❌ لا |
|
|
70
|
+
| **المحرر المرئي** | ✅ نعم، محرر مرئي محلي + نظام إدارة محتوى اختياري؛ يمكنه إخراج محتوى قاعدة الشيفرة؛ قابل للتضمين | ❌ لا / متوفر عبر منصات التوطين الخارجية | ❌ لا / متوفر عبر منصات التوطين الخارجية |
|
|
71
|
+
| **التوجيه المحلي** | ✅ نعم، يدعم المسارات المحلية مباشرة (يعمل مع Next.js و Vite) | ✅ مدمج، يدعم App Router جزء `[locale]` | ✅ مدمج |
|
|
72
|
+
| **توليد المسارات الديناميكية** | ✅ نعم | ✅ نعم | ✅ نعم |
|
|
73
|
+
| **التعددية (جمع الكلمات)** | ✅ أنماط قائمة على التعداد | ✅ جيد | ✅ جيد |
|
|
74
|
+
| **تنسيق (التواريخ، الأرقام، العملات)** | ✅ منسقات محسّنة (Intl في الخلفية) | ✅ جيد (مساعدات Intl) | ✅ جيد (مساعدات Intl) |
|
|
75
|
+
| **تنسيق المحتوى** | ✅ .tsx، .ts، .js، .json، .md، .txt، (.yaml قيد العمل) | ✅ .json، .js، .ts | ⚠️ .json |
|
|
76
|
+
| **دعم ICU** | ⚠️ جاري العمل عليه | ✅ نعم | ⚠️ عبر الإضافة (`i18next-icu`) |
|
|
77
|
+
| **مساعدو تحسين محركات البحث (hreflang، خريطة الموقع)** | ✅ أدوات مدمجة: مساعدات لخريطة الموقع، robots.txt، البيانات الوصفية | ✅ جيد | ✅ جيد |
|
|
78
|
+
| **النظام البيئي / المجتمع** | ⚠️ أصغر ولكن ينمو بسرعة ويتفاعل | ✅ جيد | ✅ جيد |
|
|
79
|
+
| **التصيير على جانب الخادم ومكونات الخادم** | ✅ نعم، مُبسّط للتصيير على جانب الخادم / مكونات خادم React | ⚠️ مدعوم على مستوى الصفحة ولكن يحتاج إلى تمرير دوال t على شجرة المكونات لمكونات الخادم الفرعية | ⚠️ مدعوم على مستوى الصفحة ولكن يحتاج إلى تمرير دوال t على شجرة المكونات لمكونات الخادم الفرعية |
|
|
80
|
+
| **إزالة الشيفرة غير المستخدمة (تحميل المحتوى المستخدم فقط)** | ✅ نعم، لكل مكون أثناء وقت البناء عبر إضافات Babel/SWC | ⚠️ جزئي | ⚠️ جزئي |
|
|
81
|
+
| **التحميل الكسول** | ✅ نعم، لكل لغة / لكل قاموس | ✅ نعم (لكل مسار/لكل لغة)، يحتاج إلى إدارة مساحة الأسماء | ✅ نعم (لكل مسار/لكل لغة)، يحتاج إلى إدارة مساحة الأسماء |
|
|
82
|
+
| **تنظيف المحتوى غير المستخدم** | ✅ نعم، لكل قاموس أثناء وقت البناء | ❌ لا، يمكن إدارته يدويًا باستخدام إدارة المساحات الاسمية | ❌ لا، يمكن إدارته يدويًا باستخدام إدارة المساحات الاسمية |
|
|
83
|
+
| **إدارة المشاريع الكبيرة** | ✅ يشجع على التصميم المعياري، مناسب لأنظمة التصميم | ✅ معياري مع الإعداد | ✅ معياري مع الإعداد |
|
|
84
|
+
| **اختبار الترجمات المفقودة (CLI/CI)** | ✅ CLI: `npx intlayer content test` (تدقيق مناسب لـ CI) | ⚠️ غير مدمج؛ الوثائق تقترح `npx @lingual/i18n-check` | ⚠️ غير مدمج؛ يعتمد على أدوات i18next / وقت التشغيل `saveMissing` |
|
|
85
|
+
|
|
86
|
+
---
|
|
72
87
|
|
|
73
|
-
|
|
88
|
+
## المقدمة
|
|
74
89
|
|
|
75
|
-
|
|
76
|
-
- **Intlayer**: يشجع على وجود **قواميس لكل مكون** (أو لكل ميزة) **متمركزة** مع الكود الذي تخدمه. هذا يقلل العبء الذهني، ويسهل تكرار/ترحيل أجزاء واجهة المستخدم، ويقلل من النزاعات بين الفرق المختلفة. المحتوى غير المستخدم يكون من السهل اكتشافه وحذفه بشكل طبيعي.
|
|
90
|
+
يوفر Next.js دعمًا مدمجًا للتوجيه الدولي (مثل مقاطع اللغة). لكن هذه الميزة لا تقوم بالترجمة بمفردها. لا يزال يتعين عليك استخدام مكتبة لعرض المحتوى المحلي للمستخدمين.
|
|
77
91
|
|
|
78
|
-
|
|
92
|
+
توجد العديد من مكتبات i18n، ولكن في عالم Next.js اليوم، هناك ثلاث مكتبات تكتسب شعبية: next-i18next، next-intl، و Intlayer.
|
|
79
93
|
|
|
80
94
|
---
|
|
81
95
|
|
|
82
|
-
|
|
96
|
+
## الهندسة والقابلية للتوسع
|
|
83
97
|
|
|
84
|
-
- **next-intl**:
|
|
85
|
-
- **
|
|
86
|
-
- **Intlayer**: **ينشئ أنواعًا صارمة** من محتواك. **الإكمال التلقائي في بيئة التطوير** و**أخطاء وقت الترجمة** تكتشف الأخطاء المطبعية والمفاتيح المفقودة قبل النشر.
|
|
98
|
+
- **next-intl / next-i18next**: تعتمد بشكل افتراضي على **كتالوجات مركزية** لكل لغة (بالإضافة إلى **مساحات الأسماء** في i18next). تعمل بشكل جيد في البداية، لكنها غالبًا ما تصبح مساحة مشتركة كبيرة مع زيادة الترابط وتغير المفاتيح.
|
|
99
|
+
- **Intlayer**: تشجع على استخدام **قواميس لكل مكون** (أو لكل ميزة) **متمركزة** مع الكود الذي تخدمه. هذا يقلل من العبء المعرفي، ويسهل تكرار/ترحيل أجزاء واجهة المستخدم، ويقلل من النزاعات بين الفرق. المحتوى غير المستخدم يكون أسهل في الاكتشاف والحذف بشكل طبيعي.
|
|
87
100
|
|
|
88
|
-
**لماذا هذا مهم:**
|
|
101
|
+
**لماذا هذا مهم:** في قواعد الكود الكبيرة أو إعدادات نظام التصميم، **المحتوى المعياري** يتوسع بشكل أفضل من الكتالوجات الأحادية.
|
|
89
102
|
|
|
90
103
|
---
|
|
91
104
|
|
|
92
|
-
|
|
105
|
+
## أحجام الحزم والاعتمادات
|
|
106
|
+
|
|
107
|
+
بعد بناء التطبيق، الحزمة هي جافا سكريبت التي سيقوم المتصفح بتحميلها لعرض الصفحة. لذلك، حجم الحزمة مهم لأداء التطبيق.
|
|
108
|
+
|
|
109
|
+
هناك مكونان مهمان في سياق حزمة تطبيق متعدد اللغات:
|
|
110
|
+
|
|
111
|
+
- كود التطبيق
|
|
112
|
+
- المحتوى الذي يتم تحميله بواسطة المتصفح
|
|
113
|
+
|
|
114
|
+
## كود التطبيق
|
|
115
|
+
|
|
116
|
+
أهمية كود التطبيق ضئيلة في هذه الحالة. جميع الحلول الثلاثة تدعم تقنية tree-shaking، مما يعني أن الأجزاء غير المستخدمة من الكود لا تُدرج في الحزمة.
|
|
117
|
+
|
|
118
|
+
إليك مقارنة لحجم حزمة جافا سكريبت التي يتم تحميلها بواسطة المتصفح لتطبيق متعدد اللغات مع الحلول الثلاثة.
|
|
119
|
+
|
|
120
|
+
إذا لم نحتاج إلى أي مُنسق في التطبيق، فإن قائمة الدوال المُصدرة بعد تطبيق tree-shaking ستكون:
|
|
121
|
+
|
|
122
|
+
- **next-intlayer**: `useIntlayer`, `useLocale`, `NextIntlClientProvider`، (حجم الحزمة هو 180.6 كيلوبايت -> 78.6 كيلوبايت (gzip))
|
|
123
|
+
- **next-intl**: `useTranslations`, `useLocale`, `NextIntlClientProvider`، (حجم الحزمة هو 101.3 كيلوبايت -> 31.4 كيلوبايت (gzip))
|
|
124
|
+
- **next-i18next**: `useTranslation`, `useI18n`, `I18nextProvider`، (حجم الحزمة هو 80.7 كيلوبايت -> 25.5 كيلوبايت (gzip))
|
|
125
|
+
|
|
126
|
+
هذه الدوال هي مجرد أغلفة حول سياق/حالة React، لذا فإن التأثير الكلي لمكتبة i18n على حجم الحزمة هو ضئيل.
|
|
127
|
+
|
|
128
|
+
> Intlayer أكبر قليلاً من `next-intl` و `next-i18next` لأنه يتضمن منطقًا أكثر في دالة `useIntlayer`. هذا مرتبط بالتكامل مع markdown و `intlayer-editor`.
|
|
129
|
+
|
|
130
|
+
## المحتوى والترجمات
|
|
131
|
+
|
|
132
|
+
غالبًا ما يتجاهل المطورون هذا الجزء، ولكن دعونا نعتبر حالة تطبيق يتكون من 10 صفحات بـ 10 لغات. لنفترض أن كل صفحة تحتوي على محتوى فريد بنسبة 100% لتبسيط الحساب (في الواقع، هناك الكثير من المحتوى المكرر بين الصفحات، مثل عنوان الصفحة، الرأس، التذييل، إلخ).
|
|
133
|
+
|
|
134
|
+
المستخدم الذي يرغب في زيارة صفحة `/fr/about` سيقوم بتحميل محتوى صفحة واحدة بلغة معينة. تجاهل تحسين المحتوى يعني تحميل 8200% `((1 + (((10 صفحات - 1) × (10 لغات - 1)))) × 100)` من محتوى التطبيق بشكل غير ضروري. هل ترى المشكلة؟ حتى لو كان هذا المحتوى نصًا فقط، وبينما من المحتمل أنك تفضل التفكير في تحسين صور موقعك، فإنك ترسل محتوى غير مفيد عبر العالم وتجعل أجهزة المستخدمين تعالجه دون فائدة.
|
|
135
|
+
|
|
136
|
+
مسألتان مهمتان:
|
|
93
137
|
|
|
94
|
-
-
|
|
95
|
-
- **Intlayer**: **الكشف أثناء البناء** مع **تحذيرات/أخطاء** للمواقع أو المفاتيح المفقودة.
|
|
138
|
+
- **التقسيم حسب المسار:**
|
|
96
139
|
|
|
97
|
-
|
|
140
|
+
> إذا كنت في صفحة `/about`، لا أريد تحميل محتوى صفحة `/home`
|
|
141
|
+
|
|
142
|
+
- **التقسيم حسب اللغة:**
|
|
143
|
+
|
|
144
|
+
> إذا كنت في صفحة `/fr/about`، لا أريد تحميل محتوى صفحة `/en/about`
|
|
145
|
+
|
|
146
|
+
مرة أخرى، كل الحلول الثلاثة تدرك هذه القضايا وتسمح بإدارة هذه التحسينات. الفرق بين الحلول الثلاثة هو تجربة المطور (DX).
|
|
147
|
+
|
|
148
|
+
يستخدم كل من `next-intl` و `next-i18next` نهجًا مركزيًا لإدارة الترجمات، مما يسمح بتقسيم ملفات JSON حسب اللغة وحسب الملفات الفرعية. في `next-i18next`، نسمي ملفات JSON "مساحات الأسماء" (namespaces)؛ بينما يسمح `next-intl` بإعلان الرسائل. في `intlayer`، نسمي ملفات JSON "القواميس" (dictionaries).
|
|
149
|
+
|
|
150
|
+
- في حالة `next-intl`، مثل `next-i18next`، يتم تحميل المحتوى على مستوى الصفحة/التخطيط، ثم يتم تحميل هذا المحتوى في مزود السياق. هذا يعني أن المطور يجب أن يدير ملفات JSON التي سيتم تحميلها لكل صفحة يدويًا.
|
|
151
|
+
|
|
152
|
+
> في الممارسة العملية، هذا يعني أن المطورين غالبًا ما يتخطون هذا التحسين، مفضلين تحميل كل المحتوى في مزود سياق الصفحة من أجل البساطة.
|
|
153
|
+
|
|
154
|
+
- في حالة `intlayer`، يتم تحميل كل المحتوى في التطبيق. ثم يتولى مكون إضافي (`@intlayer/babel` / `@intlayer/swc`) مهمة تحسين الحزمة عن طريق تحميل المحتوى المستخدم فقط في الصفحة. لذلك لا يحتاج المطور إلى إدارة القواميس التي سيتم تحميلها يدويًا. هذا يسمح بتحسين أفضل، وصيانة أفضل، ويقلل من وقت التطوير.
|
|
155
|
+
|
|
156
|
+
مع نمو التطبيق (خاصة عندما يعمل عدة مطورين على التطبيق)، من الشائع نسيان إزالة المحتوى الذي لم يعد مستخدمًا من ملفات JSON.
|
|
157
|
+
|
|
158
|
+
> لاحظ أن جميع ملفات JSON يتم تحميلها في جميع الحالات (next-intl، next-i18next، intlayer).
|
|
159
|
+
|
|
160
|
+
لهذا السبب، فإن نهج Intlayer أكثر كفاءة: إذا لم يعد يتم استخدام مكون ما، فلن يتم تحميل قاموسه في الحزمة.
|
|
161
|
+
|
|
162
|
+
كيفية تعامل المكتبة مع الاستبدالات (fallbacks) أمر مهم أيضًا. لنفترض أن التطبيق باللغة الإنجليزية بشكل افتراضي، وزار المستخدم صفحة `/fr/about`. إذا كانت الترجمات مفقودة بالفرنسية، فسنعتبر الاستبدال باللغة الإنجليزية.
|
|
163
|
+
|
|
164
|
+
في حالة `next-intl` و `next-i18next`، تتطلب المكتبة تحميل ملفات JSON المتعلقة باللغة الحالية، ولكن أيضًا لغة التراجع (fallback). وبالتالي، مع افتراض أن كل المحتوى قد تُرجم، ستقوم كل صفحة بتحميل محتوى غير ضروري بنسبة 100%. **بالمقارنة، تقوم `intlayer` بمعالجة التراجع أثناء وقت بناء القاموس. لذا، ستقوم كل صفحة بتحميل المحتوى المستخدم فقط.**
|
|
165
|
+
|
|
166
|
+
فيما يلي مثال على تأثير تحسين حجم الحزمة باستخدام `intlayer` في تطبيق vite + react:
|
|
167
|
+
|
|
168
|
+
| الحزمة المحسنة | الحزمة غير المحسنة |
|
|
169
|
+
| ----------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
|
|
170
|
+
|  |  |
|
|
98
171
|
|
|
99
172
|
---
|
|
100
173
|
|
|
101
|
-
|
|
174
|
+
## تايب سكريبت والسلامة
|
|
175
|
+
|
|
176
|
+
<Columns>
|
|
177
|
+
<Column>
|
|
178
|
+
|
|
179
|
+
**next-intl**
|
|
180
|
+
|
|
181
|
+
- دعم قوي لـ TypeScript، لكن **المفاتيح ليست مُعرفة بدقة بشكل افتراضي**؛ ستحتاج للحفاظ على أنماط الأمان يدويًا.
|
|
182
|
+
|
|
183
|
+
</Column>
|
|
184
|
+
<Column>
|
|
185
|
+
|
|
186
|
+
**next-i18next**
|
|
187
|
+
|
|
188
|
+
- تعريفات أساسية للهوكس؛ **التعريف الصارم للمفاتيح يتطلب أدوات/تكوين إضافي**.
|
|
102
189
|
|
|
103
|
-
|
|
104
|
-
|
|
190
|
+
</Column>
|
|
191
|
+
<Column>
|
|
105
192
|
|
|
106
|
-
|
|
193
|
+
**intlayer**
|
|
194
|
+
|
|
195
|
+
- **ينشئ أنواعًا صارمة** من المحتوى الخاص بك. **الإكمال التلقائي في بيئة التطوير** و**أخطاء وقت الترجمة** تكتشف الأخطاء الإملائية والمفاتيح المفقودة قبل النشر.
|
|
196
|
+
|
|
197
|
+
</Column>
|
|
198
|
+
</Columns>
|
|
199
|
+
|
|
200
|
+
**لماذا هذا مهم:** التحقق الصارم من الأنواع ينقل الأخطاء إلى اليسار (التكامل المستمر/البناء) بدلاً من اليمين (وقت التشغيل).
|
|
107
201
|
|
|
108
202
|
---
|
|
109
203
|
|
|
110
|
-
|
|
204
|
+
## التعامل مع الترجمات المفقودة
|
|
205
|
+
|
|
206
|
+
**next-intl**
|
|
207
|
+
|
|
208
|
+
- يعتمد على **الاستعادات أثناء وقت التشغيل** (مثل عرض المفتاح أو اللغة الافتراضية). البناء لا يفشل.
|
|
111
209
|
|
|
112
|
-
-
|
|
113
|
-
- **Intlayer** يسهل **الحد الفاصل بين الخادم والعميل** بواجهة برمجة تطبيقات موحدة ومزودين مصممين لـ RSC، بحيث لا تحتاج إلى تمرير منسقات أو دوال الترجمة عبر شجرة المكونات.
|
|
210
|
+
**next-i18next**
|
|
114
211
|
|
|
115
|
-
|
|
212
|
+
- يعتمد على **الاستعادات أثناء وقت التشغيل** (مثل عرض المفتاح أو اللغة الافتراضية). البناء لا يفشل.
|
|
213
|
+
|
|
214
|
+
**intlayer**
|
|
215
|
+
|
|
216
|
+
- **الكشف أثناء وقت البناء** مع **تحذيرات/أخطاء** للمواقع أو المفاتيح المفقودة.
|
|
217
|
+
|
|
218
|
+
**لماذا هذا مهم:** اكتشاف الفجوات أثناء البناء يمنع ظهور "سلاسل غامضة" في الإنتاج ويتماشى مع قواعد الإصدار الصارمة.
|
|
116
219
|
|
|
117
220
|
---
|
|
118
221
|
|
|
119
|
-
|
|
222
|
+
## التوجيه، الوسيط واستراتيجية عناوين URL
|
|
223
|
+
|
|
224
|
+
<Columns>
|
|
225
|
+
<Column>
|
|
226
|
+
|
|
227
|
+
**next-intl**
|
|
120
228
|
|
|
121
|
-
-
|
|
122
|
-
- **Intlayer**: يقوم بـ **تحليل الشجرة** أثناء البناء و**تحميل كسول لكل قاموس/لغة**. المحتوى غير المستخدم لا يتم شحنه.
|
|
229
|
+
- يعمل مع **توجيه Next.js المحلي** على App Router.
|
|
123
230
|
|
|
124
|
-
|
|
231
|
+
</Column>
|
|
232
|
+
<Column>
|
|
233
|
+
|
|
234
|
+
**next-i18next**
|
|
235
|
+
|
|
236
|
+
- يعمل مع **توجيه Next.js المحلي** على App Router.
|
|
237
|
+
|
|
238
|
+
</Column>
|
|
239
|
+
<Column>
|
|
240
|
+
|
|
241
|
+
**intlayer**
|
|
242
|
+
|
|
243
|
+
- كل ما سبق، بالإضافة إلى **وسيط i18n** (اكتشاف اللغة عبر الرؤوس/الكوكيز) و**مساعدين** لإنشاء عناوين URL محلية ووسوم `<link rel="alternate" hreflang="…">`.
|
|
244
|
+
|
|
245
|
+
</Column>
|
|
246
|
+
</Columns>
|
|
247
|
+
|
|
248
|
+
**لماذا هذا مهم:** تقليل طبقات الربط المخصصة؛ **تجربة مستخدم متسقة** و**تحسين SEO نظيف** عبر اللغات.
|
|
125
249
|
|
|
126
250
|
---
|
|
127
251
|
|
|
128
|
-
|
|
252
|
+
## توافق مكونات الخادم (RSC)
|
|
253
|
+
|
|
254
|
+
<Columns>
|
|
255
|
+
<Column>
|
|
256
|
+
|
|
257
|
+
**next-intl**
|
|
258
|
+
|
|
259
|
+
- يدعم Next.js 13+. غالبًا ما يتطلب تمرير دوال الترجمة/المنسقات عبر شجرة المكونات في الإعدادات المختلطة.
|
|
260
|
+
|
|
261
|
+
</Column>
|
|
262
|
+
<Column>
|
|
263
|
+
|
|
264
|
+
**next-i18next**
|
|
265
|
+
|
|
266
|
+
- يدعم Next.js 13+. قيود مماثلة مع تمرير أدوات الترجمة عبر الحدود.
|
|
267
|
+
|
|
268
|
+
</Column>
|
|
269
|
+
<Column>
|
|
270
|
+
|
|
271
|
+
**intlayer**
|
|
272
|
+
|
|
273
|
+
- يدعم Next.js 13+ ويسهل **الحد الفاصل بين الخادم/العميل** من خلال واجهة برمجة تطبيقات متسقة ومزودين موجهين لـ RSC، متجنبًا نقل أدوات التنسيق أو دوال الترجمة.
|
|
274
|
+
|
|
275
|
+
</Column>
|
|
276
|
+
</Columns>
|
|
277
|
+
|
|
278
|
+
**لماذا هذا مهم:** نموذج ذهني أنظف وحالات حافة أقل في الأشجار الهجينة.
|
|
279
|
+
|
|
280
|
+
---
|
|
281
|
+
|
|
282
|
+
## تجربة المطور، الأدوات والصيانة
|
|
283
|
+
|
|
284
|
+
<Columns>
|
|
285
|
+
<Column>
|
|
286
|
+
|
|
287
|
+
**next-intl**
|
|
288
|
+
|
|
289
|
+
- غالبًا ما يُستخدم مع منصات التوطين الخارجية وسير العمل التحريري.
|
|
290
|
+
|
|
291
|
+
</Column>
|
|
292
|
+
<Column>
|
|
293
|
+
|
|
294
|
+
**next-i18next**
|
|
295
|
+
|
|
296
|
+
- غالبًا ما يُستخدم مع منصات التوطين الخارجية وسير العمل التحريري.
|
|
297
|
+
|
|
298
|
+
</Column>
|
|
299
|
+
<Column>
|
|
300
|
+
|
|
301
|
+
**intlayer**
|
|
129
302
|
|
|
130
|
-
-
|
|
131
|
-
|
|
303
|
+
- يوفر محررًا بصريًا مجانيًا ونظام إدارة محتوى اختياري (متوافق مع Git أو خارجي)، بالإضافة إلى امتداد VSCode وترجمات بمساعدة الذكاء الاصطناعي باستخدام مفاتيح المزود الخاصة بك.
|
|
304
|
+
|
|
305
|
+
</Column>
|
|
306
|
+
</Columns>
|
|
132
307
|
|
|
133
308
|
**لماذا هذا مهم:** يقلل من تكلفة العمليات ويقصر دورة التواصل بين المطورين ومؤلفي المحتوى.
|
|
134
309
|
|
|
310
|
+
## التكامل مع منصات الترجمة (TMS)
|
|
311
|
+
|
|
312
|
+
تعتمد المؤسسات الكبيرة غالبًا على أنظمة إدارة الترجمة (TMS) مثل **Crowdin**، **Phrase**، **Lokalise**، **Localizely**، أو **Localazy**.
|
|
313
|
+
|
|
314
|
+
- **لماذا تهتم الشركات**
|
|
315
|
+
- **التعاون والأدوار**: يشارك عدة أطراف: المطورون، مدراء المنتجات، المترجمون، المراجعون، فرق التسويق.
|
|
316
|
+
- **الحجم والكفاءة**: الترجمة المستمرة، والمراجعة في السياق.
|
|
317
|
+
|
|
318
|
+
- **next-intl / next-i18next**
|
|
319
|
+
- عادةً ما تستخدم **كتالوجات JSON مركزية**، لذا فإن التصدير/الاستيراد مع أنظمة إدارة الترجمة (TMS) يكون بسيطًا.
|
|
320
|
+
- أنظمة بيئية ناضجة وأمثلة/تكاملات للمنصات المذكورة أعلاه.
|
|
321
|
+
|
|
322
|
+
- **Intlayer**
|
|
323
|
+
- يشجع على **القواميس اللامركزية لكل مكون** ويدعم محتوى **TypeScript/TSX/JS/JSON/MD**.
|
|
324
|
+
- هذا يحسن من التجزئة في الكود، لكنه قد يجعل تكامل أنظمة إدارة الترجمة (TMS) السهل الاستخدام أكثر صعوبة عندما يتوقع الأداة ملفات JSON مركزية ومسطحة.
|
|
325
|
+
- يوفر Intlayer بدائل: **ترجمات بمساعدة الذكاء الاصطناعي** (باستخدام مفاتيح المزود الخاصة بك)، و**محرر مرئي/نظام إدارة محتوى**، وعمليات **CLI/CI** لالتقاط وملء الفجوات.
|
|
326
|
+
|
|
327
|
+
> ملاحظة: `next-intl` و `i18next` يقبلان أيضًا كتالوجات TypeScript. إذا كان فريقك يخزن الرسائل في ملفات `.ts` أو يوزعها حسب الميزة، فقد تواجه احتكاكًا مشابهًا مع نظام إدارة الترجمة (TMS). ومع ذلك، تبقى العديد من إعدادات `next-intl` مركزية في مجلد `locales/`، مما يجعل إعادة هيكلتها إلى JSON لـ TMS أسهل قليلاً.
|
|
328
|
+
|
|
329
|
+
## تجربة المطور
|
|
330
|
+
|
|
331
|
+
هذا الجزء يقدم مقارنة عميقة بين الحلول الثلاثة. بدلاً من النظر في الحالات البسيطة كما هو موضح في وثائق "البدء السريع" لكل حل، سنأخذ في الاعتبار حالة استخدام حقيقية، أكثر تشابهًا مع مشروع حقيقي.
|
|
332
|
+
|
|
333
|
+
### هيكل التطبيق
|
|
334
|
+
|
|
335
|
+
هيكل التطبيق مهم لضمان سهولة صيانة قاعدة الشيفرة الخاصة بك.
|
|
336
|
+
|
|
337
|
+
<Tab defaultTab="next-intl" group='techno'>
|
|
338
|
+
|
|
339
|
+
<TabItem label="next-i18next" value="next-i18next">
|
|
340
|
+
|
|
341
|
+
```bash
|
|
342
|
+
.
|
|
343
|
+
├── i18n.config.ts
|
|
344
|
+
└── src
|
|
345
|
+
├── locales
|
|
346
|
+
│ ├── en
|
|
347
|
+
│ │ ├── common.json
|
|
348
|
+
│ │ └── about.json
|
|
349
|
+
│ └── fr
|
|
350
|
+
│ ├── common.json
|
|
351
|
+
│ └── about.json
|
|
352
|
+
├── app
|
|
353
|
+
│ ├── i18n
|
|
354
|
+
│ │ └── server.ts
|
|
355
|
+
│ └── [locale]
|
|
356
|
+
│ ├── layout.tsx
|
|
357
|
+
│ └── about.tsx
|
|
358
|
+
└── components
|
|
359
|
+
├── I18nProvider.tsx
|
|
360
|
+
├── ClientComponent.tsx
|
|
361
|
+
└── ServerComponent.tsx
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
</TabItem>
|
|
365
|
+
<TabItem label="next-intl" value="next-intl">
|
|
366
|
+
|
|
367
|
+
```bash
|
|
368
|
+
.
|
|
369
|
+
├── i18n.ts
|
|
370
|
+
├── locales
|
|
371
|
+
│ ├── en
|
|
372
|
+
│ │ ├── home.json
|
|
373
|
+
│ │ └── navbar.json
|
|
374
|
+
│ ├── fr
|
|
375
|
+
│ │ ├── home.json
|
|
376
|
+
│ │ └── navbar.json
|
|
377
|
+
│ └── es
|
|
378
|
+
│ ├── home.json
|
|
379
|
+
│ └── navbar.json
|
|
380
|
+
└── src
|
|
381
|
+
├── middleware.ts
|
|
382
|
+
├── app
|
|
383
|
+
│ ├── i18n
|
|
384
|
+
│ │ └── server.ts
|
|
385
|
+
│ └── [locale]
|
|
386
|
+
│ └── home.tsx
|
|
387
|
+
└── components
|
|
388
|
+
└── Navbar
|
|
389
|
+
└── index.tsx
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
</TabItem>
|
|
393
|
+
<TabItem label="intlayer" value="intlayer">
|
|
394
|
+
|
|
395
|
+
```bash
|
|
396
|
+
.
|
|
397
|
+
├── intlayer.config.ts
|
|
398
|
+
└── src
|
|
399
|
+
├── middleware.ts
|
|
400
|
+
├── app
|
|
401
|
+
│ └── [locale]
|
|
402
|
+
│ ├── layout.tsx
|
|
403
|
+
│ └── home
|
|
404
|
+
│ ├── index.tsx
|
|
405
|
+
│ └── index.content.ts
|
|
406
|
+
└── components
|
|
407
|
+
└── Navbar
|
|
408
|
+
├── index.tsx
|
|
409
|
+
└── index.content.ts
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
</TabItem>
|
|
413
|
+
</Tab>
|
|
414
|
+
|
|
415
|
+
#### المقارنة
|
|
416
|
+
|
|
417
|
+
- **next-intl / next-i18next**: كتالوجات مركزية (JSON؛ مساحات الأسماء/الرسائل). هيكل واضح، يتكامل جيدًا مع منصات الترجمة، لكنه قد يؤدي إلى المزيد من التعديلات عبر الملفات مع نمو التطبيقات.
|
|
418
|
+
- **Intlayer**: قواميس `.content.{ts|js|json}` لكل مكون متواجدة بجانب المكونات. يسهل إعادة استخدام المكونات والتفكير المحلي؛ يضيف ملفات ويعتمد على أدوات وقت البناء.
|
|
419
|
+
|
|
420
|
+
#### الإعداد وتحميل المحتوى
|
|
421
|
+
|
|
422
|
+
كما ذُكر سابقًا، يجب عليك تحسين كيفية استيراد كل ملف JSON إلى كودك.
|
|
423
|
+
كيفية تعامل المكتبة مع تحميل المحتوى أمر مهم.
|
|
424
|
+
|
|
425
|
+
<Tab defaultTab="next-intl" group='techno'>
|
|
426
|
+
<TabItem label="next-i18next" value="next-i18next">
|
|
427
|
+
|
|
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";
|
|
433
|
+
|
|
434
|
+
export const rtlLocales = ["ar", "he", "fa", "ur"] as const;
|
|
435
|
+
export const isRtl = (locale: string) =>
|
|
436
|
+
(rtlLocales as readonly string[]).includes(locale);
|
|
437
|
+
|
|
438
|
+
export function localizedPath(locale: string, path: string) {
|
|
439
|
+
return locale === defaultLocale ? path : "/" + locale + path;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const ORIGIN = "https://example.com";
|
|
443
|
+
export function abs(locale: string, path: string) {
|
|
444
|
+
return ORIGIN + localizedPath(locale, path);
|
|
445
|
+
}
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
```ts fileName="src/app/i18n/server.ts"
|
|
449
|
+
import { createInstance } from "i18next";
|
|
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
|
+
```
|
|
479
|
+
|
|
480
|
+
```tsx fileName="src/components/I18nProvider.tsx"
|
|
481
|
+
"use client";
|
|
482
|
+
|
|
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;
|
|
524
|
+
});
|
|
525
|
+
|
|
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");
|
|
580
|
+
|
|
581
|
+
return (
|
|
582
|
+
<I18nProvider locale={locale} namespaces={[...namespaces]}>
|
|
583
|
+
<main>
|
|
584
|
+
<h1>{tAbout("title")}</h1>
|
|
585
|
+
|
|
586
|
+
<ClientComponent />
|
|
587
|
+
<ServerComponent t={tAbout} locale={locale} count={0} />
|
|
588
|
+
</main>
|
|
589
|
+
</I18nProvider>
|
|
590
|
+
);
|
|
591
|
+
}
|
|
592
|
+
```
|
|
593
|
+
|
|
594
|
+
</TabItem>
|
|
595
|
+
<TabItem label="next-intl" value="next-intl">
|
|
596
|
+
|
|
597
|
+
```tsx fileName="src/i18n.ts"
|
|
598
|
+
import { getRequestConfig } from "next-intl/server";
|
|
599
|
+
import { notFound } from "next/navigation";
|
|
600
|
+
|
|
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
|
+
}
|
|
613
|
+
|
|
614
|
+
export default getRequestConfig(async ({ locale }) => {
|
|
615
|
+
if (!locales.includes(locale as any)) notFound();
|
|
616
|
+
|
|
617
|
+
return {
|
|
618
|
+
messages: await loadMessages(locale),
|
|
619
|
+
};
|
|
620
|
+
});
|
|
621
|
+
```
|
|
622
|
+
|
|
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
|
+
}
|
|
636
|
+
|
|
637
|
+
export default async function LocaleLayout({
|
|
638
|
+
children,
|
|
639
|
+
params,
|
|
640
|
+
}: {
|
|
641
|
+
children: ReactNode;
|
|
642
|
+
params: Promise<{ locale: string }>;
|
|
643
|
+
}) {
|
|
644
|
+
const { locale } = await params;
|
|
645
|
+
|
|
646
|
+
// Set the active request locale for this server render (RSC)
|
|
647
|
+
unstable_setRequestLocale(locale);
|
|
648
|
+
|
|
649
|
+
const dir = getLocaleDirection(locale);
|
|
650
|
+
|
|
651
|
+
return (
|
|
652
|
+
<html lang={locale} dir={dir}>
|
|
653
|
+
<body>{children}</body>
|
|
654
|
+
</html>
|
|
655
|
+
);
|
|
656
|
+
}
|
|
657
|
+
```
|
|
658
|
+
|
|
659
|
+
```tsx fileName="src/app/[locale]/about/page.tsx"
|
|
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";
|
|
665
|
+
|
|
666
|
+
export const dynamic = "force-static";
|
|
667
|
+
|
|
668
|
+
export default async function AboutPage({
|
|
669
|
+
params,
|
|
670
|
+
}: {
|
|
671
|
+
params: Promise<{ locale: string }>;
|
|
672
|
+
}) {
|
|
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);
|
|
685
|
+
|
|
686
|
+
return (
|
|
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>
|
|
698
|
+
);
|
|
699
|
+
}
|
|
700
|
+
```
|
|
701
|
+
|
|
702
|
+
</TabItem>
|
|
703
|
+
<TabItem label="intlayer" value="intlayer">
|
|
704
|
+
|
|
705
|
+
```tsx fileName="intlayer.config.ts"
|
|
706
|
+
import { type IntlayerConfig, Locales } from "intlayer";
|
|
707
|
+
|
|
708
|
+
const config: IntlayerConfig = {
|
|
709
|
+
internationalization: {
|
|
710
|
+
locales: [Locales.ENGLISH, Locales.FRENCH, Locales.SPANISH],
|
|
711
|
+
defaultLocale: Locales.ENGLISH,
|
|
712
|
+
},
|
|
713
|
+
};
|
|
714
|
+
|
|
715
|
+
export default config;
|
|
716
|
+
```
|
|
717
|
+
|
|
718
|
+
```tsx fileName="src/app/[locale]/layout.tsx"
|
|
719
|
+
import { getHTMLTextDir } from "intlayer";
|
|
720
|
+
import {
|
|
721
|
+
IntlayerClientProvider,
|
|
722
|
+
generateStaticParams,
|
|
723
|
+
type NextLayoutIntlayer,
|
|
724
|
+
} from "next-intlayer";
|
|
725
|
+
|
|
726
|
+
export const dynamic = "force-static";
|
|
727
|
+
|
|
728
|
+
const LocaleLayout: NextLayoutIntlayer = async ({ children, params }) => {
|
|
729
|
+
const { locale } = await params;
|
|
730
|
+
|
|
731
|
+
return (
|
|
732
|
+
<html lang={locale} dir={getHTMLTextDir(locale)}>
|
|
733
|
+
<body>
|
|
734
|
+
<IntlayerClientProvider locale={locale}>
|
|
735
|
+
{children}
|
|
736
|
+
</IntlayerClientProvider>
|
|
737
|
+
</body>
|
|
738
|
+
</html>
|
|
739
|
+
);
|
|
740
|
+
};
|
|
741
|
+
|
|
742
|
+
export default LandingLayout;
|
|
743
|
+
```
|
|
744
|
+
|
|
745
|
+
```tsx fileName="src/app/[locale]/about/page.tsx"
|
|
746
|
+
import { PageContent } from "@components/PageContent";
|
|
747
|
+
import type { NextPageIntlayer } from "next-intlayer";
|
|
748
|
+
import { IntlayerServerProvider, useIntlayer } from "next-intlayer/server";
|
|
749
|
+
import { ClientComponent, ServerComponent } from "@components";
|
|
750
|
+
|
|
751
|
+
const LandingPage: NextPageIntlayer = async ({ params }) => {
|
|
752
|
+
const { locale } = await params;
|
|
753
|
+
const { title } = useIntlayer("about", locale);
|
|
754
|
+
|
|
755
|
+
return (
|
|
756
|
+
<IntlayerServerProvider locale={locale}>
|
|
757
|
+
<main>
|
|
758
|
+
<h1>{title}</h1>
|
|
759
|
+
<ClientComponent />
|
|
760
|
+
<ServerComponent />
|
|
761
|
+
</main>
|
|
762
|
+
</IntlayerServerProvider>
|
|
763
|
+
);
|
|
764
|
+
};
|
|
765
|
+
|
|
766
|
+
export default LandingPage;
|
|
767
|
+
```
|
|
768
|
+
|
|
769
|
+
</TabItem>
|
|
770
|
+
</Tab>
|
|
771
|
+
|
|
772
|
+
#### مقارنة
|
|
773
|
+
|
|
774
|
+
جميع الثلاثة تدعم تحميل المحتوى حسب اللغة والمزودين.
|
|
775
|
+
|
|
776
|
+
- مع **next-intl/next-i18next**، عادةً ما تقوم بتحميل الرسائل/المساحات المختارة لكل مسار وتضع الموفرين حيثما دعت الحاجة.
|
|
777
|
+
|
|
778
|
+
- مع **Intlayer**، يتم إضافة تحليل وقت البناء لاستنتاج الاستخدام، مما يمكن أن يقلل من التوصيل اليدوي وقد يسمح بموفر جذر واحد.
|
|
779
|
+
|
|
780
|
+
اختر بين التحكم الصريح والأتمتة بناءً على تفضيل الفريق.
|
|
781
|
+
|
|
782
|
+
### الاستخدام في مكون العميل
|
|
783
|
+
|
|
784
|
+
لنأخذ مثالاً على مكون عميل يعرض عدادًا.
|
|
785
|
+
|
|
786
|
+
<Tab defaultTab="next-intl" group='techno'>
|
|
787
|
+
<TabItem label="next-i18next" value="next-i18next">
|
|
788
|
+
|
|
789
|
+
**الترجمات (one JSON per namespace under `src/locales/...`)**
|
|
790
|
+
|
|
791
|
+
```json fileName="src/locales/en/about.json"
|
|
792
|
+
{
|
|
793
|
+
"title": "About",
|
|
794
|
+
"description": "About page description",
|
|
795
|
+
"counter": {
|
|
796
|
+
"label": "Counter",
|
|
797
|
+
"increment": "Increment"
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
```
|
|
801
|
+
|
|
802
|
+
```json fileName="src/locales/fr/about.json"
|
|
803
|
+
{
|
|
804
|
+
"title": "À propos",
|
|
805
|
+
"description": "Description de la page À propos",
|
|
806
|
+
"counter": {
|
|
807
|
+
"label": "Compteur",
|
|
808
|
+
"increment": "Incrémenter"
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
```
|
|
812
|
+
|
|
813
|
+
**مكون العميل (loads only the required namespace)**
|
|
814
|
+
|
|
815
|
+
```tsx fileName="src/components/ClientComponent.tsx"
|
|
816
|
+
"use client";
|
|
817
|
+
|
|
818
|
+
import React, { useState } from "react";
|
|
819
|
+
import { useTranslation } from "react-i18next";
|
|
820
|
+
|
|
821
|
+
const ClientComponent = () => {
|
|
822
|
+
const { t, i18n } = useTranslation("about");
|
|
823
|
+
const [count, setCount] = useState(0);
|
|
824
|
+
|
|
825
|
+
const numberFormat = new Intl.NumberFormat(i18n.language);
|
|
826
|
+
|
|
827
|
+
return (
|
|
828
|
+
<div>
|
|
829
|
+
<p>{numberFormat.format(count)}</p>
|
|
830
|
+
<button
|
|
831
|
+
aria-label={t("counter.label")}
|
|
832
|
+
onClick={() => setCount((c) => c + 1)}
|
|
833
|
+
>
|
|
834
|
+
{t("counter.increment")}
|
|
835
|
+
</button>
|
|
836
|
+
</div>
|
|
837
|
+
);
|
|
838
|
+
};
|
|
839
|
+
|
|
840
|
+
export default ClientComponent;
|
|
841
|
+
```
|
|
842
|
+
|
|
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`.
|
|
845
|
+
|
|
846
|
+
</TabItem>
|
|
847
|
+
<TabItem label="next-intl" value="next-intl">
|
|
848
|
+
|
|
849
|
+
**الترجمات (shape reused; load them into next-intl messages as you prefer)**
|
|
850
|
+
|
|
851
|
+
```json fileName="locales/en/about.json"
|
|
852
|
+
{
|
|
853
|
+
"counter": {
|
|
854
|
+
"label": "Counter",
|
|
855
|
+
"increment": "Increment"
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
```
|
|
859
|
+
|
|
860
|
+
```json fileName="locales/fr/about.json"
|
|
861
|
+
{
|
|
862
|
+
"counter": {
|
|
863
|
+
"label": "Compteur",
|
|
864
|
+
"increment": "Incrémenter"
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
```
|
|
868
|
+
|
|
869
|
+
**مكون العميل**
|
|
870
|
+
|
|
871
|
+
```tsx fileName="src/components/ClientComponentExample.tsx"
|
|
872
|
+
"use client";
|
|
873
|
+
|
|
874
|
+
import React, { useState } from "react";
|
|
875
|
+
import { useTranslations, useFormatter } from "next-intl";
|
|
876
|
+
|
|
877
|
+
const ClientComponentExample = () => {
|
|
878
|
+
// نطاق مباشر للكائن المتداخل
|
|
879
|
+
const t = useTranslations("about.counter");
|
|
880
|
+
const format = useFormatter();
|
|
881
|
+
const [count, setCount] = useState(0);
|
|
882
|
+
|
|
883
|
+
return (
|
|
884
|
+
<div>
|
|
885
|
+
<p>{format.number(count)}</p>
|
|
886
|
+
<button
|
|
887
|
+
aria-label={t("label")}
|
|
888
|
+
onClick={() => setCount((count) => count + 1)}
|
|
889
|
+
>
|
|
890
|
+
{t("increment")}
|
|
891
|
+
</button>
|
|
892
|
+
</div>
|
|
893
|
+
);
|
|
894
|
+
};
|
|
895
|
+
```
|
|
896
|
+
|
|
897
|
+
> Don't forget to add "about" message on the page client message
|
|
898
|
+
|
|
899
|
+
</TabItem>
|
|
900
|
+
<TabItem label="intlayer" value="intlayer">
|
|
901
|
+
|
|
902
|
+
**المحتوى**
|
|
903
|
+
|
|
904
|
+
```ts fileName="src/components/ClientComponentExample/index.content.ts"
|
|
905
|
+
import { t, type Dictionary } from "intlayer";
|
|
906
|
+
|
|
907
|
+
const counterContent = {
|
|
908
|
+
key: "counter",
|
|
909
|
+
content: {
|
|
910
|
+
label: t({ en: "Counter", fr: "Compteur" }),
|
|
911
|
+
increment: t({ en: "Increment", fr: "Incrémenter" }),
|
|
912
|
+
},
|
|
913
|
+
} satisfies Dictionary;
|
|
914
|
+
|
|
915
|
+
export default counterContent;
|
|
916
|
+
```
|
|
917
|
+
|
|
918
|
+
**مكون العميل**
|
|
919
|
+
|
|
920
|
+
```tsx fileName="src/components/ClientComponentExample/index.tsx"
|
|
921
|
+
"use client";
|
|
922
|
+
|
|
923
|
+
import React, { useState } from "react";
|
|
924
|
+
import { useNumber, useIntlayer } from "next-intlayer";
|
|
925
|
+
|
|
926
|
+
const ClientComponentExample = () => {
|
|
927
|
+
const [count, setCount] = useState(0);
|
|
928
|
+
const { label, increment } = useIntlayer("counter"); // returns strings
|
|
929
|
+
const { number } = useNumber();
|
|
930
|
+
|
|
931
|
+
return (
|
|
932
|
+
<div>
|
|
933
|
+
<p>{number(count)}</p>
|
|
934
|
+
<button aria-label={label} onClick={() => setCount((count) => count + 1)}>
|
|
935
|
+
{increment}
|
|
936
|
+
</button>
|
|
937
|
+
</div>
|
|
938
|
+
);
|
|
939
|
+
};
|
|
940
|
+
```
|
|
941
|
+
|
|
942
|
+
</TabItem>
|
|
943
|
+
</Tab>
|
|
944
|
+
|
|
945
|
+
#### المقارنة
|
|
946
|
+
|
|
947
|
+
- **تنسيق الأرقام**
|
|
948
|
+
- **next-i18next**: لا يوجد `useNumber`؛ يستخدم `Intl.NumberFormat` (أو i18next-icu).
|
|
949
|
+
- **next-intl**: `useFormatter().number(value)`.
|
|
950
|
+
- **Intlayer**: `useNumber()` مدمج.
|
|
951
|
+
|
|
952
|
+
- **المفاتيح**
|
|
953
|
+
- احتفظ ببنية متداخلة (`about.counter.label`) وحدد نطاق الخطاف الخاص بك وفقًا لذلك (`useTranslation("about")` + `t("counter.label")` أو `useTranslations("about.counter")` + `t("label")`).
|
|
954
|
+
|
|
955
|
+
- **مواقع الملفات**
|
|
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.
|
|
959
|
+
|
|
135
960
|
---
|
|
136
961
|
|
|
137
|
-
|
|
962
|
+
### الاستخدام في مكون الخادم
|
|
963
|
+
|
|
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.
|
|
965
|
+
|
|
966
|
+
<Tab defaultTab="next-intl" group='techno'>
|
|
967
|
+
<TabItem label="next-i18next" value="next-i18next">
|
|
968
|
+
|
|
969
|
+
```tsx fileName="src/components/ServerComponent.tsx"
|
|
970
|
+
type ServerComponentProps = {
|
|
971
|
+
t: (key: string) => string;
|
|
972
|
+
locale: string;
|
|
973
|
+
count: number;
|
|
974
|
+
};
|
|
975
|
+
|
|
976
|
+
const ServerComponent = ({ t, locale, count }: ServerComponentProps) => {
|
|
977
|
+
const formatted = new Intl.NumberFormat(locale).format(count);
|
|
978
|
+
|
|
979
|
+
return (
|
|
980
|
+
<div>
|
|
981
|
+
<p>{formatted}</p>
|
|
982
|
+
<button aria-label={t("counter.label")}>{t("counter.increment")}</button>
|
|
983
|
+
</div>
|
|
984
|
+
);
|
|
985
|
+
};
|
|
986
|
+
|
|
987
|
+
export default ServerComponent;
|
|
988
|
+
```
|
|
989
|
+
|
|
990
|
+
</TabItem>
|
|
991
|
+
<TabItem label="next-intl" value="next-intl">
|
|
992
|
+
|
|
993
|
+
```tsx fileName="src/components/ServerComponent.tsx"
|
|
994
|
+
type ServerComponentProps = {
|
|
995
|
+
formattedCount: string;
|
|
996
|
+
label: string;
|
|
997
|
+
increment: string;
|
|
998
|
+
};
|
|
999
|
+
|
|
1000
|
+
const ServerComponent = ({
|
|
1001
|
+
formattedCount,
|
|
1002
|
+
label,
|
|
1003
|
+
increment,
|
|
1004
|
+
}: ServerComponentProps) => {
|
|
1005
|
+
return (
|
|
1006
|
+
<div>
|
|
1007
|
+
<p>{formattedCount}</p>
|
|
1008
|
+
<button aria-label={label}>{increment}</button>
|
|
1009
|
+
</div>
|
|
1010
|
+
);
|
|
1011
|
+
};
|
|
1012
|
+
|
|
1013
|
+
export default ServerComponent;
|
|
1014
|
+
```
|
|
1015
|
+
|
|
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:
|
|
1019
|
+
>
|
|
1020
|
+
> - `import { getTranslations, getFormatter } from "next-intl/server";`
|
|
1021
|
+
> - `const t = await getTranslations("about.counter");`
|
|
1022
|
+
> - `const formatter = await getFormatter().then((formatter) => formatter.number());`
|
|
1023
|
+
|
|
1024
|
+
</TabItem>
|
|
1025
|
+
<TabItem label="intlayer" value="intlayer">
|
|
1026
|
+
|
|
1027
|
+
```tsx fileName="src/components/ServerComponent.tsx"
|
|
1028
|
+
import { useIntlayer, useNumber } from "next-intlayer/server";
|
|
1029
|
+
|
|
1030
|
+
type ServerComponentProps = {
|
|
1031
|
+
count: number;
|
|
1032
|
+
};
|
|
1033
|
+
|
|
1034
|
+
const ServerComponent = ({ count }: ServerComponentProps) => {
|
|
1035
|
+
const { label, increment } = useIntlayer("counter");
|
|
1036
|
+
const { number } = useNumber();
|
|
1037
|
+
|
|
1038
|
+
return (
|
|
1039
|
+
<div>
|
|
1040
|
+
<p>{number(count)}</p>
|
|
1041
|
+
<button aria-label={label}>{increment}</button>
|
|
1042
|
+
</div>
|
|
1043
|
+
);
|
|
1044
|
+
};
|
|
1045
|
+
```
|
|
1046
|
+
|
|
1047
|
+
</TabItem>
|
|
1048
|
+
</Tab>
|
|
1049
|
+
|
|
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`).
|
|
1051
|
+
|
|
1052
|
+
### البيانات الوصفية / خريطة الموقع / روبوتات البحث
|
|
1053
|
+
|
|
1054
|
+
ترجمة المحتوى أمر رائع. لكن الناس عادةً ما ينسون أن الهدف الرئيسي من التدويل هو جعل موقعك الإلكتروني أكثر ظهورًا للعالم. التدويل هو رافعة مذهلة لتحسين ظهور موقعك الإلكتروني.
|
|
1055
|
+
|
|
1056
|
+
Here's a list of good practices regarding multilingual SEO.
|
|
1057
|
+
|
|
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
|
|
1061
|
+
>
|
|
1062
|
+
- do not forget to exclude prefixed pages from the robots.txt (e.g. `/dashboard`, and `/fr/dashboard`, `/es/dashboard`)
|
|
1063
|
+
>
|
|
1064
|
+
- use custom Link component to redirect to the most localized page (e.g. in french `<a href="/fr/about">A propos</a>` )
|
|
1065
|
+
>
|
|
1066
|
+
|
|
1067
|
+
Developers often forget to properly reference their pages across locales.
|
|
1068
|
+
|
|
1069
|
+
<Tab defaultTab="next-intl" group='techno'>
|
|
1070
|
+
|
|
1071
|
+
<TabItem label="next-i18next" value="next-i18next">
|
|
1072
|
+
|
|
1073
|
+
```ts fileName="i18n.config.ts"
|
|
1074
|
+
export const locales = ["en", "fr"] as const;
|
|
1075
|
+
export type Locale = (typeof locales)[number];
|
|
1076
|
+
export const defaultLocale: Locale = "en";
|
|
1077
|
+
|
|
1078
|
+
export function localizedPath(locale: string, path: string) {
|
|
1079
|
+
return locale === defaultLocale ? path : "/" + locale + path;
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
const ORIGIN = "https://example.com";
|
|
1083
|
+
export function abs(locale: string, path: string) {
|
|
1084
|
+
return ORIGIN + localizedPath(locale, path);
|
|
1085
|
+
}
|
|
1086
|
+
```
|
|
138
1087
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
1088
|
+
```tsx fileName="src/app/[locale]/about/layout.tsx"
|
|
1089
|
+
import type { Metadata } from "next";
|
|
1090
|
+
import { locales, defaultLocale, localizedPath } from "@/i18n.config";
|
|
1091
|
+
|
|
1092
|
+
export async function generateMetadata({
|
|
1093
|
+
params,
|
|
1094
|
+
}: {
|
|
1095
|
+
params: { locale: string };
|
|
1096
|
+
}): Promise<Metadata> {
|
|
1097
|
+
const { locale } = params;
|
|
1098
|
+
|
|
1099
|
+
// Import the correct JSON bundle from src/locales
|
|
1100
|
+
const messages = (await import("@/locales/" + locale + "/about.json"))
|
|
1101
|
+
.default;
|
|
1102
|
+
|
|
1103
|
+
const languages = Object.fromEntries(
|
|
1104
|
+
locales.map((locale) => [locale, localizedPath(locale, "/about")])
|
|
1105
|
+
);
|
|
1106
|
+
|
|
1107
|
+
return {
|
|
1108
|
+
title: messages.title,
|
|
1109
|
+
description: messages.description,
|
|
1110
|
+
alternates: {
|
|
1111
|
+
canonical: localizedPath(locale, "/about"),
|
|
1112
|
+
languages: { ...languages, "x-default": "/about" },
|
|
1113
|
+
},
|
|
1114
|
+
};
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
export default async function AboutPage() {
|
|
1118
|
+
return <h1>About</h1>;
|
|
1119
|
+
}
|
|
1120
|
+
```
|
|
1121
|
+
|
|
1122
|
+
```ts fileName="src/app/sitemap.ts"
|
|
1123
|
+
import type { MetadataRoute } from "next";
|
|
1124
|
+
import { locales, defaultLocale, abs } from "@/i18n.config";
|
|
1125
|
+
|
|
1126
|
+
export default function sitemap(): MetadataRoute.Sitemap {
|
|
1127
|
+
const languages = Object.fromEntries(
|
|
1128
|
+
locales.map((locale) => [locale, abs(locale, "/about")])
|
|
1129
|
+
);
|
|
1130
|
+
return [
|
|
1131
|
+
{
|
|
1132
|
+
url: abs(defaultLocale, "/about"),
|
|
1133
|
+
lastModified: new Date(),
|
|
1134
|
+
changeFrequency: "monthly",
|
|
1135
|
+
priority: 0.7,
|
|
1136
|
+
alternates: { languages },
|
|
1137
|
+
},
|
|
1138
|
+
];
|
|
1139
|
+
}
|
|
1140
|
+
```
|
|
1141
|
+
|
|
1142
|
+
```ts fileName="src/app/robots.ts"
|
|
1143
|
+
import type { MetadataRoute } from "next";
|
|
1144
|
+
import { locales, defaultLocale, localizedPath } from "@/i18n.config";
|
|
1145
|
+
|
|
1146
|
+
const ORIGIN = "https://example.com";
|
|
1147
|
+
|
|
1148
|
+
const expandAllLocales = (path: string) => [
|
|
1149
|
+
localizedPath(defaultLocale, path),
|
|
1150
|
+
...locales
|
|
1151
|
+
.filter((locale) => locale !== defaultLocale)
|
|
1152
|
+
.map((locale) => localizedPath(locale, path)),
|
|
1153
|
+
];
|
|
1154
|
+
|
|
1155
|
+
export default function robots(): MetadataRoute.Robots {
|
|
1156
|
+
const disallow = [
|
|
1157
|
+
...expandAllLocales("/dashboard"),
|
|
1158
|
+
...expandAllLocales("/admin"),
|
|
1159
|
+
];
|
|
1160
|
+
|
|
1161
|
+
return {
|
|
1162
|
+
rules: { userAgent: "*", allow: ["/"], disallow },
|
|
1163
|
+
host: ORIGIN,
|
|
1164
|
+
sitemap: ORIGIN + "/sitemap.xml",
|
|
1165
|
+
};
|
|
1166
|
+
}
|
|
1167
|
+
```
|
|
1168
|
+
|
|
1169
|
+
</TabItem>
|
|
1170
|
+
<TabItem label="next-intl" value="next-intl">
|
|
1171
|
+
|
|
1172
|
+
```tsx fileName="src/app/[locale]/about/layout.tsx"
|
|
1173
|
+
import type { Metadata } from "next";
|
|
1174
|
+
import { locales, defaultLocale } from "@/i18n";
|
|
1175
|
+
import { getTranslations } from "next-intl/server";
|
|
1176
|
+
|
|
1177
|
+
function localizedPath(locale: string, path: string) {
|
|
1178
|
+
return locale === defaultLocale ? path : "/" + locale + path;
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
export async function generateMetadata({
|
|
1182
|
+
params,
|
|
1183
|
+
}: {
|
|
1184
|
+
params: { locale: string };
|
|
1185
|
+
}): Promise<Metadata> {
|
|
1186
|
+
const { locale } = params;
|
|
1187
|
+
const t = await getTranslations({ locale, namespace: "about" });
|
|
1188
|
+
|
|
1189
|
+
const url = "/about";
|
|
1190
|
+
const languages = Object.fromEntries(
|
|
1191
|
+
locales.map((locale) => [locale, localizedPath(locale, url)])
|
|
1192
|
+
);
|
|
1193
|
+
|
|
1194
|
+
return {
|
|
1195
|
+
title: t("title"),
|
|
1196
|
+
description: t("description"),
|
|
1197
|
+
alternates: {
|
|
1198
|
+
canonical: localizedPath(locale, url),
|
|
1199
|
+
languages: { ...languages, "x-default": url },
|
|
1200
|
+
},
|
|
1201
|
+
};
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
// ... Rest of the page code
|
|
1205
|
+
```
|
|
1206
|
+
|
|
1207
|
+
```tsx fileName="src/app/sitemap.ts"
|
|
1208
|
+
import type { MetadataRoute } from "next";
|
|
1209
|
+
import { locales, defaultLocale } from "@/i18n";
|
|
1210
|
+
|
|
1211
|
+
const origin = "https://example.com";
|
|
1212
|
+
|
|
1213
|
+
const formatterLocalizedPath = (locale: string, path: string) =>
|
|
1214
|
+
locale === defaultLocale ? origin + path : origin + "/" + locale + path;
|
|
1215
|
+
|
|
1216
|
+
export default function sitemap(): MetadataRoute.Sitemap {
|
|
1217
|
+
const aboutLanguages = Object.fromEntries(
|
|
1218
|
+
locales.map((l) => [l, formatterLocalizedPath(l, "/about")])
|
|
1219
|
+
);
|
|
1220
|
+
|
|
1221
|
+
return [
|
|
1222
|
+
{
|
|
1223
|
+
url: formatterLocalizedPath(defaultLocale, "/about"),
|
|
1224
|
+
lastModified: new Date(),
|
|
1225
|
+
changeFrequency: "monthly",
|
|
1226
|
+
priority: 0.7,
|
|
1227
|
+
alternates: { languages: aboutLanguages },
|
|
1228
|
+
},
|
|
1229
|
+
];
|
|
1230
|
+
}
|
|
1231
|
+
```
|
|
1232
|
+
|
|
1233
|
+
```tsx fileName="src/app/robots.ts"
|
|
1234
|
+
import type { MetadataRoute } from "next";
|
|
1235
|
+
import { locales, defaultLocale } from "@/i18n";
|
|
1236
|
+
|
|
1237
|
+
const origin = "https://example.com";
|
|
1238
|
+
const withAllLocales = (path: string) => [
|
|
1239
|
+
path,
|
|
1240
|
+
...locales
|
|
1241
|
+
.filter((locale) => locale !== defaultLocale)
|
|
1242
|
+
.map((locale) => "/" + locale + path),
|
|
1243
|
+
];
|
|
1244
|
+
|
|
1245
|
+
export default function robots(): MetadataRoute.Robots {
|
|
1246
|
+
const disallow = [
|
|
1247
|
+
...withAllLocales("/dashboard"),
|
|
1248
|
+
...withAllLocales("/admin"),
|
|
1249
|
+
];
|
|
1250
|
+
|
|
1251
|
+
return {
|
|
1252
|
+
rules: { userAgent: "*", allow: ["/"], disallow },
|
|
1253
|
+
host: origin,
|
|
1254
|
+
sitemap: origin + "/sitemap.xml",
|
|
1255
|
+
};
|
|
1256
|
+
}
|
|
1257
|
+
```
|
|
1258
|
+
|
|
1259
|
+
</TabItem>
|
|
1260
|
+
<TabItem label="intlayer" value="intlayer">
|
|
1261
|
+
|
|
1262
|
+
```typescript fileName="src/app/[locale]/about/layout.tsx"
|
|
1263
|
+
import { getIntlayer, getMultilingualUrls } from "intlayer";
|
|
1264
|
+
import type { Metadata } from "next";
|
|
1265
|
+
import type { LocalPromiseParams } from "next-intlayer";
|
|
1266
|
+
|
|
1267
|
+
export const generateMetadata = async ({
|
|
1268
|
+
params,
|
|
1269
|
+
}: LocalPromiseParams): Promise<Metadata> => {
|
|
1270
|
+
const { locale } = await params;
|
|
1271
|
+
|
|
1272
|
+
const metadata = getIntlayer("page-metadata", locale);
|
|
1273
|
+
|
|
1274
|
+
const multilingualUrls = getMultilingualUrls("/about");
|
|
1275
|
+
|
|
1276
|
+
return {
|
|
1277
|
+
...metadata,
|
|
1278
|
+
alternates: {
|
|
1279
|
+
canonical: multilingualUrls[locale as keyof typeof multilingualUrls],
|
|
1280
|
+
languages: { ...multilingualUrls, "x-default": "/about" },
|
|
1281
|
+
},
|
|
1282
|
+
};
|
|
1283
|
+
};
|
|
1284
|
+
|
|
1285
|
+
// ... Rest of the page code
|
|
1286
|
+
```
|
|
1287
|
+
|
|
1288
|
+
```tsx fileName="src/app/sitemap.ts"
|
|
1289
|
+
import { getMultilingualUrls } from "intlayer";
|
|
1290
|
+
import type { MetadataRoute } from "next";
|
|
1291
|
+
|
|
1292
|
+
const sitemap = (): MetadataRoute.Sitemap => [
|
|
1293
|
+
{
|
|
1294
|
+
url: "https://example.com/about",
|
|
1295
|
+
alternates: {
|
|
1296
|
+
languages: { ...getMultilingualUrls("https://example.com/about") },
|
|
1297
|
+
},
|
|
1298
|
+
},
|
|
1299
|
+
];
|
|
1300
|
+
```
|
|
1301
|
+
|
|
1302
|
+
```tsx fileName="src/app/robots.ts"
|
|
1303
|
+
import { getMultilingualUrls } from "intlayer";
|
|
1304
|
+
import type { MetadataRoute } from "next";
|
|
1305
|
+
|
|
1306
|
+
const getAllMultilingualUrls = (urls: string[]) =>
|
|
1307
|
+
urls.flatMap((url) => Object.values(getMultilingualUrls(url)) as string[]);
|
|
1308
|
+
|
|
1309
|
+
const robots = (): MetadataRoute.Robots => ({
|
|
1310
|
+
rules: {
|
|
1311
|
+
userAgent: "*",
|
|
1312
|
+
allow: ["/"],
|
|
1313
|
+
disallow: getAllMultilingualUrls(["/dashboard"]),
|
|
1314
|
+
},
|
|
1315
|
+
host: "https://example.com",
|
|
1316
|
+
sitemap: "https://example.com/sitemap.xml",
|
|
1317
|
+
});
|
|
1318
|
+
|
|
1319
|
+
export default robots;
|
|
1320
|
+
```
|
|
1321
|
+
|
|
1322
|
+
</TabItem>
|
|
1323
|
+
</Tab>
|
|
1324
|
+
|
|
1325
|
+
> Intlayer provides a `getMultilingualUrls` function to generate multilingual URLs for your sitemap.
|
|
142
1326
|
|
|
143
1327
|
---
|
|
144
1328
|
|
|
145
|
-
|
|
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
|
+
|
|
1402
|
+
---
|
|
1403
|
+
|
|
1404
|
+
## والفائز هو…
|
|
1405
|
+
|
|
1406
|
+
ليس الأمر بسيطًا. كل خيار له مزاياه وعيوبه. إليك كيف أراه:
|
|
1407
|
+
|
|
1408
|
+
<Columns>
|
|
1409
|
+
<Column>
|
|
1410
|
+
|
|
1411
|
+
**next-intl**
|
|
1412
|
+
|
|
1413
|
+
- أبسط، خفيف الوزن، مع قرارات أقل مفروضة عليك. إذا كنت تريد حلاً **بسيطًا**، وتشعر بالراحة مع الكتالوجات المركزية، وكان تطبيقك **صغير إلى متوسط الحجم**.
|
|
1414
|
+
|
|
1415
|
+
</Column>
|
|
1416
|
+
<Column>
|
|
1417
|
+
|
|
1418
|
+
**next-i18next**
|
|
1419
|
+
|
|
1420
|
+
- ناضج، مليء بالميزات، يحتوي على العديد من الإضافات المجتمعية، لكن تكلفة الإعداد أعلى. إذا كنت تحتاج إلى **نظام إضافات i18next** (مثل قواعد ICU المتقدمة عبر الإضافات) وكان فريقك يعرف i18next بالفعل، مع قبول **المزيد من التكوين** من أجل المرونة.
|
|
1421
|
+
|
|
1422
|
+
</Column>
|
|
1423
|
+
<Column>
|
|
1424
|
+
|
|
1425
|
+
**Intlayer**
|
|
1426
|
+
|
|
1427
|
+
- مبني لـ Next.js الحديث، مع محتوى معياري، أمان نوعي، أدوات، وتقليل الكود المكرر. إذا كنت تقدر **المحتوى المخصص للمكونات**، **TypeScript الصارم**، **ضمانات وقت البناء**، **tree-shaking**، وأدوات التوجيه/SEO/المحرر المدمجة - خاصة لـ **Next.js App Router**، أنظمة التصميم و**قواعد الشيفرة الكبيرة والمعيارية**.
|
|
1428
|
+
|
|
1429
|
+
</Column>
|
|
1430
|
+
</Columns>
|
|
1431
|
+
|
|
1432
|
+
إذا كنت تفضل إعدادًا بسيطًا وتقبل بعض التوصيلات اليدوية، فإن next-intl خيار جيد. إذا كنت تحتاج كل الميزات ولا تمانع التعقيد، فإن next-i18next يعمل بشكل جيد. ولكن إذا كنت تريد حلاً حديثًا وقابلًا للتوسع وذو محتوى معياري مع أدوات مدمجة، فإن Intlayer تهدف إلى تقديم ذلك لك مباشرةً من الصندوق.
|
|
1433
|
+
|
|
1434
|
+
> **بديل لفرق المؤسسات**: إذا كنت بحاجة إلى حل مثبت يعمل بشكل مثالي مع منصات التوطين المعروفة مثل **Crowdin**، **Phrase**، أو أنظمة إدارة الترجمة الاحترافية الأخرى، فكر في استخدام **next-intl** أو **next-i18next** لنظامهم البيئي الناضج والتكاملات المثبتة.
|
|
1435
|
+
|
|
1436
|
+
> **خارطة الطريق المستقبلية**: تخطط Intlayer أيضًا لتطوير إضافات تعمل فوق حلول **i18next** و **next-intl**. هذا سيمنحك مزايا Intlayer في الأتمتة، والبنية النحوية، وإدارة المحتوى مع الحفاظ على الأمان والاستقرار الذي توفره هذه الحلول المعروفة في كود التطبيق الخاص بك.
|
|
1437
|
+
|
|
1438
|
+
## نجوم GitHub
|
|
1439
|
+
|
|
1440
|
+
نجوم GitHub هي مؤشر قوي على شعبية المشروع، وثقة المجتمع، وأهميته على المدى الطويل. وعلى الرغم من أنها ليست مقياسًا مباشرًا للجودة التقنية، إلا أنها تعكس عدد المطورين الذين يجدون المشروع مفيدًا، ويتابعون تقدمه، ومن المحتمل أن يتبنوه. لتقدير قيمة المشروع، تساعد النجوم في مقارنة الجذب بين البدائل وتوفر رؤى حول نمو النظام البيئي.
|
|
146
1441
|
|
|
147
|
-
|
|
148
|
-
- **احتفظ بالكتالوجات القديمة بالتوازي**: استخدم جسرًا أثناء الترحيل؛ تجنب التغيير الجذري المفاجئ.
|
|
149
|
-
- **فعّل الفحوصات الصارمة**: دع اكتشاف وقت البناء يكشف الفجوات مبكرًا.
|
|
150
|
-
- **اعتمد الوسائط الوسيطة والمساعدين**: قم بتوحيد اكتشاف اللغة وعلامات SEO على مستوى الموقع.
|
|
151
|
-
- **قِس حجم الحزم**: توقع **تقليل حجم الحزمة** مع حذف المحتوى غير المستخدم.
|
|
1442
|
+
[](https://www.star-history.com/#i18next/next-i18next&amannn/next-intl&aymericzip/intlayer)
|
|
152
1443
|
|
|
153
1444
|
---
|
|
154
1445
|
|
|
155
1446
|
## الخلاصة
|
|
156
1447
|
|
|
157
|
-
|
|
1448
|
+
تنجح المكتبات الثلاث جميعها في التوطين الأساسي. الفرق هو **كمية العمل التي يجب عليك القيام بها** لتحقيق إعداد قوي وقابل للتوسع في **Next.js الحديثة**:
|
|
158
1449
|
|
|
159
|
-
- مع **Intlayer**،
|
|
160
|
-
- إذا كانت فرقك تقدر **قابلية الصيانة والسرعة** في تطبيق متعدد اللغات يعتمد على المكونات، فإن Intlayer تقدم
|
|
1450
|
+
- مع **Intlayer**، يكون **المحتوى المعياري**، و**TypeScript الصارم**، و**السلامة أثناء وقت البناء**، و**حزم شجرة المهملة**، و**موجه التطبيقات من الدرجة الأولى + أدوات تحسين محركات البحث** هي **الإعدادات الافتراضية**، وليست مهامًا شاقة.
|
|
1451
|
+
- إذا كانت فرقك تقدر **قابلية الصيانة والسرعة** في تطبيق متعدد اللغات يعتمد على المكونات، فإن Intlayer تقدم التجربة **الأكمل** اليوم.
|
|
161
1452
|
|
|
162
|
-
راجع
|
|
1453
|
+
راجع [وثيقة "لماذا Intlayer؟"](https://intlayer.org/doc/why) لمزيد من التفاصيل.
|