تطوير الواجهات والجوال Frontend & Mobile الأدلة التقنية

CSS للعربية واتجاه RTL: الخصائص المنطقية Logical Properties

كيف تكتب ملف CSS واحداً يعمل بالعربية والإنجليزية، ولماذا تخطئ margin-left في الصفحة العربية، مع قياسات حقيقية من Chrome.

CSS للعربية واتجاه RTL: الخصائص المنطقية Logical Properties

لنفرض أنك في فريق صغير يبني متجراً إلكترونياً لسماعات الرأس وأجهزة الصوت، وكانت الواجهة الأولى بالإنجليزية فقط لأن أول العملاء كانوا من الخارج، ثم قررت الإدارة إطلاق النسخة العربية خلال أسبوعين. المطور المسؤول أضاف dir="rtl" إلى الصفحة، وترجم النصوص، ورأى أن القائمة العلوية انقلبت وحدها فاطمأن، ثم وصل تقرير المصممة في اليوم التالي بعشرين ملاحظة: الأيقونة ملتصقة بالنص في زر «أضف إلى السلة»، والخط البرتقالي الذي يميز البطاقة ما زال على اليسار، والصورة المصغرة في الجهة الخطأ، وآخر سطر في الوصف معلق على اليسار، وسهم «المنتج التالي» يشير إلى الخلف، والعنوان الكبير مقصوص من الأسفل في الجوال، والنص يظهر لثانية بخط النظام ثم يقفز إلى الخط العربي. وكل هذه الملاحظات لها سبب واحد تقريباً، وهو أن ملف CSS كتب بلغة اليسار واليمين، بينما المتصفح يفكر في بداية السطر ونهايته.

وهذا المقال من مرحلة هندسة الواجهات وتطبيقات الويب Frontend، وسوف نأخذ فيه نفس البطاقة ونفس الصفحة ونعرضها في Chrome باتجاهين، ونقيس كل خطأ بالبكسل قبل إصلاحه وبعده. أما ترتيب الكلمات داخل السطر نفسه، أي خوارزمية الاتجاه الثنائي BiDi وعزل الأسماء والأرقام وتنسيق التواريخ، فله مقال خاص في نفس المرحلة، وهنا نركز على التخطيط Layout والخطوط. وسوف نناقش في هذا المقال ما يلي:

  • كيف يضع المتصفح العناصر: نموذج الصندوق والتدفق العادي، ومحورا السطر والكتل اللذان تبنى عليهما الخصائص المنطقية.
  • ماذا تفعل الخصائص الفيزيائية مثل margin-left وborder-left وfloat: left في صفحة عربية، وكيف تستبدلها بالخصائص المنطقية Logical Properties.
  • أين تضع الاتجاه: السمة dir في HTML أم الخاصية direction في CSS، ولماذا يهم الفرق.
  • Flexbox وGrid في الصفحة العربية، وما يحدث إذا قلبت الترتيب يدوياً بـ row-reverse، والتصميم المتجاوب بـ auto-fit وminmax.
  • الأيقونات: أيها تقلب وأيها تبقى كما هي.
  • الخطوط العربية: سلسلة البدائل Fallback، وارتفاع السطر، وletter-spacing، وfont-display.
  • تجربة عملية كاملة في Chrome مع القياسات الحقيقية، ثم الأخطاء الشائعة وقائمة مراجعة قبل الإطلاق.

كيف يضع المتصفح العناصر في الصفحة؟

قبل أن نتحدث عن اليمين واليسار نحتاج إلى فكرتين بسيطتين، لأن كل ما يأتي بعدهما مبني عليهما. الفكرة الأولى هي نموذج الصندوق Box Model، فكل عنصر في الصفحة يرسمه المتصفح كصندوق مستطيل له أربع طبقات من الداخل إلى الخارج: المحتوى Content، ثم الحشو Padding، ثم الحد Border، ثم الهامش Margin، وكل طبقة من الثلاث الأخيرة لها أربعة جوانب يمكن أن تختلف قيمها. وقد اعتدنا منذ بدايات CSS أن نسمي هذه الجوانب بأسماء الشاشة: الأعلى top والأسفل bottom واليسار left واليمين right، فنكتب padding-left وborder-right، وهذه هي الخصائص الفيزيائية Physical Properties، أي أنها مربوطة بجوانب الشاشة مهما كانت لغة الصفحة. ولا تنس أن box-sizing: border-box يجعل العرض الذي تكتبه يشمل الحشو والحد، وهو ما نستخدمه في كل أمثلة المقال لأنه يجعل الحساب أسهل، وتفاصيل النموذج كاملة في شرح MDN لنموذج الصندوق.

والفكرة الثانية هي التدفق العادي Normal Flow، وهو الطريقة التي يرتب بها المتصفح العناصر إذا لم تطلب منه شيئاً خاصاً، فالعناصر الكتلية Block مثل div وp وh3 تأتي واحدة تحت الأخرى، وكل منها يأخذ عرض الأب كاملاً، والعناصر السطرية Inline مثل span وa تأتي متتالية داخل السطر كما تأتي الكلمات، وإذا امتلأ السطر انتقلت إلى السطر التالي. ولاحظ أنني لم أقل إن العناصر السطرية تأتي من اليسار إلى اليمين، وهذا مقصود، فالتدفق العادي في شرح MDN للتدفق يبدأ من بداية السطر، وبداية السطر تحددها لغة الصفحة: في الإنجليزية على اليسار، وفي العربية على اليمين. ولذلك عندما أضاف مطور المتجر dir="rtl" انقلب النص والقائمة وحدهما دون أي سطر CSS، لأنهما يتبعان التدفق، ولم ينقلب ما كتب بأسماء الشاشة.

وإذا أردت أن تعرف ما يحدث داخل المتصفح بعد ذلك، أي كيف تتحول هذه الصناديق إلى شجرة ثم تحسب أماكنها Layout ثم ترسم Paint، فهذا موضوع مقال المتصفح من الداخل في نفس المرحلة، وهنا يكفينا أن نعرف أن المتصفح يحسب مكان كل صندوق من اتجاه الكتابة أولاً، ثم يحول النتيجة إلى إحداثيات على الشاشة.

اتجاه السطر واتجاه الكتل: inline وblock

المواصفة التي تعرف هذا كله اسمها CSS Writing Modes، وهي تتعامل مع الصفحة بمحورين بدلاً من أربع جهات. المحور الأول هو محور السطر Inline Axis، وهو الاتجاه الذي تسير فيه الكلمات داخل السطر، والمحور الثاني هو محور الكتل Block Axis، وهو الاتجاه الذي تتراكم فيه الفقرات والعناوين واحدة بعد الأخرى. وفي العربية والإنجليزية محور الكتل من الأعلى إلى الأسفل في الحالتين، أما محور السطر فمن اليمين إلى اليسار في العربية ومن اليسار إلى اليمين في الإنجليزية، وفي اليابانية العمودية مثلاً يصبح محور السطر من الأعلى إلى الأسفل ومحور الكتل من اليمين إلى اليسار، ولهذا اختارت المواصفة أسماء لا تذكر الشاشة أصلاً.

ولكل محور بداية ونهاية، فتصبح الجهات الأربع: بداية السطر inline-start ونهايته inline-end، وبداية الكتل block-start ونهايتها block-end. والصورة التالية تبين نفس الصندوق في صفحة إنجليزية وصفحة عربية:

مربعان متجاوران، الأيمن في صفحة إنجليزية والأيسر في صفحة عربية، حول كل منهما أسماء الجهات الفيزيائية top وbottom وleft وright بالرمادي والمنطقية block-start وblock-end وinline-start وinline-end بالبرتقالي، وخط برتقالي سميك على جهة inline-start يقع على اليسار في الإنجليزية وعلى اليمين في العربية، وسهم أزرق لاتجاه السطر وسهم أخضر لاتجاه الكتل
الجهة اليسرى تبقى يساراً في الصفحتين، أما بداية السطر فتنتقل مع لغة الصفحة

لاحظ في الصورة أن block-start هو الأعلى في الحالتين، ولذلك لن تجد فرقاً بين margin-top وmargin-block-start في المواقع العربية والإنجليزية، والفرق كله في المحور الأفقي، وهذا يعني عملياً أن مشكلة الواجهة العربية محصورة في كل خاصية فيها كلمة left أو right، وهي التي سوف نتتبعها في البطاقة التالية.

ماذا يحدث للبطاقة عندما تقلب الصفحة؟

لنأخذ بطاقة المنتج من المتجر كما كتبها الفريق للنسخة الإنجليزية، وفيها ما تجده في أي بطاقة حقيقية: خط برتقالي على جهة البداية يميز المنتجات المختارة، وزر إغلاق في الزاوية، وصورة مصغرة يلتف حولها النص، وزر شراء فيه أيقونة تفصلها عن النص مسافة، ورابط «المنتج التالي» مع سهم، وزران متلاصقان للتصفية زواياهما الخارجية مستديرة. والـ CSS الخاص بها كتب بالخصائص الفيزيائية كما يلي:

.card { position: relative; border-left: 4px solid #E95420; padding: 16px 48px 16px 16px; text-align: left; }
.card .close { position: absolute; top: 10px; right: 10px; }
.card .thumb { float: left; margin-right: 14px; }
.btn .icon { margin-right: 8px; }
.seg button:first-child { border-top-left-radius: 10px; border-bottom-left-radius: 10px; }
.seg button:last-child { border-top-right-radius: 10px; border-bottom-right-radius: 10px; }

ثم عرضنا نفس البطاقة في Chrome مرتين، مرة في صفحة إنجليزية ومرة في صفحة عربية، والفرق الوحيد بين الصفحتين هو السمة dir والنصوص المترجمة، والصورة التالية لقطة حقيقية من المتصفح:

لقطة من Chrome لبطاقة المنتج بالخصائص الفيزيائية: على اليمين النسخة العربية والخط البرتقالي ما زال على يسارها والصورة المصغرة على اليسار وآخر سطر من الوصف على اليسار والأيقونة ملتصقة بنص زر أضف إلى السلة وسهم المنتج التالي يشير إلى اليمين، وعلى اليسار النسخة الإنجليزية سليمة
نفس CSS في الصفحتين: الإنجليزية سليمة، والعربية فيها ستة أخطاء دون أي رسالة خطأ

ولم نكتف بالنظر، فقد قسنا كل عنصر بالدالتين getBoundingClientRect وgetComputedStyle كما سوف ترى في التجربة العملية، والنتيجة في الصفحة العربية كما يلي:

  • الأيقونة ملتصقة بالنص: المسافة بين الأيقونة والنص في الإنجليزية 8 بكسلات، وفي العربية صفر، والسبب أن الأيقونة أصبحت على يمين النص لأنها أول عنصر في السطر، أما الهامش فبقي على يمينها، أي على الجهة الخارجية للزر بدلاً من الجهة التي تواجه النص.
  • الخط البرتقالي على الجهة الخطأ: الحد الأيسر بقي 4 بكسلات والأيمن صفراً، والمصمم أراده على بداية البطاقة، وبداية البطاقة العربية على اليمين.
  • الصورة المصغرة على اليسار: لأن float: left يضعها على اليسار دائماً، فيبدأ القارئ العربي بالنص ثم يجد الصورة في نهاية السطر.
  • آخر سطر في الوصف معلق على اليسار: الخاصية text-align قيمتها left، فالأسطر الممتلئة تبدو سليمة، والسطر الأخير القصير يلتصق باليسار، وهذا من أكثر الأخطاء التي تفضح أن الصفحة مترجمة.
  • الزوايا المستديرة في الداخل: زر «الكل» أصبح أول زر على اليمين، ولكن زاويته المستديرة ما زالت على يساره، أي في المنتصف بين الزرين.
  • السهم يشير إلى الخلف: سهم «المنتج التالي» يشير إلى اليمين، وفي الصفحة العربية التالي على اليسار.

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

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

الخصائص المنطقية Logical Properties: margin-inline-start وأخواتها

الخصائص المنطقية تعرفها مواصفة CSS Logical Properties، وهي نسخة من كل خاصية فيزيائية تقريباً ولكن بأسماء المحورين بدلاً من جهات الشاشة، فبدلاً من أن تقول للمتصفح «ضع الهامش على يمين الأيقونة» تقول له «ضع الهامش في نهاية الأيقونة»، ويقرر المتصفح أين النهاية حسب لغة الصفحة. والجدول التالي يبين الخصائص التي تحتاجها في أغلب المشاريع:

الفيزيائيةالمنطقيةفي الصفحة العربية تساوي
margin-leftmargin-inline-startmargin-right
margin-rightmargin-inline-endmargin-left
padding-left وpadding-right معاًpadding-inline: 16px 48pxالبداية 16 على اليمين والنهاية 48 على اليسار
border-leftborder-inline-startborder-right
left وright وtopinset-inline-start وinset-inline-end وinset-block-startright وleft وtop
border-top-left-radiusborder-start-start-radiusborder-top-right-radius
float: leftfloat: inline-startfloat: right
text-align: lefttext-align: starttext-align: right
width وheightinline-size وblock-sizeنفس القيم في العربية والإنجليزية

وبهذا الجدول أعاد الفريق كتابة البطاقة، ولاحظ أن عدد الأسطر لم يتغير، وأن الملف واحد للغتين:

.card { position: relative; border-inline-start: 4px solid #E95420; padding-block: 16px; padding-inline: 16px 48px; text-align: start; }
.card .close { position: absolute; inset-block-start: 10px; inset-inline-end: 10px; }
.card .thumb { float: inline-start; margin-inline-end: 14px; }
.btn .icon { margin-inline-end: 8px; }
.seg button:first-child { border-start-start-radius: 10px; border-end-start-radius: 10px; }
.seg button:last-child { border-start-end-radius: 10px; border-end-end-radius: 10px; }
.chev:dir(rtl) { transform: scaleX(-1); }

والصورة التالية نفس البطاقة بعد الإصلاح، في نفس النافذة ونفس المتصفح:

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

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

الاختصارات: padding-inline وinset وزوايا الحدود

أكثر ما يربك عند البداية هو ترتيب القيم في الاختصارات، فالخاصية padding-inline: 16px 48px تعني البداية ثم النهاية، أي 16 في البداية و48 في النهاية، وإذا كتبت قيمة واحدة طبقت على الجهتين، وpadding-block بنفس الطريقة للأعلى والأسفل. أما الاختصار القديم padding: 16px 48px 16px 16px فيبقى فيزيائياً، أي أعلى ثم يمين ثم أسفل ثم يسار، ولا توجد طريقة في CSS اليوم لتجعل هذا الاختصار ذا الأربع قيم منطقياً، ولذلك نقسمه إلى سطرين: padding-block وpadding-inline. والخاصية margin مثلها تماماً.

وفي العناصر ذات الموضع المطلق Absolute Position نستخدم inset-inline-start وinset-inline-end بدلاً من left وright، كما في صفحة MDN للخاصية inset-inline-end، وهي التي نقلت زر الإغلاق إلى اليسار في الصفحة العربية. أما الزوايا فأسماؤها أطول قليلاً لأن كل زاوية تقع بين محورين، فالاسم يذكر جهة محور الكتل أولاً ثم جهة محور السطر: border-start-start-radius هي زاوية بداية الكتل وبداية السطر، أي الأعلى يميناً في العربية، وborder-end-start-radius هي الأسفل يميناً، والتفاصيل في صفحة MDN للخاصية border-start-start-radius. ولتتذكرها بسهولة: الكلمة الأولى للأعلى والأسفل، والثانية لبداية السطر ونهايته.

float وtext-align: القيم المنطقية

بعض الخصائص لم تحتج إلى اسم جديد وإنما إلى قيم جديدة، فالخاصية float أصبحت تقبل inline-start وinline-end كما في صفحة MDN للخاصية float، وكذلك clear. والخاصية text-align تقبل start وend منذ زمن بعيد، بل إن قيمتها الافتراضية في المتصفحات الحديثة هي start، وهذا يعني أن أفضل إصلاح لكثير من الصفحات هو حذف text-align: left وليس استبداله، لأن المطور كتبه أصلاً ليؤكد القيمة الافتراضية في الصفحة الإنجليزية. ولاحظ في القياس أن getComputedStyle أعاد القيمة start كما هي ولم يحولها إلى right، فإذا كان عندك اختبار آلي يتحقق من المحاذاة فاجعله يتحقق من موضع النص بالبكسل وليس من قيمة الخاصية.

أين تضع الاتجاه: dir في HTML أم direction في CSS؟

هناك طريقتان لتقول للمتصفح إن الصفحة عربية من اليمين إلى اليسار: السمة dir="rtl" على عنصر html، أو الخاصية direction: rtl في CSS. والطريقتان تعطيان نفس النتيجة في التخطيط في أغلب الحالات، ولكن مجموعة التدويل في W3C تقول صراحة في صفحة الإجابة عن سمة dir: إذا كان اتجاه المستند كله من اليمين إلى اليسار فضع dir="rtl" على عنصر html، ولا تستخدم CSS أبداً لتحديد الاتجاه الأساسي، واستخدم القيم المنطقية start وend في الخصائص. وكذلك تقول صفحة MDN للخاصية direction إن الاتجاه يحدد عادة داخل المستند بسمة dir وليس بهذه الخاصية مباشرة.

والسبب الأول أن الاتجاه جزء من معنى المحتوى وليس من شكله، فالنص العربي يبقى عربياً إذا فشل تحميل ملف CSS، أو إذا قرأته أداة لا تفهم CSS مثل قارئ الشاشة أو محرك البحث أو برنامج البريد. والسبب الثاني ظهر لنا في التجربة نفسها، فقد صنعنا صفحة فيها body { direction: rtl; } دون أي سمة dir، ووضعنا فيها نفس سهم «المنتج التالي» مع القاعدة .chev:dir(rtl) { transform: scaleX(-1); }، والنتيجة كما يلي:

-- /nodir: html dir="", body direction rtl, chevron matches :dir(rtl) false, transform none
-- /card?dir=rtl&css=logical: html dir="rtl", body direction rtl, chevron matches :dir(rtl) true, transform matrix(-1, 0, 0, 1, 0, 0)

الصفحة الأولى اتجاهها في CSS من اليمين إلى اليسار، والنص على اليمين، ولكن المحدد :dir(rtl) لم يطابق السهم، فبقي السهم دون قلب، والسبب أن المحدد :dir يقرأ الاتجاه من سمة dir في HTML وليس من الخاصية direction في CSS. أما الصفحة الثانية التي فيها dir="rtl" فطابق المحدد وظهرت مصفوفة القلب matrix(-1, 0, 0, 1, 0, 0). أي أن من يضع الاتجاه في CSS يخسر بصمت كل قاعدة مبنية على :dir()، ولن يرى أي خطأ إلا عندما ينتبه أحد للسهم.

وفي الصفحة العربية نفسها تحتاج أحياناً إلى عنصر اتجاهه إنجليزي، مثل حقل البريد الإلكتروني ورقم الهاتف وكتلة الكود، وهنا تضع dir="ltr" على ذلك العنصر وحده، وأما النصوص التي تأتي من المستخدم ولا تعرف لغتها مسبقاً، مثل اسم المنتج الذي يكتبه البائع، فتحتاج إلى dir="auto" أو عنصر bdi، وهذه هي منطقة خوارزمية الاتجاه الثنائي التي شرحناها في مقالها الخاص، وقد ذكرنا طرفاً منها أيضاً في أسئلة مقابلة مطور Frontend. ولا تنس السمة lang="ar" بجانب dir، فهي التي يعتمد عليها المتصفح في اختيار الخط المناسب وفي النطق في قارئ الشاشة، وغيابها يجعل المتصفح يخمن.

Flexbox: المحور الرئيسي Main Axis يتبع الاتجاه

الخبر الجيد أن Flexbox وGrid كتبا من البداية بلغة المحاور، فلا تحتاج فيهما إلى خصائص منطقية جديدة. في Flexbox كل حاوية لها محور رئيسي Main Axis تصطف عليه العناصر، ومحور عرضي Cross Axis عمودي عليه، وعند flex-direction: row يكون المحور الرئيسي هو محور السطر، فيبدأ من بداية السطر، أي من اليمين في الصفحة العربية، كما تشرح صفحة MDN لمفاهيم Flexbox. وكذلك الخصائص justify-content: flex-start وalign-items وgap، فكلها مبنية على البداية والنهاية.

وفي المتجر شريط علوي فيه الشعار ثم روابط التصنيفات ثم زر السلة، والسلة مدفوعة إلى نهاية الشريط بالقاعدة margin-inline-start: auto، وهي طريقة شائعة في Flexbox لدفع عنصر إلى الطرف الآخر، والصورة التالية تبين ما حدث للمحور في كل حالة، مع الإحداثيات التي قسناها في نافذة عرضها 1000 بكسل:

ثلاثة صفوف من المربعات المرقمة 1 و2 و3: في الصفحة الإنجليزية مع row تبدأ من اليسار، وفي العربية مع row تبدأ من اليمين، وفي العربية مع row-reverse تعود لتبدأ من اليسار بسهم أحمر، وتحتها صف Grid فيه aside على اليمين وmain على اليسار في الصفحة العربية
في Flexbox وGrid لا تكتب شيئاً للعربية: الترتيب ينقلب وحده مع dir

والصورة التالية لقطة حقيقية من Chrome للصفحة العربية كاملة، وفيها الشريط العلوي وشبكة المنتجات والعمود الجانبي:

لقطة من Chrome للصفحة العربية: شريط علوي داكن فيه متجر الصوت والسماعات والعروض على اليمين وزر السلة البرتقالي على اليسار، وتحته عمود التصنيفات على اليمين وشبكة من خمسة منتجات تبدأ من اليمين بثلاثة أعمدة
الشريط والعمود الجانبي والشبكة انقلبت دون أي سطر CSS خاص بالعربية

ماذا يحدث إذا قلبت الترتيب يدوياً بـ row-reverse؟

هذا خطأ شائع في المشاريع التي تترجم واجهتها بعد بنائها، فالمطور الذي تعود على الخصائص الفيزيائية يظن أن الصفحة العربية تحتاج إلى قلب كل شيء يدوياً، فيكتب في ملف العربية flex-direction: row-reverse للشريط العلوي، والنتيجة أن الصف ينقلب مرتين: مرة بسبب dir="rtl" ومرة بسبب row-reverse، فيعود الترتيب الإنجليزي. وقد قسنا ذلك في التجربة فكان الشعار عند x=36، أي على اليسار، والسلة عند x=268 بجانب الروابط بدلاً من الطرف الآخر:

لقطة من Chrome للشريط العلوي في الصفحة العربية مع row-reverse: متجر الصوت على اليسار ثم العروض والسماعات ثم زر السلة في المنتصف تقريباً، والجهة اليمنى من الشريط فارغة
row-reverse فوق dir="rtl" أعاد الترتيب الإنجليزي، وترك الجهة اليمنى فارغة

وسبب مكان السلة الغريب أن row-reverse يجعل بداية المحور الرئيسي على اليسار، ولكن margin-inline-start: auto ما زال مربوطاً بالسطر وليس بالمحور، وبداية السطر العربي على اليمين، فوضع المتصفح الهامش التلقائي على يمين السلة، فدفعها نحو بقية العناصر بدلاً من دفعها بعيداً عنها. وهذا مثال واضح على أن خلط القلب اليدوي مع القلب التلقائي يعطيك نتائج لا يتوقعها أحد، والقاعدة: لا تستخدم row-reverse وorder لتغيير الاتجاه، والسبب أنهما يغيران الترتيب البصري فقط ويتركان ترتيب الكود كما هو، فيصبح ترتيب التنقل بزر Tab مختلفاً عن ترتيب العين، وهذا يضر مستخدم لوحة المفاتيح وقارئ الشاشة كما شرحنا في مقال سهولة الوصول. استخدمهما فقط عندما يكون الترتيب المختلف مقصوداً في اللغتين معاً.

Grid: المسارات Tracks ووحدة fr والمناطق Areas

في Grid تقسم الحاوية إلى مسارات Tracks، أي أعمدة وصفوف، ثم تضع العناصر فيها، وتشرح صفحة MDN لمفاهيم Grid كيف تحسب المسارات. والقاعدة التي تهمنا هنا أن العمود الأول هو عمود البداية، فالقاعدة grid-template-columns: 200px 1fr تعني عموداً بعرض 200 بكسل في بداية السطر، ثم عموداً يأخذ ما بقي من المساحة، ووحدة fr تعني جزءاً من المساحة الحرة بعد طرح الأعمدة الثابتة والفجوات. وفي القياس كان العمود الجانبي في الصفحة الإنجليزية عند x=20 وفي العربية عند x=780، أي أنه انتقل من اليسار إلى اليمين دون أن نكتب شيئاً.

والأجمل من ترقيم الأعمدة هو المناطق المسماة Named Areas، فبدلاً من أن تقول «العمود الأول» تعطي كل منطقة اسماً، وترسم الصفحة كأنها خريطة نصية:

.page { display: grid; grid-template-columns: 200px 1fr; grid-template-areas: "side main"; gap: 16px; }
aside { grid-area: side; }
main { grid-area: main; }
@media (max-width: 600px) {
  .page { grid-template-columns: 1fr; grid-template-areas: "main" "side"; }
}

لاحظ أن النص "side main" يقرأ من بداية السطر، فالمنطقة side في البداية، وهي على اليمين في العربية. وقد يتساءل البعض: إذا كانت الخريطة تكتب بأحرف إنجليزية من اليسار إلى اليمين، فلماذا لا يضعها المتصفح على اليسار؟ والإجابة أن الخاصية grid-template-areas تعرف الأعمدة بترتيبها، والترتيب يبدأ من بداية السطر مثل أي شيء آخر في Grid، فالخريطة تصف ترتيب الأعمدة وليس مكانها على الشاشة. ولاحظ أيضاً أن الخصائص justify-items وjustify-self في Grid تعمل على محور السطر، فالقيمة start فيها تعني اليمين في الصفحة العربية.

repeat(auto-fit, minmax()) والتصميم المتجاوب Responsive

شبكة المنتجات في المتجر لا تحدد عدد الأعمدة أصلاً، وإنما تقول للمتصفح: ضع أكبر عدد من الأعمدة التي لا يقل عرض كل منها عن 180 بكسلاً، ووزع الباقي عليها بالتساوي:

.list { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 12px; }

والدالة repeat مع auto-fit تحسب عدد الأعمدة من العرض المتاح، وminmax(180px, 1fr) تقول إن العمود لا يقل عن 180 ولا يزيد عن جزء من المساحة الحرة. وفي نافذة عرضها 1000 بكسل حسب Chrome ثلاثة أعمدة عرض كل منها 240 بكسلاً، وفي نافذة عرضها 390 بكسلاً، وهو عرض جوال شائع، أصبح عموداً واحداً بعرض 350 بكسلاً، ومع قاعدة @media انتقلت المنطقة main فوق side، والقياس أكد ذلك بالقيمة main above aside: true:

لقطة من Chrome للصفحة العربية بعرض 390 بكسلاً: الشريط العلوي فيه متجر الصوت على اليمين والسلة على اليسار والروابط مخفية، ثم المنتجات من 1 إلى 5 في عمود واحد، ثم التصنيفات في الأسفل
في الجوال: عمود واحد، والمنتجات قبل التصنيفات، والترتيب العربي محفوظ

والدرس العربي في التصميم المتجاوب أن النص العربي يحتاج غالباً إلى عرض مختلف عن الإنجليزي، فالكلمات العربية أقصر في بعض الحالات وأطول في غيرها، والحروف تحتاج إلى ارتفاع أكبر كما سوف نرى، ولذلك لا تثبت عرض الأزرار والبطاقات بالبكسل بناءً على النص الإنجليزي، واجعل المحتوى يحدد العرض بـ min-width وminmax، واختبر كل نقطة توقف Breakpoint باللغتين. وفي المتجر مثلاً كان زر «أضف إلى السلة» أعرض قليلاً من «Add to cart» في اللقطات السابقة، وعنوان المنتج احتاج إلى سطرين في اللغتين ولكن انقسم عند كلمات مختلفة، فلا توجد قاعدة ثابتة غير الاختبار.

ما الذي لا ينقلب وحده؟

بعد ترحيل الخصائص المنطقية وFlexbox وGrid يبقى عدد من الأشياء التي لا تعرف شيئاً عن اتجاه الصفحة، لأنها مبنية على إحداثيات الشاشة بطبيعتها، وهي التي تظهر منها أخطاء المرحلة الثانية:

  • الإزاحة بـ transform: الدالة translateX(100%) تحرك العنصر إلى اليمين دائماً، فالقائمة الجانبية التي تنزلق من اليسار في الإنجليزية تنزلق من الجهة الخطأ في العربية. والحل أن تضرب الإزاحة في متغير يتغير مع الاتجاه، مثل --dir: 1 في الإنجليزية و-1 تحت :dir(rtl)، ثم translateX(calc(100% * var(--dir))).
  • ظل الصندوق box-shadow: الإزاحة الأفقية فيه فيزيائية، فالظل الذي يقع على يمين البطاقة يبقى على يمينها، وهذا مقبول غالباً لأن الضوء في الواجهة لا علاقة له بلغة القراءة.
  • موضع الخلفية background-position: صورة الخلفية في left center تبقى على اليسار، فإذا كانت أيقونة داخل حقل البحث فانقلها بـ :dir(rtl) أو استبدلها بعنصر حقيقي داخل Flexbox.
  • الحركات Animations والقيم في JavaScript: أي كود يحسب style.left أو يقرأ event.clientX ليعرف الاتجاه يجب أن يقرأ اتجاه الصفحة من document.dir أو من getComputedStyle(el).direction، وإلا فسوف يعمل في لغة واحدة فقط.

الأيقونات: أيها تقلب وأيها تبقى؟

في تقرير المصممة كانت هناك ملاحظة عن سهم «المنتج التالي»، وملاحظة معاكسة لم تكن متوقعة: أحد المطورين قلب كل أيقونات الصفحة بـ scaleX(-1) في النسخة العربية، فأصبحت عدسة البحث مقلوبة وعلامة الصح مقلوبة وأيقونة الساعة تدور عكس عقارب الساعة. والقاعدة التي تحكم ذلك ليست في CSS وإنما في معنى الأيقونة، وأوضح مرجع لها هو إرشادات Apple للواجهات من اليمين إلى اليسار، وخلاصتها في الصورة التالية:

جدول من عمودين: تقلب، وفيه أسهم السابق والتالي وسهم القائمة الفرعية وشريط التقدم وأيقونة محاذاة النص، ولا تقلب، وفيه العدسة والتنزيل وعلامة الصح والرقم 2026، وبجانب كل أيقونة شكلها في الصفحة الإنجليزية ثم العربية
تقلب الأيقونة إذا كان معناها مرتبطاً باتجاه القراءة، وتبقى إذا كانت شيئاً حقيقياً أو تشير إلى اتجاه حقيقي

ونفصلها في نقاط:

  • تقلب أيقونات الحركة للأمام والخلف: السابق والتالي وسهم الرجوع وسهم القائمة الفرعية، لأن «التالي» في العربية على اليسار.
  • تقلب أيقونات النص والقراءة: محاذاة الفقرة والمسافة البادئة Indent والقائمة المرقمة، لأنها صورة لنص يقرأ من اتجاه معين.
  • تقلب أدوات التقدم: شريط التقدم Progress والمنزلق Slider وأداة التقييم بالنجوم تمتلئ من بداية السطر، وتقول Apple إن الأرقام فيها ينقلب ترتيبها ولكن لا تقلب الأرقام نفسها.
  • لا تقلب الأشياء الحقيقية: العدسة والساعة والكاميرا والهاتف، فالساعة تدور بنفس الاتجاه في كل لغة كما تقول Apple.
  • لا تقلب الاتجاه الحقيقي: سهم التنزيل يشير إلى الأسفل في كل اللغات، وسهم يشير إلى منطقة معينة في الشاشة يبقى يشير إليها.
  • لا تقلب الشعارات والعلامات العالمية: علامة الصح وشعارات الشركات، وتنبه Apple إلى أن الشعار المقلوب يربك المستخدم وقد تكون له تبعات قانونية.

وفي الكود نضع على الأيقونات التي تقلب فقط صنفاً خاصاً، ونقلبها بالمحدد :dir(rtl) كما في البطاقة: .chev:dir(rtl) { transform: scaleX(-1); }، وفي القياس كانت قيمة transform لسهم «المنتج التالي» في الصفحة العربية matrix(-1, 0, 0, 1, 0, 0)، وفي الإنجليزية none، بينما بقيت عدسة البحث كما هي في الحالتين. ولاحظ أن القلب بـ scaleX(-1) يقلب الأيقونة حول مركزها، فإذا كانت الأيقونة غير متماثلة في المساحة الفارغة حولها فقد تحتاج إلى نسخة مرسومة خاصة للعربية، وهذا ما تقصده Apple عندما تقول إن الأيقونة المعقدة تحتاج إلى نظر في مكوناتها قبل قلبها.

الخطوط العربية: الاختيار وسلسلة البدائل Fallback

بعد أن استقام التخطيط بقيت ملاحظات المصممة عن الخط، وأولها أن عنوان المنتج في بعض الصفحات يظهر بخط يختلف عن بقية الصفحة. والسبب كان في سلسلة الخطوط font-family، فالفريق اختار للنسخة الإنجليزية خطاً لاتينياً جميلاً اسمه Manrope، وكتب font-family: "Manrope", sans-serif، وهذا الخط لا يحتوي على أي حرف عربي. فماذا يفعل المتصفح عندما يجد حرفاً عربياً في عنصر خطه Manrope؟ يبحث في بقية السلسلة عن خط فيه هذا الحرف، فإذا لم يجد أخذ خطاً من خطوط النظام، وهذا يتم لكل حرف على حدة.

وقد استخدمنا في التجربة الأمر CSS.getPlatformFontsForNode من بروتوكول DevTools، وهو الأمر الذي يقول لك أي خط استخدمه Chrome فعلاً لكل عنصر، وهو نفس ما تراه في لوحة Computed في DevTools تحت عنوان Rendered Fonts، والنتيجة كما يلي:

   fonts used by #ls0: IBM Plex Sans Arabic (31 glyphs, web font)
   fonts used by #fb1: Manrope ExtraLight (10 glyphs, web font), Arial (28 glyphs)
   fonts used by #fb2: IBM Plex Sans Arabic (27 glyphs, web font), Manrope ExtraLight (10 glyphs, web font)

في السطر الثاني رسم Chrome الكلمة اللاتينية بخط Manrope، ثم رسم الحروف العربية الثمانية والعشرين بخط Arial من النظام، لأن السلسلة ليس فيها خط عربي، وArial على Windows فيه حروف عربية. وعلى جهاز Mac سوف يختار خطاً آخر، وعلى أندرويد خطاً ثالثاً، أي أن صفحتك العربية سوف تظهر بثلاثة أشكال مختلفة دون أن يقرر أحد ذلك. وفي السطر الثالث أضفنا الخط العربي بعد Manrope مباشرة: font-family: "Manrope", "Plex Arabic", sans-serif، فرسم Chrome الحروف العربية بخط IBM Plex Sans Arabic واللاتينية بخط Manrope، وهذه هي الطريقة الصحيحة لصفحة فيها اللغتان: خط لاتيني ثم خط عربي ثم بديل عام. ولاحظ أن اسم الخط ظهر Manrope ExtraLight مع أن الكلمة مرسومة بالوزن العادي، والسبب أن الملف خط متغير Variable Font، والاسم المسجل فيه هو اسم أول نسخة فيه، فلا تعتمد على هذا الاسم لتعرف الوزن.

لقطة من Chrome لست عينات: جملة عربية بدون letter-spacing ثم بمسافة 2px ثم 0.2em والحروف متصلة في الثلاث مع اتساع المسافات بين الكلمات فقط، ثم جملة بخط Alexandria مقصوصة من الأعلى والأسفل في صندوق ارتفاعه 26 بكسلاً، ثم جملة بخط Manrope وحده يظهر فيها العربي بخط النظام، ثم بخط Manrope مع Plex Arabic يظهر فيها العربي بالخط الصحيح
من الأعلى: letter-spacing لا يفصل الحروف في Chrome، وline-height: 1 يقص الحروف، والخط العربي الغائب من السلسلة يستبدله النظام

وعند اختيار الخط العربي نفسه انتبه إلى ثلاثة أشياء. الأول أن الخطوط العربية الجيدة تأتي بأوزان كثيرة، فاحمل فقط الأوزان التي تستخدمها، لأن كل وزن ملف مستقل قد يكون حجمه مئات الكيلوبايتات، وقد تحتاج إلى تقسيمه بالخاصية unicode-range حتى لا يحمل الزائر الإنجليزي حروفاً عربية لن يراها. والثاني أن الخطين العربي واللاتيني في نفس الصفحة يجب أن يتوازنا بصرياً، وهذا ما تقصده Apple عندما تقول إنك قد تحتاج إلى موازنة الخطين المتجاورين، فبعض الخطوط العربية تبدو أصغر من اللاتينية بنفس الحجم، وقد تحتاج إلى حجم أكبر قليلاً للعربية. والثالث أن الخط يحمل من سيرفرك أو من شبكة توصيل المحتوى CDN، فاقرأ عن التخزين المؤقت للملفات الثابتة في مقال CDN، لأن ملف الخط الذي يحمل في كل زيارة يبطئ ظهور النص كما سوف نرى في قسم font-display. وتجد توصيات عامة عن تحميل الخطوط في دليل web.dev لأفضل ممارسات الخطوط.

ارتفاع السطر Line Height للحروف العربية

الملاحظة الثانية كانت عن العنوان المقصوص في الجوال، وقد وجدنا السبب في مكون يعرض عنوان المنتج في سطر واحد، كتبه المطور هكذا: height: 26px; line-height: 1; overflow: hidden، والفكرة أن يقطع العنوان الطويل بدلاً من أن يفسد شكل البطاقة، وهي فكرة تعمل بالإنجليزية لأن الحروف اللاتينية لا تنزل كثيراً تحت خط القاعدة ولا تصعد كثيراً فوقه. أما العربية ففيها الهمزة تحت الألف في «إلى»، والياء والجيم والنون التي تنزل تحت الخط، والهمزة فوق الألف في «التأمين»، وكلها تحتاج إلى مساحة. والصورة التالية مبنية على القياس الحقيقي في Chrome لخط Alexandria بحجم 26 بكسلاً:

عبارة إلى جانب التأمين مكبرة مع خطوط أفقية: حبر الحروف من 26 فوق خط القاعدة إلى 9 تحته، وصندوق الخط من 25 فوق إلى 7 تحت، وحد line-height: 1 من 22 فوق إلى 4 تحت، فتخرج همزة التأمين من الأعلى وهمزة إلى والنون والياء من الأسفل
line-height: 1 أضيق من صندوق الخط العربي، فيقص overflow: hidden ما خرج منه

والأرقام كما طبعتها التجربة: صندوق الخط Font Box، أي المساحة التي يعلن الخط أنه يحتاجها، 25 بكسلاً فوق خط القاعدة و7 تحته، أي 32 بكسلاً، وهذا هو ارتفاع السطر عند line-height: normal بالضبط. أما الحبر الحقيقي للحروف، أي أعلى نقطة وأدنى نقطة مرسومة، فكان 26 فوق و9 تحت. وعند line-height: 1 يصبح السطر 26 بكسلاً، فيوزع المتصفح النقص البالغ 6 بكسلات على الأعلى والأسفل، 3 لكل جهة، فيمتد السطر من 22 فوق الخط إلى 4 تحته، ويخرج الحبر 4 بكسلات من الأعلى و5 من الأسفل، ومع overflow: hidden يقص المتصفح هذه البكسلات. وقد قاس Chrome في العنصر نفسه clientHeight = 26 وscrollHeight = 29، أي أن 3 بكسلات من صندوق الخط خرجت من أسفل الحد، ولم يحسب ما خرج من الأعلى لأن المحتوى لا يمرر إلى الأعلى أصلاً.

وللمقارنة كان صندوق خط Arial اللاتيني بنفس الحجم 24 فوق و6 تحت، والحبر الحقيقي لجملة إنجليزية 19 فوق و5 تحت، أي أن الحروف اللاتينية تبقى داخل صندوقها بفارق كبير، ولهذا لا يرى المطور المشكلة في النسخة الإنجليزية. والقاعدة العملية: اجعل line-height للنص العربي بين 1.6 و1.8 للفقرات، ولا تنزل في العناوين عن قيمة تتسع لصندوق الخط، وإذا أردت قطع النص في سطر واحد فاستخدم text-overflow: ellipsis مع white-space: nowrap دون ارتفاع ثابت، وإذا أردت عدة أسطر فاستخدم line-clamp مع line-height كاف. وفي التجربة كان السطر عند 1.7 يساوي 44.1875 بكسلاً وليس 44.2، لأن Chrome يحسب المواضع بوحدات صغيرة من البكسل ثم يقرب الناتج إليها، والقيم التي تقبلها الخاصية مشروحة في صفحة MDN للخاصية line-height، وأفضلها الرقم دون وحدة لأنه يورث كنسبة وليس كقيمة محسوبة.

letter-spacing وtext-transform مع العربية

الملاحظة الثالثة أن عناوين الأقسام في التصميم الإنجليزي مكتوبة بأحرف كبيرة ومسافات بين الحروف، أي text-transform: uppercase; letter-spacing: 0.2em، وقد انتقلت نفس القاعدة إلى العربية. والتحذير المعروف بين مطوري الواجهات العربية أن letter-spacing يفصل الحروف العربية المتصلة فتصبح الكلمة حروفاً متفرقة، ولذلك توقعنا أن نرى ذلك في التجربة، ولكن Chrome 156 أعطى نتيجة مختلفة:

   inline spans: Latin 136 -> 156px with 2px, Arabic 313 -> 321px with 2px

الكلمة الإنجليزية Typography فيها عشرة حروف، فزاد عرضها 20 بكسلاً، أي 2 بكسل بعد كل حرف كما هو متوقع. أما الجملة العربية فزادت 8 بكسلات فقط، وفي الصورة السابقة ترى أن الحروف بقيت متصلة في العينات الثلاث، وأن المسافات اتسعت بين الكلمات فقط. وهذا السلوك ليس صدفة، فمواصفة CSS Text في قسم الخطوط المتصلة تقول إن المتصفح الذي لا يستطيع توسيع النص المتصل دون فصل حروفه يجب ألا يضيف مسافات بين حروفه أبداً، وأن يعامل كل كلمة كأنها حرف واحد، وهذا ما فعله Chrome. ولكن لا تعتمد على هذا، فالسلوك مختلف بين المتصفحات وإصداراتها، والأهم أن القاعدة نفسها لا معنى لها في العربية، فالمصمم أراد بها مظهراً لاتينياً، والحل أن تلغيها للعربية بالمحدد :lang(ar) أو :dir(rtl): letter-spacing: normal.

ونفس الكلام عن text-transform: uppercase، فلا توجد أحرف كبيرة وصغيرة في العربية، فالقاعدة لا تفعل شيئاً وتبقى في الملف دون فائدة، ولكن انتبه إلى أنها تغير الكلمات اللاتينية داخل النص العربي، مثل اسم المنتج أو العلامة التجارية، فإذا كتب البائع اسم منتجه iPhone فسوف يظهر IPHONE. وإذا أردت تمييز العناوين في العربية فاستخدم الوزن Font Weight والحجم واللون بدلاً من المسافات والأحرف الكبيرة، وتنبه صفحة MDN للخاصية letter-spacing في قسم التدويل إلى أن الحروف العربية يتوقع منها أن تبقى متصلة.

font-display: متى يظهر النص؟

الملاحظة الأخيرة أن النص يظهر بخط النظام لحظة ثم يقفز إلى الخط العربي، وفي بعض الصفحات لا يظهر النص أصلاً لثوان على الاتصال البطيء. وهذا يتحكم فيه الوصف font-display داخل @font-face، وتعرف مواصفة CSS Fonts لكل خط ثلاث فترات: فترة الحجب Block Period يرسم فيها المتصفح النص غير مرئي إذا لم يصل الخط، وفترة التبديل Swap Period يرسم فيها النص بخط بديل ويبدله عند وصول الخط، ثم فترة الفشل التي يبقى فيها البديل. والقيمة block تعطي فترة حجب قصيرة توصي المواصفة بأن تكون 3 ثوان، والقيمة swap تعطي فترة حجب صغيرة جداً ثم تبديلاً دون حد.

ولنرى الفرق جعلنا سيرفر التجربة يتأخر 3 ثوان قبل أن يرسل ملف الخط، ثم صورنا الصفحة بعد ثانية واحدة من فتحها:

لقطتان من Chrome بعد ثانية من فتح الصفحة: على اليسار مع font-display: swap يظهر عنوان عروض نهاية الأسبوع بخط Tahoma البديل، وعلى اليمين مع font-display: block المساحة فارغة والعنوان غير مرئي
بعد ثانية واحدة والخط لم يصل: swap يعرض النص بالبديل، وblock يخفيه
-- block: after 1s: font loading, heading width 253px | after 4s: font loaded, heading width 244px
-- swap: after 1s: font loading, heading width 253px | after 4s: font loaded, heading width 244px

في المخرج أعلاه لاحظ أن العنوان كان له عرض 253 بكسلاً بعد ثانية في الحالتين، حتى في block التي لا يرى فيها المستخدم شيئاً، والسبب أن المتصفح يحسب مكان النص بالخط البديل ثم يرسمه شفافاً، فالمساحة محجوزة والنص غير مرئي. وبعد 4 ثوان وصل الخط فأصبح العرض 244 بكسلاً، أي أن العنوان صغر 9 بكسلات عند التبديل، وهذا هو القفز الذي لاحظته المصممة، فكل ما تحت العنوان وبجانبه يتحرك. والاختيار بينهما قرار وليس قاعدة: swap للنصوص الطويلة والمحتوى الذي يريد القارئ أن يبدأ به فوراً، مع اختيار خط بديل قريب في المقاسات من الخط العربي حتى يقل القفز، وblock للشعارات والعناوين القصيرة التي يفسد شكلها الخط البديل. وهناك قيمتان أخريان في صفحة MDN للوصف font-display: fallback التي تتوقف عن التبديل بعد فترة قصيرة، وoptional التي لا تبدل أبداً بعد أول رسم، وهي مناسبة إذا كان الخط مخزناً في الزيارات التالية.

التجربة العملية Lab: نفس البطاقة في Chrome باتجاهين

كل الأرقام والصور في هذا المقال جاءت من تجربة واحدة تتكون من ملفين في مجلد lab: سيرفر صغير بمكتبة Node.js القياسية وحدها يقدم الصفحات والخطوط، وسكربت يفتح Chrome دون واجهة Headless ويتحكم فيه عبر بروتوكول DevTools، فيفتح كل صفحة ويقيسها ويصورها.

الصفحات التي يقدمها السيرفر

السيرفر يبني كل صفحة من قالب واحد، ويغير فيها شيئين فقط حسب الرابط: السمة dir واللغة، وملف CSS الفيزيائي أو المنطقي. والجزء المهم منه هو دالة البطاقة:

const card = (dir, css) => {
    const t = TEXT[dir];
    return `<!doctype html><html lang="${t.lang}" dir="${dir}"><head><meta charset="utf-8"><title>card</title>
<style>${BASE}${css === 'logical' ? LOGICAL : PHYSICAL}</style></head><body>
<div class="card" id="card">
  <button class="close" id="close" aria-label="close">✕</button>
  <div class="thumb" id="thumb"></div>
  <h3 id="title"><span id="titletext">${t.title}</span></h3>
  <p id="body">${t.body}</p>
  <div class="row">
    <button class="btn" id="buy">${ICON_DOWN}<span id="buylabel">${t.buy}</span></button>
    <a class="link" id="next" href="#">${t.next}${ICON_CHEV}</a>
    <span class="link" id="search">${ICON_FIND}${t.search}</span>
  </div>
  <div class="seg" id="seg"><button id="seg1">${t.seg[0]}</button><button id="seg2">${t.seg[1]}</button></div>
</div></body></html>`;
};

في الكود أعلاه لاحظ التالي:

  • السمتان lang وdir على عنصر html نفسه، فالاتجاه موجود قبل أن يحمل أي CSS.
  • الثابتان PHYSICAL وLOGICAL هما نفس ملفي CSS اللذين عرضناهما في أول المقال، وBASE فيه الألوان والأحجام المشتركة، فالفرق بين الصفحتين محصور في الخصائص التي ندرسها.
  • لكل عنصر نريد قياسه id خاص، والنص داخل زر الشراء في span مستقل حتى نقيس المسافة بينه وبين الأيقونة بدقة.
  • الصفحات الأخرى في نفس الملف: /compare تعرض البطاقة باللغتين جنباً إلى جنب في إطارين iframe، و/layout الشريط والشبكة، و/type عينات الخط، و/fontload الخط المتأخر، و/nodir الصفحة التي اتجاهها من CSS فقط. والمسار /slowfont/ ينتظر 3 ثوان قبل إرسال الخط.

كيف نقيس البطاقة؟

سكربت القياس يستخدم مساعد Chrome الموجود في مستودع الموقع، ويفتح البطاقة أربع مرات: فيزيائية ومنطقية، كل منهما بالإنجليزية والعربية، ويقيس فيها ما يلي:

for (const [css, dir] of [['physical', 'ltr'], ['physical', 'rtl'], ['logical', 'ltr'], ['logical', 'rtl']]) {
    await b.goto(`${URL0}/card?dir=${dir}&css=${css}`, { wait: 600 });
    const m = await b.evaluate(`(() => {
        const R = (s) => document.querySelector(s).getBoundingClientRect(), cs = (s) => getComputedStyle(document.querySelector(s));
        const card = R('#card'), icon = R('#buy .icon'), label = R('#buylabel'), close = R('#close'), title = R('#titletext'), thumb = R('#thumb');
        const iconSide = icon.left < label.left ? 'left' : 'right';
        const gapToText = Math.round(iconSide === 'left' ? label.left - icon.right : icon.left - label.right);
        const overlap = Math.max(0, Math.min(close.right, title.right) - Math.max(close.left, title.left));
        return [
          'icon ' + iconSide + ' of text, gap to text ' + gapToText + 'px, margin L/R ' + cs('#buy .icon').marginLeft + '/' + cs('#buy .icon').marginRight,
          'close button at ' + (close.left - card.left < card.right - close.right ? 'left' : 'right') + ', overlaps title by ' + Math.round(overlap) + 'px',
          'accent border L/R ' + cs('#card').borderLeftWidth + '/' + cs('#card').borderRightWidth + ', padding L/R ' + cs('#card').paddingLeft + '/' + cs('#card').paddingRight,
          'thumb on the ' + (thumb.left - card.left < card.right - thumb.right ? 'left' : 'right') + ', text-align ' + cs('#body').textAlign,
          'segment first button radius TL/TR ' + cs('#seg1').borderTopLeftRadius + '/' + cs('#seg1').borderTopRightRadius + ', chevron transform ' + cs('#next .chev').transform,
        ];
    })()`);
    console.log(`-- ${css} ${dir}`); m.forEach(x => console.log('   ' + x));
}

في الكود أعلاه لاحظ التالي:

  • الدالة getBoundingClientRect تعطي مكان العنصر على الشاشة بعد التخطيط، أي بالبكسل الحقيقي، فنعرف منها في أي جهة وقع العنصر.
  • الدالة getComputedStyle تعطي القيم النهائية بعد أن حول المتصفح الخصائص المنطقية إلى فيزيائية، ولذلك نقرأ marginLeft وmarginRight حتى في الصفحة المنطقية، فنرى أين وضع المتصفح الهامش فعلاً.
  • المسافة بين الأيقونة والنص تحسب حسب الجهة التي وقعت فيها الأيقونة، لأن الأيقونة في العربية على يمين النص.

ثم يكمل السكربت بنفس الطريقة: يقيس مواضع الشريط والشبكة في نافذة عرضها 1000 بكسل ثم 390، ويقيس الخطوط بالدالة measureText في Canvas التي تعطي صندوق الخط والحبر الحقيقي، ويسأل Chrome عن الخطوط المستخدمة بالأمر CSS.getPlatformFontsForNode، ثم يحفظ الصور في مجلد images.

تشغيل التجربة

تحتاج فقط إلى Node.js وChrome، ولا توجد أي حزمة من npm غير sharp التي يستخدمها المساعد لحفظ الصور بصيغة WebP. نشغل السيرفر في نافذة:

node content/articles/css-layout-rtl/lab/server.mjs

ثم نشغل القياس في نافذة أخرى:

node content/articles/css-layout-rtl/lab/measure.mjs

والمخرج سوف يكون كما يلي:

Chrome: Chrome/156.0.8078.49

== 1) the same card, physical vs logical properties
-- physical ltr
   icon left of text, gap to text 8px, margin L/R 0px/8px
   close button at right, overlaps title by 0px
   accent border L/R 4px/0px, padding L/R 16px/48px
   thumb on the left, text-align left
   segment first button radius TL/TR 10px/0px, chevron transform none
-- physical rtl
   icon right of text, gap to text 0px, margin L/R 0px/8px
   close button at right, overlaps title by 0px
   accent border L/R 4px/0px, padding L/R 16px/48px
   thumb on the left, text-align left
   segment first button radius TL/TR 10px/0px, chevron transform none
-- logical ltr
   icon left of text, gap to text 8px, margin L/R 0px/8px
   close button at right, overlaps title by 0px
   accent border L/R 4px/0px, padding L/R 16px/48px
   thumb on the left, text-align start
   segment first button radius TL/TR 10px/0px, chevron transform none
-- logical rtl
   icon right of text, gap to text 8px, margin L/R 8px/0px
   close button at left, overlaps title by 0px
   accent border L/R 0px/4px, padding L/R 48px/16px
   thumb on the right, text-align start
   segment first button radius TL/TR 0px/10px, chevron transform matrix(-1, 0, 0, 1, 0, 0)

== 2) Flexbox and Grid follow dir (window 1000px)
-- dir=ltr flex-direction:row  logo x=36 cart x=890 | aside x=20 item1 x=236 item2 x=488 | list columns 240px 240px 240px
-- dir=rtl flex-direction:row  logo x=885 cart x=36 | aside x=780 item1 x=524 item2 x=272 | list columns 240px 240px 240px
-- dir=rtl flex-direction:row-reverse  logo x=36 cart x=268 | aside x=780 item1 x=524 item2 x=272 | list columns 240px 240px 240px
-- dir=ltr flex-direction:row-reverse  logo x=874 cart x=619 | aside x=20 item1 x=236 item2 x=488 | list columns 240px 240px 240px
-- dir=rtl at 390px: list columns 350px, main above aside: true

== 3) Arabic type
   text width: normal 612px, 2px 612px, 0.2em 612px
   inline spans: Latin 136 -> 156px with 2px, Arabic 313 -> 321px with 2px
   clipped box: clientHeight 26, scrollHeight 29
   line box height at 26px: Alexandria line-height 1 = 26, normal = 32, 1.7 = 44.1875 | Arial normal = 31
   Alexandria 26px Arabic: font box 25.0 up / 7.0 down, ink 26.0 up / 9.0 down
   Alexandria 26px «إلى جانب التأمين»: font box 25.0 up / 7.0 down, ink 26.0 up / 9.0 down, width 188.3
   Arial 26px Latin:       font box 24.0 up / 6.0 down, ink 19.0 up / 5.0 down
   fonts used by #ls0: IBM Plex Sans Arabic (31 glyphs, web font)
   fonts used by #fb1: Manrope ExtraLight (10 glyphs, web font), Arial (28 glyphs)
   fonts used by #fb2: IBM Plex Sans Arabic (27 glyphs, web font), Manrope ExtraLight (10 glyphs, web font)

== 4) font-display: the web font answers after 3 seconds
-- block: after 1s: font loading, heading width 253px | after 4s: font loaded, heading width 244px
-- swap: after 1s: font loading, heading width 253px | after 4s: font loaded, heading width 244px

== 5) direction from CSS only (no dir attribute)
-- /nodir: html dir="", body direction rtl, chevron matches :dir(rtl) false, transform none
-- /card?dir=rtl&css=logical: html dir="rtl", body direction rtl, chevron matches :dir(rtl) true, transform matrix(-1, 0, 0, 1, 0, 0)

المخرج سطراً بسطر

القسم الأول هو البطاقة، وفيه أربع مجموعات، ونقرؤها بالمقارنة بين كل مجموعة والتي قبلها:

  • physical ltr: هذا هو المرجع، أي ما أراده المصمم: الأيقونة على يسار النص وبينهما 8 بكسلات، وزر الإغلاق على اليمين، والحد البرتقالي 4 بكسلات على اليسار، والحشو الواسع 48 على اليمين حيث زر الإغلاق، والصورة المصغرة على اليسار، والزاوية المستديرة للزر الأول على يساره.
  • physical rtl: قارن كل سطر بالمرجع. الأيقونة انتقلت إلى يمين النص لأنها أول عنصر في السطر، ولكن الهامش بقي 0px/8px، أي على يمينها، فأصبحت المسافة بينها وبين النص صفراً. وكل القيم الأخرى بقيت كما هي حرفياً: الحد على اليسار، والصورة على اليسار، والمحاذاة left، والزاوية على اليسار، والسهم دون قلب. أي أن الخصائص الفيزيائية فعلت بالضبط ما كتب فيها، والخطأ في أننا كتبنا جهة الشاشة ونحن نقصد جهة القراءة.
  • logical ltr: نفس أرقام المرجع في كل سطر، ما عدا المحاذاة التي ظهرت start بدلاً من left، وهذا يثبت أن الترحيل إلى الخصائص المنطقية لا يغير الصفحة الإنجليزية.
  • logical rtl: كل شيء انعكس بالمقدار الصحيح: الهامش 8px/0px أي على يسار الأيقونة، فعادت المسافة 8 بكسلات، وزر الإغلاق على اليسار، والحد 0px/4px على اليمين، والحشو 48px/16px انعكس معه، والصورة على اليمين، والزاوية 0px/10px على يمين الزر الأول، والسهم مقلوب بالمصفوفة matrix(-1, 0, 0, 1, 0, 0)، أي ضرب الإحداثي الأفقي في سالب واحد.
  • السطر overlaps title by 0px: بقي صفراً في الحالات الأربع، فزر الإغلاق لم يغط العنوان في أي منها، لأن الحشو الذي يحجز له المكان ينتقل معه دائماً، سواء بقيا معاً على اليمين في الفيزيائية أو انتقلا معاً إلى اليسار في المنطقية.

والقسم الثاني هو التخطيط، وكل الأرقام فيه هي الإحداثي الأفقي x لبداية العنصر من يسار النافذة:

  • في dir=ltr الشعار عند 36 والسلة عند 890، والعمود الجانبي عند 20 وأول منتج عند 236 وثانيه عند 488، أي أن الترتيب من اليسار.
  • في dir=rtl الشعار عند 885 والسلة عند 36، والعمود الجانبي عند 780، وأول منتج عند 524 وثانيه عند 272، أي أن الترتيب كله أصبح من اليمين دون أي تعديل في CSS.
  • في rtl مع row-reverse عاد الشعار إلى 36، وهذا القلب المزدوج الذي شرحناه، ولاحظ أن الشبكة والعمود الجانبي بقيا عند نفس أرقامهما لأن row-reverse طبق على الشريط فقط.
  • السطر الرابع، أي ltr مع row-reverse، يبين الحالة المعاكسة: الشعار ذهب إلى اليمين عند 874، والسلة عند 619 بجانب الروابط لنفس سبب الهامش التلقائي.
  • في كل الحالات بعرض 1000 كانت الشبكة ثلاثة أعمدة بعرض 240، وفي 390 أصبحت عموداً واحداً بعرض 350، والمنطقة main فوق side.

والقسم الثالث هو الخطوط، وقد شرحنا أغلب أسطره في أقسامها، ونضيف هنا ملاحظتين. السطر الأول يقول إن عرض العينات الثلاث 612 بكسلاً، وهذا ليس عرض النص وإنما عرض العنصر الكتلي الذي يحتويه، لأنه يأخذ عرض الأب مهما كان النص، ولهذا أضفنا السطر الثاني الذي يقيس عناصر span سطرية عرضها هو عرض النص فعلاً. وسطر ارتفاع الأسطر يقارن خط Alexandria بخط Arial عند line-height: normal: الأول 32 بكسلاً والثاني 31، أي أن الفرق في الارتفاع الكلي صغير، والمشكلة ليست في ارتفاع الخط العربي وإنما في أن حبره يقترب من حدود صندوقه ويخرج منها.

والقسم الرابع والخامس شرحناهما في قسمي font-display وسمة dir.

مشكلتان واجهتانا أثناء التجربة

المشكلة الأولى كانت في صفحة المقارنة نفسها، فقد كتبنا تحت كل إطار عنواناً بسيطاً هو dir="rtl"، وصفحة المقارنة عربية، فظهر العنوان في الصورة الأولى هكذا: "dir="rtl، أي أن علامة التنصيص الأخيرة قفزت إلى بداية السطر. والسبب أن علامات التنصيص محايدة الاتجاه، وفي نهاية سطر عربي تأخذ اتجاه الفقرة فتذهب إلى يسار النص الإنجليزي. والحل كان وضع النص الإنجليزي في عنصر bdi مع dir="ltr" كما في صفحة MDN لعنصر bdi، وهذا تذكير بأن الخصائص المنطقية تحل مشكلة مكان الصناديق، ولا تحل مشكلة ترتيب الحروف داخل السطر، وهذه موضوع مقال BiDi.

والمشكلة الثانية كانت في قياس font-display، ففي التشغيل الأول قسنا عرض العنوان h2 فأعطى 520 بكسلاً في الحالتين قبل وصول الخط وبعده، فظننا أن التبديل لا يغير شيئاً، ثم انتبهنا إلى أن h2 عنصر كتلي عرضه عرض الصفحة دائماً، فوضعنا النص في span وقسناه، فظهر الفرق الحقيقي من 253 إلى 244. ونفس الخطأ يقع فيه من يكتب اختباراً آلياً يقارن عرض عنصر كتلي قبل تغيير الخط وبعده، والقاعدة: قس العنصر السطري أو استخدم Range.getBoundingClientRect على النص نفسه.

ماذا يحدث إذا وضعت الاتجاه في CSS بدلاً من dir؟

هذه هي التجربة الخامسة في المخرج، وقد صنعناها لأن بعض أطر الواجهات وقوالب المواقع تغير الاتجاه بإضافة صنف إلى body يحتوي على direction: rtl بدلاً من تغيير السمة. والنتيجة أن الاتجاه المحسوب body direction rtl صحيح في الحالتين، فالنص والتخطيط على اليمين، ولكن :dir(rtl) أعاد false للسهم، فلم ينقلب. فإذا كانت الأيقونات في مشروعك تقلب بـ :dir() فلا بد أن يغير الإطار السمة dir على html، وإذا لم يكن ذلك ممكناً فاستخدم المحدد [dir="rtl"] .chev على صنف تضعه أنت، مع العلم أن هذا المحدد لا يرث الاتجاه من عنصر dir="auto" كما يفعل :dir().

كيف تمنع رجوع الأخطاء Regression بعد الإصلاح؟

بعد أن أصلح الفريق البطاقة بقي سؤال: كيف نضمن أن المكون التالي الذي يكتبه مطور جديد لن يعيد margin-left؟ هناك ثلاث طبقات من الحماية، ولكل منها تكلفة.

  • مراجعة CSS آلياً: أداة فحص الكود Linter مثل Stylelint يمكن أن تمنع الخصائص الفيزيائية الأفقية بالقاعدة property-disallowed-list التي تمنع أسماء خصائص تحددها أنت، فيفشل خط CI عند أي margin-left جديد، مع استثناءات مكتوبة للحالات المقصودة مثل الظلال.
  • اختبار المواضع في المتصفح: نفس فكرة سكربت القياس، ولكن داخل إطار اختبار، فتتحقق أن الصورة المصغرة على يمين النص في الصفحة العربية، كما فعلنا في اختبار الصفحة العربية في مقال Playwright عندما تحققنا أن السلة على يسار قائمة المنتجات بالدالة boundingBox.
  • مقارنة الصور Visual Regression: تصوير المكونات باللغتين ومقارنتها بصور معتمدة، وهي أقوى طبقة لأنها تلتقط كل ما في الصور السابقة، وأكثرها تكلفة لأن كل تغيير مقصود في التصميم يحتاج إلى تحديث الصور.

والترتيب العملي المناسب لأغلب الفرق: ابدأ بقاعدة الفحص لأنها الأرخص وتمنع المشكلة من مصدرها، ثم أضف اختبارات المواضع للمكونات الأساسية مثل الشريط والبطاقة والنافذة، ثم مقارنة الصور عندما يصبح عندك نظام تصميم Design System مستقر.

أخطاء شائعة

  1. ملف CSS منفصل للعربية: يضاعف الصيانة ويختلف عن الأصل مع الوقت، فاكتب ملفاً واحداً بالخصائص المنطقية.
  2. الاتجاه في CSS وحده: direction: rtl دون dir يفقدك :dir() ويضيع الاتجاه إذا لم يحمل CSS، فضع dir وlang على html.
  3. row-reverse أو order للعربية: يقلب الصف مرتين ويفصل ترتيب العين عن ترتيب لوحة المفاتيح، فاترك Flexbox يتبع الاتجاه.
  4. الاختصار ذو الأربع قيم: padding: 16px 48px 16px 16px فيزيائي دائماً، فقسمه إلى padding-block وpadding-inline.
  5. قلب كل الأيقونات: العدسة والساعة وعلامة الصح وسهم التنزيل والشعارات لا تقلب، فقط ما له معنى مرتبط باتجاه القراءة.
  6. نسيان transform وموضع الخلفية: لا ينقلبان مع الاتجاه، فراجع القوائم المنزلقة والأيقونات داخل الحقول.
  7. خط لاتيني وحده في السلسلة: يجعل النظام يختار الخط العربي، فيختلف شكل الصفحة بين الأجهزة، فضع خطاً عربياً صريحاً بعد اللاتيني.
  8. line-height صغير مع overflow: hidden: يقص همزات الحروف ونقاطها وما ينزل منها تحت الخط، فاترك للعربية ارتفاعاً لا يقل عن صندوق الخط.
  9. نقل letter-spacing وuppercase من التصميم الإنجليزي: لا معنى لهما في العربية، وقد تفصل المسافات الحروف في متصفح آخر، فألغهما بـ :lang(ar).
  10. الاختبار بعين لا تقرأ العربية: كثير من الأخطاء تبدو سليمة لمن لا يقرأ، مثل زر الإغلاق في بداية القراءة، فاجعل من يراجع الواجهة العربية قارئاً لها.

قائمة مراجعة Checklist قبل إطلاق الواجهة العربية

البندكيف تتحققالإصلاح
dir="rtl" وlang="ar" على htmlافتح المصدر أو اختبر السمتين آلياًضعهما في القالب الرئيسي وليس في CSS
لا توجد خاصية فيها left أو right للهوامش والحشو والحدود والموضعابحث في الملفات عن -left و-right وleft: وright:استبدلها بـ inline-start وinline-end وinset-inline
المحاذاة والطفوابحث عن text-align: left وfloat: leftstart وinline-start، أو احذف القيمة الافتراضية
الزوايا المستديرة للعناصر المتلاصقةانظر إلى أطراف الأزرار المجمعة والتبويباتborder-start-start-radius وأخواتها
Flexbox وGrid دون قلب يدويابحث عن row-reverse وorder في ملفات العربيةاحذفها واترك الاتجاه يقلب الترتيب
الأيقوناتراجع كل أيقونة: هل معناها مرتبط باتجاه القراءة؟اقلب ما يلزم بـ :dir(rtl) فقط
الإزاحة والحركةابحث عن translateX وbackground-position وقيم left في JavaScriptاضربها في متغير الاتجاه أو اقرأ document.dir
الخط العربي في السلسلةلوحة Rendered Fonts في DevTools على نص عربيخط عربي صريح بعد اللاتيني، ثم بديل عام
ارتفاع السطرابحث عن line-height: 1 مع ارتفاع ثابت وoverflow: hidden1.6 إلى 1.8 للفقرات، وقطع النص بـ text-overflow
تحميل الخطافتح الصفحة مع تبطئة الشبكة في DevToolsfont-display مناسب لكل خط، وتخزين مؤقت طويل للملف
العرض الضيقاعرض الصفحة بعرض 390 بكسلاً باللغتينminmax وauto-fit والمناطق المسماة

الخلاصة

وصلنا لنهاية الموضوع، وأهم ما فيه:

  • المتصفح يرتب الصفحة بمحورين: محور السطر الذي يبدأ من اليمين في العربية، ومحور الكتل من الأعلى إلى الأسفل، وكل ما كتب بأسماء الشاشة لا يعرف شيئاً عن هذا.
  • الخصائص الفيزيائية لا تعطي خطأ في الصفحة العربية، وإنما تنفذ ما كتب فيها حرفياً، فتظهر الأخطاء في المسافات والحدود والصور والمحاذاة والزوايا.
  • الخصائص المنطقية مثل margin-inline-start وinset-inline-end وborder-start-start-radius وtext-align: start تعطي نفس النتيجة في الإنجليزية بالضبط، وتنعكس وحدها في العربية، فملف واحد يكفي للغتين.
  • ضع dir وlang على html، فالاتجاه في CSS وحده يفقدك المحدد :dir().
  • Flexbox وGrid يتبعان الاتجاه دون أي تعديل، والقلب اليدوي بـ row-reverse يعيد الترتيب الإنجليزي.
  • اقلب الأيقونات التي يرتبط معناها باتجاه القراءة فقط، واترك الأشياء الحقيقية والاتجاهات الحقيقية والشعارات.
  • الخط العربي يحتاج إلى مكان صريح في سلسلة الخطوط، وإلى ارتفاع سطر أكبر من اللاتيني، ولا يحتاج إلى letter-spacing أو uppercase، واختر font-display حسب نوع النص.

وفي مرحلة هندسة الواجهات وتطبيقات الويب تجد المقالات التي تكمل هذا المقال، ومنها مقال المتصفح من الداخل الذي يشرح ما يحدث عند حساب التخطيط والرسم، ومقال التعريب والتوطين الذي يشرح ترتيب النص داخل السطر والتواريخ والأرقام:

دليلكيف يرسم المتصفح صفحتك؟ من HTML إلى البكسلاتماذا يفعل المتصفح بين وصول HTML وظهور الصفحة، ولماذا تتجمد صفحة فيها ألفا صف، وكيف تقيس كل مرحلة في Chrome وتصلحها.دليلالتعريب i18n للمطورين: لماذا ينقلب النص العربي في الواجهة؟لماذا ينقلب النص المختلط في الواجهة العربية وكيف تعزله، وكيف تنسق الأرقام والتواريخ وصيغ الجمع العربية بأدوات المتصفح نفسها.دليلحلقة الأحداث Event Loop: لماذا تتأخر النقرة في صفحتك؟كيف ترتب JavaScript المهام والوعود والرسم على خيط واحد، ولماذا تنتظر النقرة خلف مهمة طويلة، وكيف تقسم العمل لتبقى الصفحة سريعة.دليلسهولة الوصول Accessibility: موقع وتطبيق يستخدمه الجميعكيف تبني صفحات وتطبيقات يستخدمها الجميع بلوحة المفاتيح وقارئ الشاشة، من عناصر HTML الأصلية إلى النوافذ والنماذج العربية.دليلاختبارات E2E مع Playwright: من أول اختبار إلى CIكيف تكتب اختبارات متصفح ثابتة لموقع عربي مع Playwright، وتشخص الاختبار المتقلب، وتقسمها على أكثر من جهاز في خط CI.دليلأسئلة مقابلة مطور Frontend من المبتدئ إلى الخبيربنك أسئلة لمقابلة مطور Frontend، لكل سؤال إجابة المبتدئ وإجابة الخبير وأسئلة متابعة، مع تمارين عملية شغلناها فعلاً.

سجل التحديثات

  • أكتوبر 2026: كتابة المقال، وتنفيذ التجربة في Chrome 156 دون واجهة عبر بروتوكول DevTools مع Node.js 22.14، وقياس البطاقة والتخطيط والخطوط باتجاهين.
Could not load content
نشرة عرب رووت | ArabRoot

معرفة تستحق مكاناً في بريدك.

مقالات مختارة وأدوات مفيدة وأفكار لمشروعك القادم، في رسالة واحدة كل أسبوع.

يمكنك إلغاء الاشتراك متى شئت. الخصوصية

تم استلام طلبك. افتح بريدك واضغط رابط التأكيد لإتمام الاشتراك.