الرئيسيةبيئة اختبارمعرض الأعمالتطبيقوثيقةمدونة
    • Englishالإنجليزية
      EN
    • Русскийالروسية
      RU
    • 日本語اليابانية
      JA
    • françaisالفرنسية
      FR
    • 한국어الكورية
      KO
    • 中文الصينية
      ZH
    • Españolالإسبانية
      ES
    • Deutschالألمانية
      DE
    • العربيةالعربية
      AR
    • Italianoالإيطالية
      IT
    • British Englishالإنجليزية البريطانية
      EN-GB
    • Portuguêsالبرتغالية
      PT
    • हिन्दीالهندية
      HI
    • Türkçeالتركية
      TR
    • polskiالبولندية
      PL
    • Indonesiaالإندونيسية
      ID
    • Tiếng Việtالفيتنامية
      VI
    • Українськаالأوكرانية
      UK
    /
    Alt+←
    ما هو التدويل (i18n)?
    SEO و التدويل
    دليل
    • i18n باستخدام next-i18next
    • i18n باستخدام next-intl
    استخدم Intlayer على الحل الخاص بك
    • أتمتة next-i18next
    • أتمتة react-i18next
    • أتمتة next-intl
    • أتمتة react-intl
    • أتمتة vue-i18n
    مقارنات
    • next-i18next vs next-intl vs Intlayer
    • react-i18next vs react-intl vs Intlayer
    الوثائق
    1. Blog
    2. Per component vs centralized i18n
    Creation:2025-09-10Last update:2025-09-10
    استخدم هذه الصفحة والموفر AI الذي تريده
    ChatGPT
    Claude
    DeepSeek
    Google AI mode
    Gemini
    Perplexity
    Mistral
    Grok

    استخدم مساعدك المفضل للملخص واستخدم هذه الصفحة والموفر AI الذي تريده

    تمت ترجمة محتوى هذه الصفحة باستخدام الذكاء الاصطناعي.

    اعرض آخر نسخة المحتوى الأصلي باللغة الإنكليزية
    Edit this doc

    If you have an idea for improving this documentation, please feel free to contribute by submitting a pull request on GitHub.

    GitHub link to the documentation
    Copy

    Copy doc Markdown to clipboard

    التعريب لكل مكوّن مقابل التعريب المركزي

    نهج التعريب لكل مكوّن ليس مفهومًا جديدًا. على سبيل المثال، في بيئة Vue، تدعم مكتبة vue-i18n i18n SFC (Single File Component). كما يقدم Nuxt ترجمات لكل مكوّن، ويستخدم Angular نمطًا مشابهًا من خلال Feature Modules.

    حتى في تطبيق Flutter، غالبًا ما نجد هذا النمط:

    bash
    نسخ الكود

    نسخ الكود إلى الحافظة

    lib/└── features/    └── login/        ├── login_screen.dart        └── login_screen.i18n.dart  # <- الترجمة موجودة هنا
    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، نرى في الأساس مقاربات مختلفة، سأقوم بتجميعها في ثلاث فئات:

    النهج المركزي (i18next, next-intl, react-intl, lingui)

    • (بدون مساحات أسماء) يعتبر مصدرًا واحدًا لاسترجاع المحتوى. بشكل افتراضي، تقوم بتحميل المحتوى من كل الصفحات عندما يتم تحميل تطبيقك.

    النهج التفصيلي (intlayer, inlang)

    • تفصيل استرجاع المحتوى حسب المفتاح، أو حسب المكوّن.

    في هذه المدونة، لن أركز على الحلول المعتمدة على الـ compiler (compiler-based)، والتي قمت بتغطيتها سابقًا هنا: Compiler vs Declarative i18n. لاحظ أن 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 على سبيل المثال لتتعرف على التحديات التي يمثلها مجرد اتباع الممارسات الجيدة: /ar/blog/nextjs-internationalization-using-next-i18next).

    وبالتالي، تنتهي تلك المشاريع بمشكلة تحميل ملفات 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.tsxexport const MyComponent = () => {  const content = useIntlayer("my-key");  return <h1>{content.title}</h1>;};
    tsx
    نسخ الكود

    نسخ الكود إلى الحافظة

    // src/myComponent.content.tsexport 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
    نسخ الكود

    نسخ الكود إلى الحافظة

    // تمثيل المكوّن بصيغة مشابهة لـ JSXexport 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
    نسخ الكود

    نسخ الكود إلى الحافظة

    // تمثيل المكوّن بصيغة مشابهة لـ JSXexport 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 لربط المحتوى بصيغها الخاصة.

    ما هو التدويل (i18n)?
    Alt+→

    في هذه الصفحة

      المناقشات مجهولة الهوية ويتم مراجعتها بانتظام لمعالجة المشكلات الشائعة. لا تتردد في مشاركة أفكار الميزات أو التعليقات على الوثائق أو أي شيء يتعلق بـ Intlayer, نستخدم هذه المدخلات لتشكيل خارطة الطريق وتحسين المنتج.

      lib/└── features/    └── login/        ├── login_screen.dart        └── 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);}
      locale/├── en.json├── fr.json└── es.json
      locale/├── en/│   ├── common.json│   ├── navbar.json│   ├── footer.json│   ├── home.json│   └── about.json├── fr/│   └── ...└── es/    └── ...
      // src/MyComponent.tsxexport const MyComponent = () => {  const content = useIntlayer("my-key");  return <h1>{content.title}</h1>;};
      // src/myComponent.content.tsexport const {  key: "my-key",  content: t({    ar: { title: "العنوان الخاص بي" },    en: { title: "My title" },    fr: { title: "Mon titre" }  })}
      // مسار الملف: .intlayer/dynamic_dictionary/en/my-key.json{  "key": "my-key",  "content": { "title": "My title" },}
      // تمثيل المكوّن بصيغة مشابهة لـ JSXexport 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>;};
      // تمثيل المكوّن بصيغة مشابهة لـ JSXexport 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>;};