---
createdAt: 2025-09-10
updatedAt: 2025-09-10
title: التعريب لكل مكوّن مقابل التعريب المركزي: نهج جديد مع Intlayer
description: غوص عميق في استراتيجيات التعريب في React، مع مقارنة النهج المركزي، لكل-مفتاح، ولكل-مكوّن، وتقديم Intlayer.
keywords:
- i18n
- React
- التدويل
- Intlayer
- تحسين
- حجم الحزمة
slugs:
- blog
- per-component-vs-centralized-i18n
---
# التعريب لكل مكوّن مقابل التعريب المركزي
نهج التعريب لكل مكوّن ليس مفهومًا جديدًا. على سبيل المثال، في بيئة Vue، تدعم مكتبة `vue-i18n` [i18n SFC (Single File Component)](https://vue-i18n.intlify.dev/guide/advanced/sfc.html). كما يقدم Nuxt [ترجمات لكل مكوّن](https://i18n.nuxtjs.org/docs/guide/per-component-translations)، ويستخدم Angular نمطًا مشابهًا من خلال [Feature Modules](https://v17.angular.io/guide/feature-modules).
حتى في تطبيق Flutter، غالبًا ما نجد هذا النمط:
```bash
lib/
└── features/
└── login/
├── login_screen.dart
└── login_screen.i18n.dart # <- الترجمة موجودة هنا
```
```dart fileName='lib/features/login/login_screen.i18n.dart'
import 'package:i18n_extension/i18n_extension.dart';
extension Localization on String {
static var _t = Translations.byText("en") +
{
"Hello": {
"en": "Hello",
"fr": "Bonjour",
},
};
String get i18n => localize(this, _t);
}
```
ومع ذلك، في عالم React، نرى في الأساس مقاربات مختلفة، سأقوم بتجميعها في ثلاث فئات:
<Columns>
<Column>
**النهج المركزي** (i18next, next-intl, react-intl, lingui)
- (بدون مساحات أسماء) يعتبر مصدرًا واحدًا لاسترجاع المحتوى. بشكل افتراضي، تقوم بتحميل المحتوى من كل الصفحات عندما يتم تحميل تطبيقك.
</Column>
<Column>
**النهج التفصيلي** (intlayer, inlang)
- تفصيل استرجاع المحتوى حسب المفتاح، أو حسب المكوّن.
</Column>
</Columns>
> في هذه المدونة، لن أركز على الحلول المعتمدة على الـ compiler (compiler-based)، والتي قمت بتغطيتها سابقًا هنا: [Compiler vs Declarative i18n](https://github.com/aymericzip/intlayer/blob/main/docs/blog/ar/compiler_vs_declarative_i18n.md).
> لاحظ أن i18n المعتمدة على الـ compiler (مثل Lingui) تقوم ببساطة بأتمتة استخراج وتحميل المحتوى. تحت الغطاء، غالبًا ما تشترك في نفس القيود التي تواجه النهج الأخرى.
> لاحظ أنه كلما زادت دقة تفصيل طريقة استرجاع المحتوى، زادت المخاطرة بإدخال حالة (state) ومنطق إضافي داخل مكوناتك.
النهج التفصيلي أكثر مرونة من النهج المركزي، لكنه غالبًا ما يكون مقايضة. حتى وإن كانت تلك المكتبات تروّج لخاصية "tree shaking"، ففي الممارسة العملية، غالبًا ما ستجد نفسك تقوم بتحميل الصفحة بكل لغة.
بشكل عام، يمكن تبسيط القرار كالتالي:
- إذا كان تطبيقك يحتوي على صفحات أكثر من عدد اللغات، فعليك تفضيل النهج التفصيلي.
- إذا كان عدد اللغات أكبر من عدد الصفحات، فعليك التوجه نحو النهج المركزي.
بالطبع، مؤلفو المكتبات على دراية بهذه القيود ويقدّمون حلولاً بديلة. من بينها: التقسيم إلى namespaces، التحميل الديناميكي لملفات JSON (`await import()`)، أو إزالة المحتوى أثناء عملية البناء.
في نفس الوقت، يجب أن تعلم أنه عندما تقوم بتحميل المحتوى بشكل ديناميكي، فإنك تُولد طلبات إضافية إلى الخادم. كل `useState` إضافي أو hook يعني طلب خادم إضافي.
> لإصلاح هذه النقطة، تقترح Intlayer تجميع تعريفات المحتوى المتعددة تحت مفتاح واحد، ثم تقوم Intlayer بدمج ذلك المحتوى.
ولكن من بين كل هذه الحلول، يتضح أن النهج الأكثر شعبية هو النهج المركزي.
### فلماذا يُعد النهج المركزي شائعًا إلى هذا الحد؟
- أولاً، كانت i18next أول حل يصبح مستخدمًا على نطاق واسع، واتّبع فلسفة مستوحاة من معماريات PHP و Java (MVC)، التي تعتمد على فصل صارم للمسؤوليات (الحفاظ على فصل المحتوى عن الكود). وصلت في عام 2011، مما وضع معاييره حتى قبل التحول الكبير نحو Component-Based Architectures (مثل React).
- ثم، بمجرد اعتماد مكتبة على نطاق واسع، يصبح من الصعب نقل النظام البيئي إلى أنماط أخرى.
- يجعل استخدام النهج المركزي أيضًا الأمور أسهل في أنظمة إدارة الترجمة مثل Crowdin و Phrase و Localized.
- المنطق وراء نهج per-component أكثر تعقيدًا من النهج المركزي ويستغرق وقتًا إضافيًا للتطوير، خصوصًا عندما تضطر لحل مشكلات مثل تحديد مكان المحتوى.
### حسنًا، لكن لماذا لا نلتزم فقط بالنهج المركزي؟
دعني أشرح لماذا قد يكون ذلك مشكلة لتطبيقك:
- **البيانات غير المستخدمة:**
عندما يتم تحميل صفحة، غالبًا ما تقوم بتحميل المحتوى الخاص بكل الصفحات الأخرى. (في تطبيق من 10 صفحات، هذا يعني تحميل 90% من المحتوى غير المستخدم). هل تقوم بتحميل نافذة منبثقة بشكل كسول؟ مكتبة i18n لا تهتم، فهي تحمّل السلاسل أولًا على أي حال.
- **الأداء:**
في كل عملية إعادة عرض، يتم تهيئة كل مكون من مكوناتك بحمولة JSON ضخمة، مما يؤثر على تفاعلية التطبيق مع نموه.
- **الصيانة:**
الحفاظ على ملفات JSON كبيرة أمر مؤلم. عليك التنقل بين الملفات لإضافة ترجمة، مع التأكد من عدم وجود ترجمات مفقودة ولا وجود **مفاتيح يتيمة** متروكة.
- **نظام التصميم:**
يُحدث ذلك عدم توافق مع أنظمة التصميم (مثل مكون `LoginForm`) ويقيد تكرار المكونات عبر تطبيقات مختلفة.
**"لكننا اخترعنا Namespaces!"**
بالتأكيد، وهذا تقدم كبير. لنلقِ نظرة على مقارنة حجم الحزمة الرئيسية لتكوين Vite + React + React Router v7 + Intlayer. قمنا بمحاكاة تطبيق مكوّن من 20 صفحة.
المثال الأول لا يتضمن ترجمات تُحمّل عند الطلب لكل لغة ولا تقسيمًا للـ namespaces. المثال الثاني يتضمن تنقية المحتوى (content purging) + التحميل الديناميكي للترجمات.
| حزمة مُحسّنة | حزمة غير مُحسّنة |
| --------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
|  |  |
إذن، بفضل الـnamespaces، انتقلنا من هذا الهيكل:
```bash
locale/
├── en.json
├── fr.json
└── es.json
```
إلى هذا الهيكل:
```bash
locale/
├── en/
│ ├── common.json
│ ├── navbar.json
│ ├── footer.json
│ ├── home.json
│ └── about.json
├── fr/
│ └── ...
└── es/
└── ...
```
الآن عليك إدارة بدقة أي أجزاء من محتوى تطبيقك يجب تحميلها وأين. النتيجة، أن الغالبية العظمى من المشاريع تتخطى هذه الجزئية بسبب التعقيد (انظر دليل next-i18next على سبيل المثال لتتعرف على التحديات التي يمثلها مجرد اتباع الممارسات الجيدة: https://github.com/aymericzip/intlayer/blob/main/docs/blog/ar/i18n_using_next-i18next.md).
وبالتالي، تنتهي تلك المشاريع بمشكلة تحميل ملفات JSON الضخمة التي شرحناها سابقًا.
> ملاحظة: هذه المشكلة ليست خاصة بـ i18next فقط، بل تنطبق على جميع النهج المركزية المذكورة أعلاه.
ومع ذلك، أود أن أذكّركم بأن ليست كل الأساليب الجزئية تحل هذه المشكلة. على سبيل المثال، نهج `vue-i18n SFC` أو `inlang` لا يقومان بشكل افتراضي بتحميل الترجمات لكل لغة بشكل كسول (lazy load)، لذا فإنك ببساطة تستبدل مشكلة حجم الحزمة بمشكلة أخرى.
علاوة على ذلك، دون فصل مناسب للمسؤوليات (separation of concerns)، يصبح استخراج الترجمات وتقديمها للمترجمين للمراجعة أكثر صعوبة بكثير.
### كيف يحل نهج Intlayer القائم على كل مكوّن هذه المشكلة
يتبع Intlayer عدة خطوات:
1. **الإعلان:** أعلن محتواك في أي مكان داخل قاعدة الشيفرة باستخدام ملفات `*.content.{ts|jsx|cjs|json|json5|...}`. هذا يضمن فصل المسؤوليات مع إبقاء المحتوى موضوعًا جنبًا إلى جنب مع الكود. يمكن أن يكون ملف المحتوى مخصصًا لكل لغة (per-locale) أو متعدد اللغات.
2. **المعالجة:** تقوم Intlayer بتشغيل خطوة بناء لمعالجة منطق JS، والتعامل مع حالات السقوط الخاصة بالترجمات المفقودة، وتوليد أنواع TypeScript، وإدارة المحتوى المكرر، وجلب المحتوى من نظام إدارة المحتوى (CMS) الخاص بك، والمزيد.
3. **التنقية:** عندما يُبنى تطبيقك، تقوم Intlayer بتنقية المحتوى غير المستخدم (مماثل إلى حد ما لكيفية إدارة Tailwind للفئات الخاصة بك) عن طريق استبدال المحتوى كما يلي:
**الإعلان:**
```tsx
// src/MyComponent.tsx
export const MyComponent = () => {
const content = useIntlayer("my-key");
return <h1>{content.title}</h1>;
};
```
```tsx
// src/myComponent.content.ts
export const {
key: "my-key",
content: t({
ar: { title: "العنوان الخاص بي" },
en: { title: "My title" },
fr: { title: "Mon titre" }
})
}
```
**المعالجة:** تقوم Intlayer ببناء القاموس بناءً على ملف `.content` وتولد:
```json5
// مسار الملف: .intlayer/dynamic_dictionary/en/my-key.json
{
"key": "my-key",
"content": { "title": "My title" },
}
```
**الاستبدال:** يقوم Intlayer بتحويل المكوّن الخاص بك أثناء عملية بناء التطبيق.
**- وضع الاستيراد الثابت:**
```tsx
// تمثيل المكوّن بصيغة مشابهة لـ JSX
export const MyComponent = () => {
const content = useDictionary({
key: "my-key",
content: {
nodeType: "translation",
translation: {
en: { title: "My title" },
fr: { title: "Mon titre" },
},
},
});
return <h1>{content.title}</h1>;
};
```
**- وضع الاستيراد الديناميكي:**
```tsx
// تمثيل المكوّن بصيغة مشابهة لـ JSX
export const MyComponent = () => {
const content = useDictionaryAsync({
en: () =>
import(".intlayer/dynamic_dictionary/en/my-key.json", {
with: { type: "json" },
}).then((mod) => mod.default),
// نفس الشيء بالنسبة للغات الأخرى
});
return <h1>{content.title}</h1>;
};
```
> `useDictionaryAsync` يستخدم آلية مشابهة لـ Suspense لتحميل JSON المحلي فقط عند الحاجة.
**الفوائد الرئيسية لهذا النهج per-component:**
- إبقاء إعلان المحتوى قرب مكوناتك يسمح بصيانة أفضل (مثلاً نقل مكون إلى تطبيق أو نظام تصميم آخر. حذف مجلد المكون يزيل المحتوى المرتبط أيضاً، كما تفعل على الأرجح بالفعل لملفات `.test` و`.stories`)
- نهج لكل-مكوّن يمنع وكلاء الذكاء الاصطناعي من الحاجة إلى التنقّل عبر كل ملفاتك المختلفة. فهو يعالج كل الترجمات في مكان واحد، مما يحدّ من تعقيد المهمة ومن عدد الرموز (tokens) المستخدمة.
### القيود
بطبيعة الحال، هذا النهج يأتي بمقايضات:
- يصبح من الأصعب الربط مع أنظمة l10n الأخرى والأدوات الإضافية.
- قد تُصبح مقيدًا (vendor lock-in)، وهو ما يحدث بالفعل مع أي حل i18n بسبب الـ syntax الخاص به.
لهذا السبب تحاول Intlayer توفير مجموعة أدوات كاملة لـ i18n (مفتوحة المصدر ومجانية 100%)، تتضمن ترجمة باستخدام AI بواسطة مزوّد AI ومفاتيح API الخاصة بك. كما توفر Intlayer أدوات لمزامنة JSON الخاص بك، وتعمل مثل محولات رسائل ICU / vue-i18n / i18next لربط المحتوى بصيغها الخاصة.