المؤلف:
    إنشاء:2026-09-02آخر تحديث:2026-09-02

    Vite i18n: الأجزاء المتعلقة بـ Vite نفسه، وليس بإطار عملك

    معظم الشروحات المعنونة بـ "Vite i18n" هي في الواقع شروحات لـ React أو Vue تصادف أنها تستخدم Vite. يتناول هذا المقال الطبقة الأساسية أدناها: كيف يتم استيراد الكتالوجات، ماذا يفعل Rollup بها، ولماذا التحميل الكسول (lazy loading) الذي كتبته ربما لا يكون كسولاً في الواقع.

    جدول المحتويات

    الاستيراد الثابت هو الافتراضي، وهو متزامن وفوري

    أبسط إعداد يستورد كل كتالوج في الجزء العلوي من الوحدة البرمجية (Module):

    src/i18n.ts
    import en from "./locales/en.json";
    import fr from "./locales/fr.json";
    import ja from "./locales/ja.json";
    

    هذا يعني تضمين ثلاثة كتالوجات في حزمة الإدخال الأساسية (Entry chunk)، في كل صفحة، ولكل زائر. هذا محتمل للغتين ومائة نص. لكن عند الوصول إلى عشر لغات، يصبح هذا أكبر تكلفة يمكن تفاديها في الحزمة بأكملها.

    import.meta.glob والمعامل الذي يخطئ فيه الجميع

    خاصية استيراد glob في Vite هي الحل المعتاد:

    ts
    const catalogs = import.meta.glob("./locales/*.json");
    
    export const loadCatalog = async (locale: string) => {
      const load = catalogs[`./locales/${locale}.json`];
      return (await load()) as Record<string, string>;
    };
    

    التحميل الكسول هو السلوك الافتراضي: كل عنصر هو دالة تُرجع استيراداً ديناميكياً، ويقوم Rollup بإنشاء حزمة منفصلة لكل ملف. أما إضافة { eager: true } فتدمج جميع الملفات مباشرة داخل الوحدة المستوردة، وهو بالضبط ما كنت تحاول تجنبه:

    ts
    // تضمين كافة اللغات في حزمة الدخول الأساسية (غير مستحسن إطلاقاً):
    const catalogs = import.meta.glob("./locales/*.json", { eager: true });
    

    الفخ يكمن في أن كلا الخيارين يعملان بشكل جيد في بيئة التطوير، لأن Vite يقدم الوحدات دون تجميعها في حزم. الفرق الحقيقي لا يظهر إلا داخل مجلد dist. اختبر ذلك عبر npx vite build && npx vite preview وافحص محتويات حزمة الدخول فعلياً.

    التقسيم حسب المسار نادراً ما يقسم الملفات فعلياً

    هذا هو السلوك الذي يفاجئ الكثيرين. تقوم بتقسيم الكتالوجات حسب الصفحات:

    plaintext
    locales/en/home.json
    locales/en/checkout.json
    

    ثم يقوم مساران مختلفان باستيراد checkout.json، فيقوم Rollup برفع هذا الملف إلى حزمة مشتركة يتم تنزيلها في كلتا الصفحتين. يعتمد تقسيم الحزم في Rollup على مخطط علاقات الوحدات (Module graph) وليس على أسماء المجلدات: أي وحدة يمكن الوصول إليها من أكثر من نقطة دخول تصبح تابعة لحزمة مشتركة. إضافة مسار ثالث لا يغير شيئاً، وإضافة مسار رابع قد تعيد توزيع الحزم بشكل غير متوقع.

    بالتالي، لا يصمد التقسيم حسب المسار إلا إذا كان مخطط الاستيراد منفصلاً تماماً. وإذا كان حجم الحزمة يهمك، فتحقق بالأدوات بدلاً من التخمين:

    bash
    npx vite build && npx vite-bundle-visualizer
    

    إذا كنت مضطراً لفرض حدود تقسيم معينة، فإن build.rollupOptions.output.manualChunks هو المهرب الوحيد، ولكن على حساب الصيانة اليدوية المستمرة.

    الكتالوجات لا تدعم التحديث الحي (HMR) تلقائياً

    عدل على أي مكون، وسيقوم Vite بتحديثه على الفور. لكن عدل على locales/fr.json، وبحسب طريقة الاستيراد، قد لا يحدث أي شيء. فالـ JSON المستورد ديناميكياً يفتقر لحدود HMR الأصلية، مما يجعل مخطط الوحدات عاجزاً عن معرفة كيفية إبطال المكونات المستهلكة له.

    يلجأ المطورون عادة إلى إعادة تشغيل خادم التطوير مع كل تعديل على النصوص، دون إدراك أن هذا الأمر يمكن تفاديه عبر إضافات i18n المتخصصة، حيث يجب على الإضافة استقبال تحديث HMR ودفع الرسائل المحدثة مباشرة للتطبيق المشغل.

    define يثبت اللغة بشكل دائم داخل الكود المترجم

    من المغري تحديد اللغة الافتراضية أثناء وقت البناء:

    vite.config.ts
    export default defineConfig({
      define: {
        __DEFAULT_LOCALE__: JSON.stringify(process.env.LOCALE ?? "en"),
      },
    });
    

    خاصية define تقوم باستبدال نصي بحت في وقت التجميع. القيمة الموجودة أثناء البناء هي التي يتم شحنها في الكود النهائي، مما يلزمك بعمل بناء منفصل لكل لغة. هذه استراتيجية معتمدة في حلول مثل نظام i18n المدمج في Angular، لكنها لا تناسبك إذا كنت تريد لعملية نشر واحدة خدمة جميع اللغات.

    القيم التي تتغير حسب طلب المستخدم يجب إبقاؤها بعيدة عن define وتحديدها وقت التشغيل.

    نقل معالجة النصوص إلى وقت البناء

    تتجه جميع الخيارات الناضجة في هذا المجال نحو مسار واحد: التوقف عن معالجة وتفسير النصوص داخل متصفح المستخدم.

    الإضافة ما تنقله إلى وقت البناء
    @intlify/unplugin-vue-i18n تجميع نصوص vue-i18n إلى دوال تصيير (شحن حزمة وقت التشغيل الخفيفة فقط)
    Lingui (ماكرو + إضافة) استخراج وتجميع الكتالوجات واستبدال الماكرو بمعرفات نصوص مختصرة
    Paraglide (inlang) تجميع كل رسالة في دالة مستقلة قابلة للـ tree-shaking
    vite-intlayer بناء قواميس لكل مكون على حدة، وتفريغ وضغط الحقول غير المستخدمة

    المكسب مزدوج: التخلص من مترجم النصوص الثقيل في الحزمة النهائية للمتصفح، والقدرة على حذف النصوص غير المستخدمة برمجياً. التكلفة المقابلة: خادم التطوير ونظام CI يحتاجان كلاهما للإضافة، كما يتطلب تشغيل tsc المجرد خارج Vite ضبطاً إضافياً.

    SSR: إياك والاحتفاظ باللغة على مستوى الوحدة البرمجية

    عند استخدام التصيير من جهة الخادم (SSR)، سواء عبر إطار عمل أو عبر vite-plugin-ssr، تنص القاعدة الذهبية على: المتغير الموجود على مستوى الوحدة والذي يحمل اللغة الحالية تتم مشاركته بين كافة الطلبات المتزامنة المعالجة في نفس خادم Node.js.

    ts
    // آمن تماماً داخل المتصفح. لكنه تسريب خطير للبيانات بين الطلبات على الخادم:
    export let currentLocale = "en";
    

    إذا طلب مستخدمان الخادم في نفس اللحظة، ستحدث حالة تسابق برمجية، وسيرى أحدهما لغة الآخر. هذا لا يحدث في التطوير المحلي لأنك المستخدم الوحيد. حدد اللغة لكل طلب بمفرده ومررها صراحة عبر السياق (Context) أو تخزين الطلب الخاص بإطار العمل.

    إضافة Vite الخاصة بـ Intlayer

    توفر Intlayer إضافة واحدة موحدة تدير بناء القواميس، ومراقبة الملفات أثناء التطوير، ومسار التحسين:

    vite.config.ts
    import react from "@vitejs/plugin-react";
    import { defineConfig } from "vite";
    import { intlayer } from "vite-intlayer";
    
    export default defineConfig({
      plugins: [react(), intlayer()],
    });
    

    إعادة كتابة الاستيرادات، والتفريغ (purge)، والضغط (minify) مفعلة بشكل افتراضي. يتم ضبط الإعدادات الرئيسية في ملف intlayer.config.ts:

    intlayer.config.ts
    import type { IntlayerConfig } from "intlayer";
    
    const config: IntlayerConfig = {
      build: {
        purge: true, // حذف حقول المحتوى التي لا يقرؤها أي مكون
        minify: true, // إعادة تسمية مفاتيح المحتوى إلى أسماء بديلة مختصرة
      },
    };
    
    export default config;
    

    نظراً لأن المحتوى يُصرح به لكل مكون على حدة بدلاً من حفظه في ملفات لغات عالمية ضخمة، فإن مرحلة التفريغ تعتمد على مخطط ارتباط الوحدات الحقيقي، مما يجعل حذف الأكواد غير المستغلة آمناً للغاية. التفاصيل في توثيق تحسين الحزم.

    أخطاء شائعة

    • وضع { eager: true } على استيراد كان الهدف منه أن يكون كسولاً. يعمل محلياً، ولكنه يشحن كل اللغات في الإنتاج.
    • الظن بأن أسماء المجلدات تقسم الحزم تلقائياً. يتبع Rollup مسار الاستيراد وليس المجلدات.
    • إعادة تشغيل خادم التطوير لرؤية تعديلات النصوص. دليل على غياب معالج HMR مناسب.
    • تثبيت اللغة في define. يلزمك بعمل بناء منفصل لكل لغة.
    • حفظ حالة اللغة على مستوى الوحدة في SSR. يسبب تسريب وتضارب اللغات بين المستخدمين المتزامنين.
    • قياس الأداء على خادم التطوير. الملفات غير المجمعة محلياً لا تمثل بأي شكل حزمة الإنتاج.

    للمزيد من القراءة

    التعليقات

    لا توجد تعليقات بعد. كن أول من يشارك أفكاره.

    مقالات ذات صلة

    آخر المقالات