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

    تنسيق التواريخ والأرقام حسب اللغة باستخدام Intl

    ترجمة النصوص هي النصف المرئي فقط من التدويل (i18n). أما النصف الآخر الذي يولد تقارير الأخطاء باستمرار فهو التنسيق: مستخدم ألماني يرى 1,234.56 بدلاً من 1.234,56، أو مستخدم ياباني يرى 08/02/2026 ويفهم أنه شهر أغسطس، أو تاريخ يتم عرضه بشكل مختلف بين الخادم والمتصفح فيؤدي إلى تعطل الصفحة بسبب عدم تطابق الهيدرة (Hydration mismatch) في React.

    لا شيء من هذا يتطلب تثبيت مكتبة خارجية. واجهة Intl البرمجية القياسية مدمجة بالفعل في كل بيئة تشغيل حديثة.

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

    ابدأ بحذف دوال مساعدة التواريخ المكتوبة يدوياً

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

    ts
    // الكود الذي يجب عليك حذفه:
    const formatDate = (d: Date) =>
      `${d.getMonth() + 1}/${d.getDate()}/${d.getFullYear()}`;
    

    تحل Intl.DateTimeFormat محلها بالكامل وتضمن صحة التنسيق في كل لغة:

    ts
    new Intl.DateTimeFormat("de-DE", { dateStyle: "long" }).format(date);
    // "2. August 2026"
    new Intl.DateTimeFormat("ja-JP", { dateStyle: "long" }).format(date);
    // "2026年8月2日"
    

    ينطبق الأمر ذاته على الأرقام. استدعاء toFixed(2) ينتج 1234.56 في كل مكان، وهو أمر خاطئ في معظم الدول الأوروبية.

    ما تغطيه واجهة Intl

    الواجهة البرمجية استخداماتها
    Intl.DateTimeFormat التواريخ والأوقات مع إعدادات جاهزة مثل dateStyle و timeStyle
    Intl.NumberFormat الأرقام العشرية، العملات، النسب المئوية، الوحدات، والترميز المختصر
    Intl.RelativeTimeFormat "منذ 3 أيام"، "خلال ساعتين"
    Intl.ListFormat دمج القوائم مثل "أ، ب، وج"
    Intl.PluralRules تحديد فئة الجمع المناسبة للرقم
    Intl.Collator الترتيب الهجائي السليم للنصوص وفق قواعد اللغة

    تعتبر Intl.Collator من أكثر الأدوات التي يغفل عنها المطورون. ترتيب النصوص عبر array.sort() العادية يعتمد على ترتيب محارف يونيكود، مما يضع الحروف المشكولة بعد حرف z ويخل بترتيب الحروف الخاصة. عند ترتيب قوائم يراها المستخدم، استخدم دائماً أداة collator.

    ts
    ["zebra", "édouard", "apple"].sort(new Intl.Collator("ar").compare);
    // ["apple", "édouard", "zebra"]
    

    تفضيل الإعدادات الجاهزة على الخيارات المصنوعة يدوياً

    تتيح خيارات dateStyle و timeStyle للغة نفسها تحديد الترتيب المنطقي والفواصل المناسبة. بينما يمنحك تحديد year و month و day يدوياً تحكماً نادراً ما يكون مرغوباً، لأن الترتيب الصحيح يختلف باختلاف الثقافات، وتنقض بذلك بيانات CLDR بافتراضات غير دقيقة.

    ts
    // اللغة تحدد التنسيق المناسب تلقائياً:
    new Intl.DateTimeFormat(locale, { dateStyle: "medium" }).format(d);
    
    // تحديد التنسيق يدوياً يجعله غير مناسب في مناطق أخرى:
    new Intl.DateTimeFormat(locale, {
      year: "numeric",
      month: "2-digit",
      day: "2-digit",
    }).format(d);
    

    لا تلجأ لتحديد المكونات بشكل صريح إلا إذا كانت هناك متطلبات تصميمية تلزمك بعرض ثابت ومحدد، كأعمدة الجداول الضيقة.

    إنشاء كائنات التنسيق عملية مكلفة برمجياً

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

    ts
    // إعادة إنشاء المنسق في كل دورة (غير فعال):
    rows.map((r) => new Intl.NumberFormat(locale).format(r.total));
    
    // إنشاؤه مرة واحدة ثم إعادة استخدامه (فعال):
    const nf = new Intl.NumberFormat(locale);
    rows.map((r) => nf.format(r.total));
    

    تنطوي دوال toLocaleDateString() و toLocaleString() على نفس المشكلة الخفية: كل استدعاء يبني منسقاً جديداً داخلياً. وهي مناسبة لقيمة واحدة فقط، وغير صالحة للقوائم.

    قم بتخزينها مؤقتاً بالاعتماد على دمج كود اللغة والخيارات:

    ts
    const cache = new Map<string, Intl.NumberFormat>();
    
    const getNumberFormat = (
      locale: string,
      options: Intl.NumberFormatOptions = {}
    ) => {
      const key = `${locale}:${JSON.stringify(options)}`;
      let formatter = cache.get(key);
      if (!formatter) {
        formatter = new Intl.NumberFormat(locale, options);
        cache.set(key, formatter);
      }
      return formatter;
    };
    

    خطأ المنطقة الزمنية الذي لا يظهر إلا في بيئة الإنتاج

    هذه المشكلة تستنزف ساعات طويلة من وقت المطورين. يقوم الخادم بتصيير التاريخ عبر SSR، ثم يستلمه المتصفح لعمل الهيدرة، فيلقي React خطأ عدم تطابق الهيدرة لأن النص المُولد من الخادم اختلف عن المتصفح.

    السبب هو أن Intl.DateTimeFormat تستخدم المنطقة الزمنية لجهاز التشغيل إذا لم يتم تحديدها بوضوح. خادم الإنتاج يعمل بتوقيت UTC، بينما حاسوب التطوير يعمل بتوقيت محلي مختلف. لذا يظل الخطأ خفياً محلياً ولا ينفجر إلا في الإنتاج.

    ts
    // الخادم بـ UTC والمتصفح بـ UTC+3 يختلفان في النتيجة، مما يفشل الهيدرة:
    new Intl.DateTimeFormat(locale, { dateStyle: "short" }).format(d);
    
    // كلاهما يتطابق تماماً:
    new Intl.DateTimeFormat(locale, { dateStyle: "short", timeZone: "UTC" }).format(
      d
    );
    

    ثلاثة حلول عملية:

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

    أياً كان اختيارك، حدد خاصية timeZone دائماً وبشكل صريح لأي تاريخ يتم تصييره في الخادم والعميل معاً. فالتاريخ بدون منطقة زمنية هو تاريخ يحمل قيمتين مختلفتين.

    العملة تحتاج إلى كود عملة، وليس إلى مجرد لغة

    اللغة والعملة مفهومان منفصلان. تحديد fr-FR لا يعني حتماً التعامل باليورو: فقد يطالع مستخدم فرنسي فاتورة بالدولار الأمريكي.

    ts
    new Intl.NumberFormat("fr-FR", { style: "currency", currency: "USD" }).format(
      1234.5
    );
    // "1 234,50 $US"
    

    تتحكم اللغة في الفواصل، وتجميع الأرقام، وموضع رمز العملة. بينما تأتي العملة نفسها من بيانات المعاملة. استنتاج العملة من كود اللغة خطأ محاسبي مباشر.

    انتبه أيضاً لخيار currencyDisplay. في الواجهات التي تجتمع فيها عدة عملات تشترك برمز الدولار ($)، فإن قيمة "code" تزيل أي لبس بين الدولار الأمريكي والكندي والأسترالي.

    الوقت النسبي أكثر وضوحاً من التوقيت المطلق

    بالنسبة للأحداث الأخيرة، فإن "منذ ساعتين" أسهل في القراءة من الطابع الزمني الثابت، وتتولى Intl.RelativeTimeFormat توطين ذلك بدقة.

    ts
    new Intl.RelativeTimeFormat("ar", { numeric: "auto" }).format(-1, "day");
    // "أمس"
    

    خاصية numeric: "auto" هي التي تنتج "أمس" بدلاً من التعبير الرقمي الجاف "قبل يوم واحد".

    ما تقدمه مكتبة Intlayer

    تغلف Intlayer هذه الواجهات في دوال مساعدة ذاتية التخزين المؤقت، لتغنيك عن إدارة خرائط الذاكرة يدوياً، وتطبق اللغة الحالية تلقائياً دون الحاجة لتمريرها في كل موضع استدعاء.

    ts
    import {
      number,
      currency,
      date,
      relativeTime,
      units,
      compact,
      list,
    } from "intlayer";
    
    number(1234.5); // "1,234.5"
    currency(1234.5, { currency: "EUR" }); // "1,234.50 €"
    date(new Date(), "short");
    relativeTime(now, twoHoursAgo, { unit: "hour", numeric: "auto" }); // "منذ ساعتين"
    units(5, { unit: "kilometer", unitDisplay: "long" }); // "5 كيلومتر"
    compact(1200); // "1.2 ألف"
    list(["تفاحة", "موزة", "برتقالة"]); // "تفاحة، موزة، وبرتقالة"
    

    تقبل الدالة date() الإعدادات الجاهزة ("short", "long", "dateOnly", "timeOnly", "full"). وتوجد أدوات مقابلة لـ React و Vue على هيئة hooks و composables تستخرج اللغة النشطة مباشرة من السياق.

    هذه مجرد طبقة تخزين مؤقت ومطابقة للغة مبنية فوق الواجهة القياسية للمنصة، أما سلوك التنسيق الفعلي فهو نابع من Intl. راجع كافة التفاصيل في توثيق أدوات التنسيق.

    أخطاء شائعة

    • استدعاء toLocaleDateString() دون تمرير لغة. يعتمد على لغة النظام المضيف، والتي تتغير في الخوادم بحسب ضبط الحاوية.
    • التنسيق داخل الحلقات التكرارية دون تخزين مؤقت. إنشاء الكائن يستهلك معظم وقت المعالجة.
    • تجاهل timeZone في التواريخ المشتركة بين الخادم والعميل. يسبب أخطاء هيدرة يستحيل إعادة إنتاجها محلياً.
    • تخمين العملة من اللغة. fr-FR لا تعني اليورو تلقائياً.
    • استخدام sort() العادية على نصوص الواجهة. استعن دائماً بـ Intl.Collator.
    • كتابة أسماء الشهور أو الأيام بشكل ثابت في الكود. جميعها مسجلة وموثوقة بالفعل في CLDR لكل لغة.
    • الإبقاء على numeric: "always" في الوقت النسبي. يؤدي لظهور "قبل يوم واحد" بدلاً من "أمس".

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

    التعليقات

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

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

    آخر المقالات