«عربي أولاً» ليست إعداد لغة تُضاف في نهاية المشروع، بل قرار معماري يمسّ طبقة العرض وطبقة البيانات وطبقة الحساب معاً. الفرق بين منتج مُعرَّب ومنتج مبنيّ بالعربية أولاً يظهر في سبعة مواضع تقنية محددة، كل واحد منها يكسر افتراضاً شائعاً في أدوات التطوير الغربية.
هذا المقال يستعرض هذه المواضع السبعة بأمثلة ملموسة من بناء منتج لإدارة العقارات في السوق السعودي، وهي تنطبق على أي منتج مالي أو إداري يستهدف المنطقة.
1. الاتجاه ليس direction: rtl
أشيع خطأ في دعم RTL هو افتراض أن ضبط dir="rtl" يكفي. المشكلة الحقيقية في الخصائص الفيزيائية مقابل المنطقية.
انظر لهذا المثال: بطاقة فيها أيقونة في التدفق الطبيعي، ووسم مثبّت بـposition: absolute عبر left: 1rem. في العربية سيبدو كل شيء سليماً — الأيقونة يميناً والوسم يساراً. لكن في الإنجليزية سينتقل التدفق الطبيعي إلى اليسار، بينما يبقى الوسم مثبّتاً عند left نفسها، فيركب أحدهما فوق الآخر.
الحل استخدام الخصائص المنطقية: inset-inline-start وinset-inline-end وmargin-inline وpadding-inline بدل left/right/margin-left. هذه الخصائص تنعكس تلقائياً مع اتجاه المستند. في Tailwind تقابلها start-* وend-* وms-* وme-*.
القاعدة العملية: أي قيمة فيزيائية في CSS هي قنبلة موقوتة تنفجر عند تبديل الاتجاه. اختبر كل شاشة باللغتين — والأهم أن تختبر شاشة تقارير معقدة لا الصفحة الرئيسية، لأن الجداول هي حيث تظهر المشكلة.
2. التقويم الهجري: لا تكتب محوّلاً بنفسك
عقود الإيجار في السعودية تُكتب بالهجري كثيراً، والأنظمة المالية تعمل بالميلادي. الإغراء الأول أن تكتب دالة تحويل بالحساب الفلكي — وهذا خطأ.
التقويم الهجري المعتمد رسمياً في السعودية هو تقويم أم القرى، وأطوال أشهره ليست ثابتة بصيغة رياضية بسيطة. أي محوّل يدوي سينحرف عن التقويم الرسمي في تواريخ معينة، وانحراف يوم واحد في تاريخ استحقاق قسط يعني نزاعاً مع المستأجر.
الحل موجود في المنصة نفسها منذ سنوات — واجهة Intl:
const hijri = date.toLocaleDateString('ar-SA-u-ca-islamic-umalqura', { year: 'numeric', month: 'long', day: 'numeric'});الوسم u-ca-islamic-umalqura يعطي تقويم أم القرى مباشرة من بيانات ICU المضمّنة في المنصة. انتبه: u-ca-islamic وحده تقويم فلكي محسوب يختلف عن أم القرى بيوم في بعض الأشهر، وar-SA بلا وسم صار يعرض الميلادي في الإصدارات الحديثة. لا تبعية خارجية، ولا جدول تحويل تتولى صيانته.
القاعدة: خزّن التاريخ ميلادياً في قاعدة البيانات، واعرضه بالتقويمين. التخزين بالهجري يجعل كل استعلام نطاقي كابوساً.
3. الأرقام المالية: Decimal لا Float
0.1 + 0.2 !== 0.3 ليست طرفة، بل سبب فروق التسوية في الأنظمة المالية. الأرقام العشرية العائمة تمثّل الكسور بالنظام الثنائي، وكثير من الكسور العشرية لا تمثيل ثنائياً دقيقاً لها.
القرار الصحيح أن تكون كل حقول المبالغ من نوع Decimal على مستوى قاعدة البيانات لا على مستوى التطبيق فقط. في تطبيق عملي بُنيت المبالغ على Decimal(20, 8):
amount Decimal @db.Decimal(20, 8)paidAmount Decimal @default(0) @db.Decimal(20, 8)لماذا ثماني خانات عشرية والريال يُكتب بخانتين؟ لأن المنتج يدعم ثماني عملات، ويحفظ المبلغ الأصلي كما أُدخل قبل التحويل. سعر الصرف يحتاج دقة أعلى من دقة العرض، وتقريب المبلغ عند التخزين يتراكم عبر آلاف السجلات.
تحذير عملي: التزم بـDecimal حتى النهاية. تحويل القيمة إلى Number في جافاسكربت لإجراء عملية حسابية واحدة يُفقدك كل ما كسبته.
وهنا فخّ يستحق التنبيه: طبقات ORM الحديثة تُعيد حقول Decimal ككائنات لا كأرقام بدائية. هذا سلوك صحيح يحمي الدقة، لكنه يفاجئ الفريق في ثلاثة مواضع. الأول أن JSON.stringify قد يحوّلها إلى نص، فيصل للواجهة "1500.00000000" بدل رقم. الثاني أن المقارنات المباشرة مثل a > b لا تعمل كما تتوقع على الكائنات. الثالث أن + تُنتج ضمّ نصوص لا جمعاً حسابياً — وهذا أخطرها لأنه لا يرمي خطأً، بل ينتج رقماً خاطئاً صامتاً.
المعالجة: اعتمد دوال المكتبة الحسابية (.plus() و.minus() و.comparedTo()) داخل طبقة الخدمة، وحوّل إلى نص منسّق عند حدود العرض فقط، وأضف اختباراً يجمع ثلاثة مبالغ كسرية ويؤكد المجموع بالضبط.
الاستثناء المشروع للأرقام العائمة هو البيانات غير المالية — الإحداثيات الجغرافية والمساحات مثلاً — حيث الدقة المطلقة غير مطلوبة.
4. الدفعة الجزئية: المشكلة التي لا تظهر في المتطلبات
هذه أكثر حالة تُغفل في تصميم الأنظمة المالية: ماذا يحدث حين يدفع العميل مبلغاً لا يغطي قسطاً كاملاً؟
المعالجة الساذجة تترك المبلغ «غير مخصص» فيتراكم دين وهمي. المعالجة الصحيحة تحتاج استراتيجية توزيع صريحة. الأشيع هو FIFO: يُسدَّد أقدم قسط مستحق أولاً.
لكن الواقع أعقد. حين يوجد رصيد مرحّل من عقد سابق، تظهر حالات لا يحسمها FIFO وحده. لذلك يفيد وجود أكثر من استراتيجية:
FIFO— الافتراضي: الأقدم أولاً، بدءاً بالمرحّل ثم الأقساط الحاليةCARRYOVER_ONLY— يسدّد الرصيد المرحّل فقطIGNORE_CARRYOVER— يتجاهل المرحّل، ويشترط تسجيل سبب
هذا الشرط الأخير هو الأهم هندسياً: أي انحراف عن القاعدة الافتراضية يجب أن يترك أثراً قابلاً للتدقيق.
نمط آخر يستحق التبني: نقطة نهاية للمعاينة تحاكي التوزيع دون حفظ، فيرى المستخدم أثر الدفعة قبل تثبيتها. هذا ما تفعله أملاكي العقارية (Amlakire) عبر معاينة توزيع الدفعة قبل التسجيل، وهو يقلل أخطاء الإدخال أكثر من أي رسالة تحذير.
5. عزل البيانات: الافتراض يجب أن يكون المنع
في منتج يخدم مكاتب عقارية متعددة، كل مكتب يدير ملاكاً متعددين، سؤال العزل ليس ميزة بل شرط وجود.
الخطأ الشائع أن يكون العزل طبقةً في واجهة المستخدم أو شرطاً يُضاف في كل استعلام يدوياً. أي استعلام يُنسى فيه الشرط يصبح تسريباً. المعمار السليم يجعل النطاق إجبارياً على مستوى طبقة الوصول للبيانات: كل كيان يحمل مُعرّف الجهة المالكة، وكل استعلام يمرّ عبر طبقة تفرض التصفية افتراضياً.
في المنتج المذكور يظهر agencyId في 73 موضعاً في مخطط قاعدة البيانات، وتُصفّي به 36 خدمة. الرقم نفسه ليس إنجازاً — الإنجاز أن يكون صفر استعلامات بلا تصفية.
اختبار عملي: اكتب اختباراً يحاول قراءة سجل تابع لجهة أخرى بمعرّف صحيح ويتوقع 404 لا 403. إعادة 403 تكشف وجود السجل، وهي تسريب معلومات بحد ذاتها.
6. الضريبة تنتمي للوحدة لا للحساب
في السعودية يخضع الإيجار التجاري لضريبة قيمة مضافة 15%، بينما الإيجار السكني معفى (0%). النمذجة الخاطئة تضع نسبة الضريبة إعداداً على مستوى الحساب أو المكتب، فينكسر النظام لحظة إدارة المكتب لعقارين مختلفي النوع.
النمذجة الصحيحة تجعل الفئة الضريبية خاصية للوحدة العقارية نفسها، ويُحتسب الوعاء عند إصدار الفاتورة من فئة الوحدة لا من إعداد عام. المبدأ العام: الخصائص التنظيمية تنتمي إلى أصغر كيان تنطبق عليه، وأي رفع لها إلى مستوى أعلى هو دين تقني مؤجل.
7. النص العربي عند حدود النظام: الإدخال والإخراج
يبقى تحدٍّ أخير يظهر عند طرفَي النظام — حيث يدخل النص وحيث يخرج.
عند الإدخال: لوحة المفاتيح العربية تنتج أرقاماً هندية ٠١٢٣٤٥٦٧٨٩، والفارسية تنتج ۰۱۲۳۴۵۶۷۸۹. تمريرها إلى parseFloat يعطي NaN مباشرة. والأسوأ أن الفاصلة العشرية العربية محرف مستقل تماماً — ٫ (U+066B) — وليست الفاصلة , ولا النقطة ..
الحل تطبيع الإدخال قبل أي تحليل رقمي:
const normalizeArabicNumbers = (value) => value .replace(/[٠-٩]/g, d => String('٠١٢٣٤٥٦٧٨٩'.indexOf(d))) .replace(/[۰-۹]/g, d => String('۰۱۲۳۴۵۶۷۸۹'.indexOf(d)));ثم توحيد كل صور الفاصلة العشرية (, و، و٫) إلى نقطة واحدة. بدون هذه الخطوة سيشتكي مستخدمون من أن «النظام يرفض المبلغ» وأنت ترى حقلاً يبدو سليماً على الشاشة.
عند الإخراج: توليد PDF عربي هو الفخ الأكبر. معظم مكتبات PDF منخفضة المستوى ترسم المحارف واحداً واحداً بلا تشكيل (ربط الحروف) وبلا خوارزمية bidi، فتخرج الكلمات مقطّعة ومعكوسة. المكتبة التي تعمل بإتقان مع اللاتينية قد تنتج مستنداً عربياً غير مقروء تماماً.
الطريق الأكثر موثوقية هو تفويض التصيير لمحرك متصفح: تبني HTML بـdir="rtl" وخط عربي مضمّن، ثم تحوّله إلى PDF عبر Chromium بلا واجهة. المتصفح يتكفّل بالتشكيل والاتجاه لأنه يحلّهما أصلاً لعرض صفحات الويب.
القاعدة: لا تختبر مسار الـPDF بنص لاتيني. اختبره بجملة عربية فيها أرقام وتاريخان، وهي أصعب حالة ممكنة.
خلاصة
التحديات السبعة تشترك في نمط واحد: كلها تنشأ من افتراض ضمني في الأدوات القياسية — أن الاتجاه من اليسار لليمين، وأن التقويم ميلادي، وأن العملة واحدة، وأن الضريبة نسبة ثابتة، وأن كل مستخدم يرى كل شيء.
بناء منتج عربي أولاً يعني كشف هذه الافتراضات مبكراً ومعالجتها في المعمار لا في طبقة العرض. الفارق في الكلفة بين المعالجتين يُقاس بإعادة كتابة كاملة.
