موقع Next.js ثنائي اللغة بالإنجليزية والعربية: التوجيه و RTL وما تراه الزواحف فعلًا
جدول المحتويات
عن الكاتب

Ahmed Mahmoud
كاتب ومطوّر
مهندس برمجيات شغوف بتطوير الويب وتصميم تجربة المستخدم.
مؤسّس ديفيا · eng-ahmed.com ↗في Devya نبني معظم منتجاتنا باللغتين الإنجليزية والعربية. أي موقع Next.js ثنائي اللغة يحتاج إلى ثلاثة أشياء تُنفَّذ بشكل صحيح: عنوان URL مستقل لكل لغة (بادئة مسار /en و /ar وليس كوكي)، وتصميم ينقلب تلقائيًا عبر خصائص CSS المنطقية بدلًا من ترقيعات يدوية لليمين واليسار، ووسوم hreflang تخبر محركات البحث أن العنوانين ترجمة لبعضهما. تعلّمنا النقطة الأولى بالطريقة الصعبة — مبدّل لغة يعتمد على الكوكيز جعل الزواحف تفهرس النصف الإنجليزي فقط من موقع بنيناه.
أهم الخلاصات
- قدّم كل لغة على عنوان URL خاص بها. بادئات المسار مثل /en/services و /ar/services هي البنية متعددة اللغات التي توصي بها وثائق Google؛ أما المحتوى الذي يتبدّل حسب الكوكي على عنوان واحد فلن تُفهرس منه إلا لغة واحدة.
- زواحف محركات البحث لا تحتفظ بالكوكيز، و Googlebot يزحف غالبًا من عناوين IP أمريكية دون تغيير ترويسة Accept-Language — لذلك مبدّل اللغة المعتمد على الكوكيز غير مرئي لها.
- ضبط dir="rtl" على عنصر html مع خصائص CSS المنطقية يقلب التصميم تلقائيًا؛ أدوات Tailwind مثل ms-* و me-* و ps-* و pe-* و text-start تقابل margin-inline-start وأخواتها.
- الخطوط العربية يجب أن تُحمَّل لكل لغة على حدة عبر next/font — مثل IBM Plex Sans Arabic مع مجموعة arabic الفرعية — حتى لا يحمّل زائر الإنجليزية الخط العربي والعكس صحيح.
- وسوم hreflang تُضاف في generateMetadata عبر alternates.languages، ويجب أن تكون متبادلة بين اللغتين، مع x-default يشير إلى عنوان اللغة الافتراضية.
كيف نبني توجيه اللغات في Next.js App Router؟
ضع اللغة في أول جزء من المسار: مقطع ديناميكي app/[locale] بحيث توجد كل صفحة على /en/... و /ar/.... يقرأ الـ layout الخاص باللغة قيمة المعامل ويضبط lang و dir على عنصر html، بينما يولّد generateStaticParams شجرتي اللغتين مسبقًا.
الـ middleware يقوم بمهمة واحدة فقط: أي طلب لمسار بلا بادئة مثل / أو /services يُعاد توجيهه إلى نظيره المترجم، بالاعتماد أولًا على كوكي التفضيل المحفوظ ثم على ترويسة Accept-Language. بعد إعادة التوجيه يصبح عنوان URL — لا الكوكي — المصدر الوحيد لحقيقة اللغة. أما الترجمات نفسها فيمكن أن تكون ملفات JSON بسيطة تُقرأ داخل مكوّنات الخادم، فلا تُشحن أي نصوص ترجمة في حزمة المتصفح؛ وتستحق مكتبة next-intl مكانها عندما تحتاج إلى صيغ الجمع ICU والتنسيق الغني ومفاتيح رسائل مُنمّطة.
لماذا يضرّ تبديل اللغة عبر الكوكيز بالسيو؟
لأن الزاحف زائر جديد دائمًا بلا أي كوكيز: يطلب العنوان فيستلم اللغة الافتراضية ويفهرسها، ولا تدخل اللغة الثانية الفهرس أبدًا. Googlebot لا يحتفظ بالكوكيز بين الطلبات، ويزحف غالبًا من عناوين IP أمريكية، ولا يغيّر ترويسة Accept-Language بشكل منهجي — ووثائق Google نفسها توصي بعناوين منفصلة لكل لغة بدلًا من محتوى متغيّر على عنوان واحد.
كما يكسر التبديل بالكوكيز وسوم hreflang تمامًا، لأنها تربط اللغات بعناوين URL بينما اللغتان تتشاركان عنوانًا واحدًا. ويضر البشر أيضًا: رابط ينسخه قارئ عربي يفتح عند المستلم باللغة التي يحددها كوكي المستلم لا بما كان يراه المرسل. ويضر التخزين المؤقت كذلك — إذ يجب على CDN أن يفرّق حسب الكوكي ليخزّن أي شيء، بينما توجيه البادئات يخزّن كل لغة كعنوان عادي.
تكلفة الانتقال أقل مما تتوقع الفرق: يتحول مبدّل اللغة إلى رابط لنفس المسار تحت البادئة الأخرى، ويبقى الكوكي فقط ليجعل إعادة توجيه المسار المجرد تتذكر اختيار الزائر.
كيف نجعل التصميم ينقلب في وضع RTL دون إعادة كتابة CSS؟
اضبط dir="rtl" على عنصر html واكتب كل التنسيقات بخصائص CSS المنطقية؛ والمتصفح يتولى القلب. الخاصية الفيزيائية مثل margin-left تبقى على اليسار في الاتجاهين، أما المنطقية margin-inline-start فتعني "الجهة التي يبدأ منها النص" — اليسار في الإنجليزية واليمين في العربية. وفي Tailwind الأمر استبدال آلي: ml-4 تصبح ms-4، و pr-6 تصبح pe-6، و text-left تصبح text-start، و border-l-2 تصبح border-s-2.
يتبع Flexbox و Grid اتجاه المستند أصلًا، فمعظم التصميم ينقلب مجانًا. ما يحتاج عناية يدوية قائمة قصيرة: الأيقونات الاتجاهية — أسهم الرجوع والأسهم الصغيرة ومؤشرات التالي — يجب أن تنعكس، ومتغيّر rtl:-scale-x-100 في Tailwind يتكفل بذلك؛ أما الشعارات وعلامات الصح وأيقونات تشغيل الوسائط فلا تنعكس. النص مختلط الاتجاه هو الأدق: اسم منتج إنجليزي أو بريد إلكتروني أو معرّف برمجي داخل جملة عربية قد يسحب علامات الترقيم إلى الجهة الخطأ، ولفّ الجزء المضمّن داخل عنصر bdi (أو استخدام dir="auto" مع محتوى المستخدمين) يعزله. وللأرقام، يحسم Intl.NumberFormat الاختيار بين الأرقام الغربية والأرقام العربية المشرقية حسب اللغة — اختر اصطلاحًا واحدًا وطبّقه في كل مكان.
كيف نحمّل الخطوط العربية دون شحنها للجميع؟
عرّف خطًا لكل نظام كتابة عبر next/font واربط متغيّر خط اللغة النشطة فقط في الـ layout — Inter لمجموعة latin و IBM Plex Sans Arabic لمجموعة arabic، وكلاهما تحت نفس متغيّر CSS. يستضيف next/font الخطوط ذاتيًا ويقتطع المجموعات وقت البناء، فلا طلب لخوادم Google Fonts وقت التشغيل ولا إزاحة تخطيط بسبب تأخر تحميل الحروف.
ملاحظتان خاصتان بالحرف العربي من تجربتنا: لا توجد مائلة (italic) في تقاليد الخط العربي — والمتصفحات تصطنع ميلًا يبدو مكسورًا، لذلك لا تستخدم تنسيقاتنا العربية المائل أبدًا وتعتمد على الوزن للتأكيد. كما أن الحروف العربية أعلى من اللاتينية، فارتفاع السطر المضبوط لعناوين إنجليزية يحتاج عادة إلى توسيع في النسخة العربية من نفس المكوّن.
كيف نضبط hreflang لتُفهرس اللغتان معًا؟
أعد alternates.languages من generateMetadata في كل صفحة، مع العنوان المطلق لكل ترجمة إضافة إلى x-default. ثلاث قواعد تُبقيها صحيحة: يجب أن تكون الوسوم متبادلة — الصفحة الإنجليزية تذكر العنوان العربي والعربية تذكر الإنجليزي وكل صفحة تذكر نفسها؛ ويجب أن تكون العناوين مطلقة و canonical؛ و x-default يحدد صفحة الزوار الذين لا تطابق لغتهم أيًا من اللغتين، وعادة يكون العنوان الإنجليزي.
ترجم بقية البيانات الوصفية في الوقت نفسه: صفحة عربية بعنوان ووصف إنجليزيين تبدو مكسورة في نتائج البحث العربية، ومحركات الإجابة التي تقتبس الصفحة ترث هذا التنافر.
الأسئلة الشائعة
س: هل يستخدم Googlebot الكوكيز أو Accept-Language للوصول إلى المحتوى العربي؟
ج: لا. Googlebot لا يحتفظ بالكوكيز ويزحف عمومًا دون تغيير Accept-Language، وغالبًا من عناوين IP أمريكية. المحتوى الذي لا يُوصَل إليه إلا عبر كوكي أو ترويسة يبقى خارج الفهرس — امنح كل لغة عنوانها الخاص.
س: هل يجب أن تكون العربية على بادئة مسار أم نطاق فرعي أم نطاق دولة؟
ج: بادئة المسار مثل /ar/ هي الأرخص تشغيليًا: نشر واحد، ونطاق واحد يراكم السلطة، و hreflang يربط النسختين. النطاقات الفرعية ونطاقات الدول تناسب أعمالًا إقليمية منفصلة لا نسخة لغوية من موقع واحد.
س: هل يكسر dir="rtl" تخطيطات Flexbox أو Grid؟
ج: لا. صفوف Flex وأعمدة Grid تتبع اتجاه المستند تلقائيًا. الكسر يأتي من الأدوات الفيزيائية مثل ml-4 أو text-left ومن العناصر ذات التموضع المطلق المثبتة بـ left أو right — استبدلها بالمكافئات المنطقية.
س: هل نحتاج next-intl لموقع ثنائي اللغة؟
ج: ليس بالضرورة. مقطع app/[locale] مع قواميس JSON تُقرأ في مكوّنات الخادم يكفي موقعًا ثنائي اللغة ثابتًا؛ وتفيد next-intl مع صيغ الجمع ICU وتنسيق التواريخ والأرقام ومفاتيح الرسائل المُنمّطة في تطبيق كبير.
س: أي الأيقونات يجب أن تنعكس في RTL؟
ج: الاتجاهية فقط — أسهم الرجوع والتقدم ومؤشرات التقدم. أما الشعارات وعلامات الصح وأزرار تشغيل الوسائط فتحافظ على اتجاهها.
قراءات إضافية
هندسة الواجهات
الوضع الداكن دون وميض في Next.js 16: كيف نستخدم سكربت سمة حاجبًا وcolor-scheme وزر تبديل آمنًا مع الـ Hydration
وميض الوضع الداكن عند إعادة تحميل الصفحة مشكلة توقيت: الخادم لا يستطيع قراءة localStorage. نشرح سكربت السمة الحاجب الذي نستخدمه في Next.js 16، ولماذا لا نضع السمة في cookie حفاظًا على الرسم الثابت، وكيف نبني زر تبديل يمر بالـ hydration دون أي عدم تطابق.
هندسة الواجهات
صور Open Graph الديناميكية في Next.js 16: كيف نبني opengraph-image.tsx ونتحقق من إصدار Satori الذي تضمّنه next/og ونحافظ على صحة معاينات الروابط
واجهت بطاقات المشاركة المولّدة لدينا ثلاث مفاجآت: خط WOFF2 لم يستطع المحرّك قراءته، وملف README لـ Satori يصف محرّكًا أحدث من المضمّن في Next.js، وصفحة ظلّت تعرض صورة الـ layout. هذه ملاحظاتنا الميدانية حول opengraph-image.tsx في Next.js 16 وأولوية البيانات الوصفية وفحص og:image قبل مشاركة أي رابط.