تخطَّ إلى المحتوى

4 آب 2026 · قراءة 8 دقائق

الاتجاه من اليمين ليس مجرد انعكاس

قلب التخطيط إلى اليمين هو الجزء السهل. هذه هي الأشياء التي لا تنقلب وحدها، وتلك التي يجب ألّا تنقلب أبداً.

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

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

الخصائص الفيزيائية لا تنعكس. المنطقية تنعكس.

margin-left تعني اليسار في كل لغة. لا رأي لها في اتجاه القراءة، لذا فالتخطيط المبني على left وright وpadding-left وtext-align: left يبقى مكانه تماماً حين تقلب المستند.

أما المكافئات المنطقية فتنعكس تلقائياً:

فيزيائية منطقية
margin-left margin-inline-start
padding-right padding-inline-end
left: 0 inset-inline-start: 0
text-align: left text-align: start
border-left border-inline-start

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

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

قيمة scrollLeft تصبح سالبة

هذه مفاجئة فعلاً، وهي ليست خللاً — بل هذا هو المعيار.

في حاوية تمرير من اليسار إلى اليمين، تبدأ scrollLeft من 0 وتتزايد حتى scrollWidth - clientWidth. أما من اليمين إلى اليسار، فموضع السكون يبقى 0، لكن التمرير يدفعها إلى الأرقام السالبة. في حاوية بفائض عرض 2297 بكسل، يكون المدى المتاح من 0 إلى -2297.

وكل ما يفترض مدى موجباً ينكسر بصمت:

// خاطئ في RTL: شريط التقدّم يبقى عند الصفر أبداً،
// لأن scrollLeft لا تصبح موجبة إطلاقاً.
const progress = el.scrollLeft / (el.scrollWidth - el.clientWidth);

// يعمل في الحالتين: قِس المسافة المقطوعة لا الموضع بإشارته.
const max = el.scrollWidth - el.clientWidth;
const progress = max > 0 ? Math.abs(el.scrollLeft) / max : 0;

وينطبق الأمر نفسه على السحب بالمؤشّر. سحبٌ يحسب startScroll - deltaX سيمرّر المسافة الصحيحة في الاتجاه الصحيح في الوضعين — لكن الحدّ الأقصى يقع في الطرف المقابل، فاختبارٌ يسحب “يساراً” في RTL إنما يدفع باتجاه الحائط، وسيبدو أن السحب معطّل وهو سليم. حين تختبرون هذا، اسحبوا في الاتجاهين.

التحويلات لا تنعكس هي الأخرى

transform: translateX(-50%) إزاحة فيزيائية نحو اليسار. وتبقى نحو اليسار تحت dir="rtl".

واجهنا هذا في شريط شعارات متحرّك. المسار مبني كصفّ مرن ويُحرّك بإزاحته إلى -50%، فتُغلق الحلقة بسلاسة لأن المحتوى مكرّر. في LTR يبدأ المسار من الحافة اليسرى وينزلق يساراً، فيسحب النسخة الثانية إلى المشهد. اقلبه إلى RTL فيبدأ المسار من الحافة اليمنى — وعندها تسحبه الإزاحة يساراً خارج الشاشة مباشرة، تاركةً شريطاً فارغاً يتّسع خلفه. بدا الشريط وكأنه يدور مرة واحدة ثم يموت.

والعلاج أن تعكسوا الوجهة، لا الآلية:

@keyframes marquee-rtl {
  from { transform: translate3d(0, 0, 0); }
  to   { transform: translate3d(50%, 0, 0); }
}

html[dir='rtl'] [data-marquee-track] {
  animation-name: marquee-rtl;
}

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

dir على كتلة ليست كـdir على سطر

ستكون لديكم أرقام ورموز منتجات وأرقام هواتف وأسماء تجارية لاتينية داخل نصّ عربي. هذه المقاطع تبقى من اليسار إلى اليمين، وخوارزمية الاتجاهين تتولّاها في الغالب — لكن ليس دائماً، وللعلاج حدٌّ حادّ.

تحديدها صراحةً صحيح:

<span dir="ltr">+963 99 20 444 50</span>

أما وضع dir على الكتلة المحيطة فليس كذلك. فـdir تضبط سياق الاتجاهين ومحاذاة الصندوق معاً، لذا فـ<dd dir="ltr"> داخل صفحة عربية يعرض محتواه بمحاذاة يسارية بينما تبقى كل الأشقّاء بمحاذاة يمينية. وقد أطلقنا هذا فعلاً: صفّ إحصاءات استقرّت قيمه على الحافة اليسرى ووصفه على اليمنى. يبدو كشبكة مكسورة، وسببه سمة واحدة على العنصر الخطأ.

أبقوا dir على أصغر عنصر سطري يحتوي فعلاً المقطع اللاتيني.

بعض الأشياء يجب ألّا تنقلب

الانعكاس ليس صحيحاً على إطلاقه، ووضع transform: scaleX(-1) على كل أيقونة هو الطريق إلى شعار مقلوب.

اقلبوا: الأسهم التي تدلّ على التقدّم أو الرجوع، ومؤشّرات التقدّم، وكل ما يمثّل اتجاه القراءة، وعلامات القوائم والإزاحات.

لا تقلبوا: الشعارات والعلامات الكتابية، وأوجه الساعات، وأزرار تشغيل الوسائط (زرّ التشغيل يبقى متّجهاً يميناً بالعربية — فهو استعارة شريط فيزيائي لا استعارة قراءة)، وعلامات الصحّ، ومعظم صور المنتجات، وأي رسم يحوي نصاً لاتينياً.

والقاعدة الثابتة: اعكسوا ما يعني إلى الأمام، واتركوا ما يعني ذاته.

محاذاة التوقّف تحتاج انعكاس حشوها أيضاً

إذا استخدم مُمرّر أفقي scroll-padding-left لمحاذاة عناصره على هامش، فسيبقى ذلك الحشو على اليسار الفيزيائي في RTL وتقع مواضع التوقّف بفارق هامش كامل. scroll-padding-inline-start تنعكس بشكل صحيح، وينبغي أن تطابق أياً كان الحشو السطري للحاوية — وإلّا استقرّ العنصر الأول قبل الموضع الذي يحاذيه كل شيء آخر في الصفحة.

كيف تختبرونه فعلاً

قراءة النصّ العربي ليست الاختبار، ولا النظر إلى لقطة شاشة. الأعطال أعلاه هندسية، فقيسوا الهندسة:

  • قارنوا الحافة الأمامية لمكوّن بالحافة الأمامية للعنوان فوقه. في LTR قارنوا getBoundingClientRect().left، وفي RTL قارنوا .right. يجب أن تتطابقا عند كل عرض شاشة.
  • تحقّقوا أن document.documentElement.scrollWidth - clientWidth تساوي 0 في اللغتين. أي قيمة أكبر تعني فائضاً أفقياً، وهو في RTL دليل على خاصية فيزيائية نجت.
  • مرّروا أو اسحبوا كل حاوية أفقية في الاتجاهين وراقبوا scrollLeft. إذا تحرّك اتجاه بمقدار بكسل واحد وتحرّك الآخر كما يجب، فأنتم تصطدمون بالحدّ لا بعطل.

لا شيء من هذا يتطلّب إتقان العربية. يتطلّب أن تعاملوا التخطيط المقلوب كتخطيط قائم بذاته، لا كعرضٍ للتخطيط الحقيقي.

الخلاصة

دعم الاتجاه من اليمين ليس مهمّة ترجمة تُنجز في النهاية. فكل مشكلة أعلاه تقريباً تعود إلى الجذر نفسه: قرارٌ اتُّخذ بمنطق فيزيائي — يمين، يسار، موجب، أمام — ولم يكن له إلّا جواب صحيح واحد لأن اتجاهاً واحداً فقط كان في الحسبان.

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

تواصلوا معنا

أخبرونا ما الذي لا يعمل

ثلاثون دقيقة، دون عرض تقديمي. اشرحوا لنا المشكلة ونعود إليكم بقراءتنا الأولى لها.

ما الذي يحدث بعدها

  1. 1. تختارون موعداً، أو ترسلون لنا المشكلة فقط.
  2. 2. نردّ خلال يوم عمل واحد.
  3. 3. تصلكم قراءتنا الأولى لها. دون مقابل ودون عرض تقديمي.