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

    لماذا لم يتم تصميم ICU MessageFormat لبيئة JavaScript

    يُعد ICU MessageFormat معياراً ممتازاً. إنه متكامل، ويعرفه المترجمون جيداً، كما تستطيع معظم أنظمة إدارة الترجمة (TMS) التعامل معه بسلاسة. تكمن المشكلة في بيئة التشغيل (runtime) التي صُمم من أجلها في الأصل. نشأ ICU في بيئات C++ و Java، حيث لا تشكل إضافة محلل نصوص (parser) ومنسق رسائل كامل سوى تكلفة ضئيلة جداً مقارنة بحجم البرنامج الكلي. أما في حزمة المتصفح (browser bundle)، فإن هذه التكلفة تُدفع عند كل تحميل للصفحة.

    يتناول هذا المقال جذور ICU، ولماذا تبدو بنيته اللغوية ثقيلة عند التعامل مع صيغ الجمع، ولماذا يضيف التوافق الكامل وزناً إضافياً لأي مكتبة تدويل (i18n) في JavaScript. إذا كنت تبحث عن تفاصيل البنية اللغوية ذاتها، يمكنك مراجعة دليل مرجع ICU Message Format أولاً.

    من IBM إلى منظمة Unicode

    يرمز ICU إلى International Components for Unicode. بدأت صياغة رسائله في لغة Java: حيث قامت شركة Taligent، وهي مشروع مشترك بين Apple و IBM، بكتابة فئات التدويل لنظام JDK 1.1 (1997)، بما في ذلك java.text.MessageFormat. واصلت IBM تطويرها تحت اسم ICU4J، ونقلتها إلى C/C++ تحت اسم ICU4C، ثم أطلقتها كمشروع مفتوح المصدر في عام 1999. وفي عام 2016، انتقل مشروع ICU إلى مظلة منظمة Unicode (Unicode Consortium)، التي تدير أيضاً مستودع بيانات اللغات والمناطق CLDR الذي يعتمد عليه.

    فيم كان يُستخدم في بداياته

    كان الهدف الأساسي هو برمجيات الخوادم وتطبيقات سطح المكتب: تطبيقات Java للمؤسسات، ومنتجات IBM، ولاحقاً أنظمة التشغيل. كانت الرسائل تُحفظ في ملفات .properties الخاصة بلغة Java والتي يتم تحميلها عبر ResourceBundle، أو في صيغة حزم الموارد الخاصة بـ ICU للغات C/C++:

    messages_fr.properties
    inbox.unread={count, plural, one {# message non lu} other {# messages non lus}}
    
    java
    String pattern = bundle.getString("inbox.unread");
    String text = new MessageFormat(pattern, Locale.FRENCH)
        .format(Map.of("count", 5)); // "5 messages non lus"
    

    لم يكن الإصدار الأول من JDK يدعم الكلمة المفتاحية plural. بل كان يعتمد على choice بنطاقات رقمية ({0,choice,0#no files|1#one file|1<{0} files})، وهو ما يناسب اللغات التي تتبع قواعد جمع مطابقة للإنجليزية فقط. أضاف ICU صيغة plural المستندة إلى قواعد CLDR في عام 2008 (ICU 4.0)، وأضاف select في عام 2010 (ICU 4.4).

    الفارق بينه وبين ملفات .po

    غالباً ما يتم الخلط بين ICU ونظام gettext، إلا أنهما ينتميان إلى مدرستين مختلفتين. تأتي ملفات .po من GNU gettext (C ثم Linux، ثم PHP و Python). يحتوي مدخل .po على أزواج بسيطة من msgid / msgstr، وتُحدد صيغ الجمع بواسطة تعبير مكتوب بلغة C في ترويسة الملف (Plural-Forms: nplurals=2; plural=(n > 1);). لا توجد أي شروط تفرع داخل نص الرسالة نفسه. في المقابل، يضع ICU التفرع داخل النص مباشرة، مما يسمح لرسالة واحدة بالجمع بين plural و select وتنسيق الأرقام معاً.

    أين يعمل ICU اليوم

    يأتي ICU4C مدمجاً بشكل أصيل في Android و iOS و macOS و Windows و Node.js ومحركات JavaScript في كل من Chrome و Firefox. واجهات برمجة التطبيقات القياسية Intl في المتصفح مبنية في معظمها عليه. وبالتالي، يحتوي المتصفح بالفعل على قواعد الجمع وتنسيقات الأرقام والتواريخ المأخوذة من ICU. ما ينقص المتصفح فقط هو محلل نصوص الرسائل: إذ لا يزال Intl.MessageFormat مقترحاً في مراحله الأولى ضمن TC39، وهو مبني على بنية MessageFormat 2 الأحدث وغير متوافق مع ICU MessageFormat 1.

    يوضح هذا المسار التاريخي أسباب التصميم المتبع:

    • استهداف بيئات تشغيل الخوادم والحواسيب المكتبية. يُعد تحليل النصوص أثناء التشغيل عملية غير مكلفة هناك، كما أن المكتبة تُثبت مرة واحدة على مستوى نظام التشغيل ولا يقوم كل زائر بتحميلها عبر الشبكة.
    • لغة مخصصة (DSL) داخل سلسلة نصية. التفرع وتنسيق الأرقام والتواريخ والتداخل يعيش كله داخل بنية نصية واحدة يمكن للمترجم تعديلها دون لمس التعليمات البرمجية.
    • استهداف الشمولية المطلقة. وجود مشغل خاص لكل حالة نحوية قد يحتاجها المترجم.

    لا يُعد أي من هذه القرارات خطأ في حد ذاته. كل ما في الأمر أنها افترضت بيئة تشغيل تختلف كلياً عن طبيعة متصفح الويب.

    بنية صيغ الجمع تتسم بالإسهاب

    أكثر تراكيب ICU شيوعاً هو التركيب الأكثر تعقيداً في الوقت ذاته. التعبير عن عدّاد يتضمن حالة الصفر يبدو كالتالي:

    text
    {count, plural,
      =0 {No unread messages}
      one {# unread message}
      other {# unread messages}
    }
    

    يتطلب هذا التركيب اسم المعامل، الكلمة المفتاحية plural، تصنيفاً لكل تفرع، أقواساً معقوفة متداخلة، واستخدام # كرمز خاص يعمل فقط داخل كتل الجمع. وإذا أضفنا جنساً نحوياً للفاعل (gender)، يتفرع النص بشكل أعمق:

    text
    {gender, select,
      female {{count, plural,
        one {She has # unread message}
        other {She has # unread messages}
      }}
      male {{count, plural,
        one {He has # unread message}
        other {He has # unread messages}
      }}
      other {{count, plural,
        one {They have # unread message}
        other {They have # unread messages}
      }}
    }
    

    تسعة أسطر من أصل خمسة عشر مخصصة فقط للبنية التنسيقية. على سبيل المثال، تحتاج اللغة البولندية أو العربية إلى تفرعات جمع متعددة لكل تفرع جنسي، مما يجعل النص المترجم كتلة متداخلة من الأقواس المعقوفة حيث يؤدي فقدان قوس إغلاق واحد } إلى تعطل الرسالة بأكملها، وغالباً لا يُكتشف هذا إلا أثناء وقت التشغيل (runtime).

    في بيئة JavaScript، يمكن التعبير عن نفس البنية كبيانات مباشرة: كائن تتطابق مفاتيحه مع فئات الجمع النحوية، مع تدقيقها فورياً عبر نظام الأنواع (type system) والمحرر، ودون الحاجة إلى وجود محلل لغوي (parser) وسيط بين الملف والقيمة النهائية.

    الشمولية تفرض ضريبتها على الحجم

    يغطي معيار ICU جوانب متعددة:

    • plural مع المطابقات الدقيقة (=0) والإزاحات (offset:)
    • selectordinal مع جدول الأعداد الترتيبية المستقل من CLDR
    • select مع إمكانية التداخل لأي عمق
    • معاملات number و date و time بالأسماء الكلاسيكية (number, currency) وبصيغ الهياكل (skeletons مثل ::currency/EUR compact-short)
    • قواعد الهروب وعلامات التنصيص ('{', '')
    • وسوم النصوص المنسقة (rich-text) في بعض البيئات (<b>…</b>)

    أي مكتبة تعد بتقديم توافق كامل 1:1 مع ICU يجب أن تشحن كل هذه المكونات في الحزمة، لأنها لا تستطيع التنبؤ في مرحلة البناء (build time) بالميزات التي ستستخدمها نصوصك. عملياً، يتطلب ذلك شحن:

    1. محلل لغوي (parser) يحول النص إلى شجرة بناء نحوية (AST)، مع معالجة أخطاء الأقواس غير الصحيحة.
    2. محلل هياكل (skeleton parser) لبنية :: الخاصة بالأرقام والتواريخ، وهي لغة صغيرة قائمة بذاتها.
    3. منسق (formatter) يجوب الـ AST ويربط كل عقدة بـ Intl.PluralRules و Intl.NumberFormat و Intl.DateTimeFormat.

    الجزء الثالث خفيف للغاية، لأن بيئة JavaScript الحديثة تحتوي بالفعل على منطق CLDR مدمجاً في Intl. أما أول جزأين، فيوجدان فقط لقراءة بنية النصوص المعقدة. في حزمة intl-messageformat من FormatJS، وهي الأساس المرجعي الذي تعتمد عليه مكتبات مثل react-intl و next-intl، يترجم هذا إلى حوالي 10 كيلوبايت من كود JavaScript المضغوط يُرسل إلى كل زائر للموقع، حتى قبل تحميل رسائل تطبيقك نفسه.

    تعتمد معظم تطبيقات الويب على جزء ضئيل للغاية: استبدال المتغيرات {name} وبضعة تفرعات plural. ومع ذلك، فإنها تضطر لتحميل المحلل الكامل للهياكل والأعداد الترتيبية، لأن تحليل النصوص في وقت التشغيل يمنع أداة الحزم (bundler) من استبعاد الأكواد غير المستخدمة.

    واجهت next-intl نفس المعضلة

    هذا الانشغال ليس مجرد ترف نظري. فقد وصلت مكتبة next-intl، وهي إحدى أكثر المكتبات شعبية المعتمدة على ICU، إلى نفس النتيجة. ففي إصدارها 4.8 (يناير 2026)، قدمت خياراً تجريبياً باسم precompile. يقوم هذا الخيار بتحليل رسائل ICU أثناء مرحلة البناء وتحويلها إلى AST مقتضب، مستبدلاً المحلل أثناء التشغيل بمقيّم (evaluator) فائق الخفة. وأوضح المشروع أن تفعيل هذا الخيار ساهم في توفير نحو 9 كيلوبايت من كود JavaScript المضغوط.

    غير أن هذا التنازل يكشف أيضاً حدود هذا النهج: حيث يتوقف t.raw عن العمل عند استخدام التجميع المسبق، لأن النص الأصلي الخام لـ ICU لم يعد موجوداً في وقت التشغيل. وبمجرد التوقف عن التحليل داخل المتصفح، فإن ما تشحنه فعلياً لم يعد ICU الخالص. بل أصبحت تشحن تمثيلاً مجمعاً له، وتتحول بنية النصوص إلى مجرد صيغة لكتابة المحتوى في البداية.

    وهنا يطرح السؤال المنطقي نفسه: ما دام المتصفح لا يقرأ هذا النص المعقد مباشرة، فلماذا يتعين على المطورين والمترجمين كتابته بتلك الصيغة المليئة بالتعقيد؟

    كيف يبدو النهج الأصيل المعتمد على JavaScript

    توفر بيئة JavaScript بالفعل الجزء الأكثر تعقيداً على نحو أصيل. إذ تدرك Intl.PluralRules فئات الجمع المعقدة للغات وفئات الأعداد الترتيبية. كما تتولى Intl.NumberFormat و Intl.DateTimeFormat إدارة العملات، الوحدات، التدوينات المختصرة والتقاويم المختلفة. كل ما تبقى بعد ذلك هو اختيار الفرع المناسب وحقن القيم، وهو أمر لا يتطلب سوى بضعة أسطر برمجية حين تكون البنية ممثلة في شكل بيانات بدلاً من سلاسل نصية.

    هذا هو النموذج الدقيق الذي تعتمده Intlayer. التفرع عبارة عن دالة داخل إعلان محتوى محكم بالأنواع (typed content declaration)، بحيث تعلن كل لغة عن الفئات التي تقتضيها قواعدها النحوية فقط:

    **/*.content.ts
    import { gender, plural, t, type Dictionary } from "intlayer";
    
    const inboxContent = {
      key: "inbox",
      content: {
        unread: t({
          ar: plural({
            zero: "لا توجد رسائل غير مقروءة",
            one: "رسالة واحدة غير مقروءة",
            two: "رسالتان غير مقروءتين",
            few: "{{count}} رسائل غير مقروءة",
            many: "{{count}} رسالة غير مقروءة",
            other: "{{count}} رسالة غير مقروءة",
          }),
          en: plural({
            one: "{{count}} unread message",
            other: "{{count}} unread messages",
          }),
          pl: plural({
            one: "{{count}} nieprzeczytana wiadomość",
            few: "{{count}} nieprzeczytane wiadomości",
            many: "{{count}} nieprzeczytanych wiadomości",
            other: "{{count}} nieprzeczytanej wiadomości",
          }),
        }),
      },
    } satisfies Dictionary;
    
    export default inboxContent;
    
    **/*.tsx
    const { unread } = useIntlayer("inbox");
    
    unread(5); // في بيئة اللغة البولندية ← "5 nieprzeczytanych wiadomości"
    

    أهم الفوارق مقارنة بنظام ICU:

    • انعدام كود المحلل اللغوي في الحزمة. تصل البنية إلى المتصفح ككائن JavaScript جاهز بالفعل. وتختار دالة plural المفتاح المطلوب عبر Intl.PluralRules المدمجة في المتصفح.
    • اكتشاف الأخطاء في مرحلة البناء. تفرع مفقود أو خطأ إملائي في تسمية مفتاح يتحول إلى خطأ في نوع البيانات بـ TypeScript، مما يمنع حدوث أعطال غير متوقعة في بيئة الإنتاج.
    • بقاء التنسيق خارج جسم الرسالة. تُعالج الأرقام والتواريخ والعملات عبر خطافات التنسيق (formatter hooks) التي تتعامل مع Intl مباشرة، دون الحاجة لتحليل نصوص الهياكل (skeletons).
    • الميزات غير المستخدمة لا تستهلك أي مساحة. إذا لم تستخدم أي رسالة ميزة gender، تحذفها أداة الحزم تماماً عبر خاصية tree-shaking.

    • خطافات التنسيق

    بالتأكيد توجد اعتبارات خاصة بهذا النهج: فهو يتطلب خطوة بناء، وملفات المحتوى تكون في هيئة كود برمجي بدلاً من نصوص مسطحة، وبعض أدوات أنظمة إدارة الترجمة TMS المصممة حصراً لـ ICU قد لا تدعم قراءة إعلانات TypeScript بشكل مباشر.

    متى يظل ICU هو الخيار الأنسب

    يبقى ICU الخيار الأفضل في الحالات التالية:

    • اعتماد خط تدفق الترجمة بالكامل عليه. الكثير من أدوات TMS تستورد وتصدر نصوص ICU، وفرق المترجمين مدربة على هذه الصيغة.
    • مشاركة الرسائل بين عدة منصات مختلفة. تغذية تطبيق iOS وتطبيق Android وتطبيق الويب من كتالوج ترجمة موحد يمثل سبباً وجيهاً للالتزام بصيغة قياسية موحدة.
    • امتلاك قاعدة نصوص ضخمة ومكتوبة مسبقاً بـ ICU. إعادة كتابة آلاف الرسائل القديمة من الصفر نادراً ما تكون مجدية بمفردها.

    في الحالة الأخيرة، لست مضطراً للاختيار بين إعادة الكتابة الكاملة أو تحمل محلل ضخم. إذ يستطيع محول التوافق مع react-intl الخاص بـ Intlayer قراءة نصوص ICU الحالية (plural و select و selectordinal و # وتنسيقات number / date / time الكلاسيكية)، مما يتيح لك الانتقال تدريجياً وحصر تكلفة ICU على الرسائل القديمة التي لا تزال بحاجة إليها فقط.

    الخلاصة

    حل ICU MessageFormat إشكالية حقيقية: إدارة القواعد النحوية تخص المترجمين، وليست شروطاً مثل if (count === 1) مبعثرة في كود التطبيق. وقد قدم هذا الحل بكفاءة في بيئات لا تكلف فيها عملية تحليل نصوص DSL أي أثر يُذكر. أما في متصفح الويب، فإن التوافق الكامل يعني إرسال محلل لغوي لميزات لا تستخدمها معظم التطبيقات، لدرجة دفعت المكتبات المبنية على ICU نفسها للاتجاه نحو التجميع المسبق لتفادي هذا العبء.

    تتضمن بيئة JavaScript بالفعل قواعد CLDR عبر كائن Intl. وما يحتاجه أي تنسيق تدويل حديث هو هيكل منطقي للتفرع، وهذا الهيكل يمكن التعبير عنه بكفاءة فائقة على هيئة بيانات محددة الأنواع.

    للاستزادة

    التعليقات

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

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

    آخر المقالات