Building Bilingual React Apps: Arabic & English with RTL Support

TL;DR

Adding dir="rtl" to your HTML tag gets you about 60% of the way to an Arabic-ready app and gives you a false sense of being done. The other 40% is layout that mirrors correctly, icons that flip, mixed Arabic-English text that doesn't shatter the line, fonts that load in Arabic without choking the page, and forms that handle numbers and punctuation sanely. The fix is to lean on CSS logical properties (ms-, pe-, start-*), use Tailwind's rtl: variant for direction-aware classes, gate Arabic fonts behind the active language, wrap embedded LTR runs in <bdi>, and test with real Arabic content, never with mirrored lorem ipsum. Build this in from day one. Retrofitting it onto a finished LTR app takes longer than rebuilding the layout layer from scratch.

Why RTL Is More Than direction: rtl

Most tutorials tell you to add dir="rtl" to your HTML tag and call it a day. That gets you maybe 60% of the way. The other 40% is where things get tricky, and where most apps targeting Arabic speakers fall apart.

I've shipped close to a dozen bilingual Arabic-English apps in the last three years, for Qobouli students, for Maka Media, for various clients across Istanbul and Riyadh. The first one took weeks of unscheduled cleanup because I treated RTL as a configuration flag instead of a design dimension. The last few took zero unscheduled cleanup, because I now build for both directions from the first line of code.

This is what I wish someone had handed me three years ago.

The Core Challenges

1. Layout Mirroring

Every margin-left becomes margin-right. Every padding-left becomes padding-right. Every flex-row visually reverses. Every absolute-positioned element pinned with left: 0 ends up on the wrong side of the screen. If you've hardcoded directional values anywhere in your codebase, you'll need to change them all, and the worst ones are the ones you forgot, which only show up in a screenshot a client sends you a week after launch.

The fix: Use logical properties. In Tailwind, that means ms-4 (margin-inline-start) instead of ml-4, pe-6 (padding-inline-end) instead of pr-6, start-0 instead of left-0, end-2 instead of right-2. These automatically flip based on direction. In vanilla CSS, the modern equivalents are margin-inline-start, padding-inline-end, inset-inline-start, and so on.

The rule I now follow: the words "left" and "right" are bugs. Every time I see them in a class name or style rule, I treat them as a code smell that needs justification.

2. Icons That Have Direction

Back arrows, chevrons, progress indicators, next/previous buttons, send-message icons: anything that implies a direction needs to flip in RTL. A back arrow pointing left in English should point right in Arabic, because in Arabic "back" is to the right.

The fix: Add rtl:rotate-180 to directional icons in Tailwind, or write a CSS rule like [dir="rtl"] .directional-icon { transform: rotateY(180deg); }. For icon libraries, build a small <DirectionalIcon> wrapper that handles this once.

Be careful not to over-flip. A play button, a heart, a checkmark, a user avatar: these have no direction and should never flip. The trick is to keep a tight list of which icons are directional, and never auto-flip everything.

3. Font Loading

Arabic fonts are significantly larger than Latin fonts, often 3 to 5 times the file size, because Arabic needs many more glyph variations for ligatures and contextual forms. Loading both fonts upfront hurts performance on every page, even for users who only see one of them.

The fix: Load the Arabic font only when the active language is Arabic, and the Latin font only when English. Use font-display: swap to prevent layout shift, and preload the active font in the <head>. If you serve from a CDN, request only the glyph subset you actually use, for most apps you can drop file size by 70% by subsetting.

For my projects I typically use Cairo or IBM Plex Sans Arabic for Arabic, and Inter or Geist for English. I switch them dynamically based on the active language, and I never ship both at the same time.

4. Mixed Content

Arabic text with embedded English technical terms ("تطبيق React مع TypeScript") produces bidirectional text. The browser's bidi algorithm handles most cases well, but numbers, punctuation, parentheses, and quotation marks can flip in surprising ways, especially when they're adjacent to brackets or hyphens.

The fix: Wrap embedded LTR runs in <bdi> tags so the bidi algorithm treats them as isolated units. For numbers in Arabic UIs, decide explicitly whether you want Arabic-Indic digits (٠١٢٣) or Western digits (0123) and apply a consistent rule. Most Arabic-speaking users today prefer Western digits for technical content, but Arabic-Indic still feels right in editorial copy.

5. Forms, Inputs, and Numbers

Form inputs deserve special attention. An input with type="email" should always be LTR, even on an Arabic page, because email addresses are LTR by nature. Same for URLs, phone numbers, and password fields. A bilingual form that gets this wrong looks broken even to users who can't articulate why.

The fix: Set dir="ltr" directly on inputs that hold LTR content, regardless of the page direction. Most form libraries support this out of the box once you tell them.

6. Animations and Transitions

A slide-in animation that enters from the left in English should enter from the right in Arabic, otherwise it feels backwards. Same for swipe gestures, drawer pull-outs, and carousel navigation.

The fix: Use logical transform values where possible (transform: translateX() with a value derived from the direction), or define two animation keyframes, one for LTR, one for RTL, and select via the [dir] attribute.

My Approach

I use a LanguageProvider context that:

  • Stores the current language in React state
  • Provides a t() function for translations
  • Sets dir and lang on the document root via a side effect
  • Loads the appropriate font on language change
  • Persists the user's choice in localStorage and reads it back on page load
  • A simplified version looks like this:

    function LanguageProvider({ children }: { children: React.ReactNode }) {
      const [language, setLanguage] = useState<'en' | 'ar'>(() => {
        return (localStorage.getItem('lang') as 'en' | 'ar') ?? 'en';
      });
    
      useEffect(() => {
        document.documentElement.lang = language;
        document.documentElement.dir = language === 'ar' ? 'rtl' : 'ltr';
        localStorage.setItem('lang', language);
      }, [language]);
    
      return (
        <LanguageContext.Provider value={{ language, setLanguage, t: makeT(language) }}>
          {children}
        </LanguageContext.Provider>
      );
    }

    Translations live in JSON files (en.json, ar.json) loaded on demand or imported statically depending on whether the app is content-heavy. For small apps I import them statically; for larger ones I lazy-load by route.

    Testing RTL

    Always test with real Arabic content. Mirrored lorem ipsum will not expose bidi issues, font-rendering problems, ligature breakage, or layout failures caused by genuinely longer Arabic words ("registration" is 12 characters; "التسجيل" is 7 but renders much wider in some fonts).

    A reasonable test matrix:

  • A single short Arabic word in a tight container: does it overflow?
  • A long Arabic sentence with embedded English term: does the term render correctly inline?
  • An email or URL inside Arabic copy: does it stay LTR?
  • A number with Arabic surrounding text: does it stay readable?
  • A form with mixed-direction fields: does each field face the right way?
  • Switching languages mid-session: does the layout reflow cleanly with no visible jank?
  • I keep a small "RTL smoke test" page in every bilingual project: ten components rendered in both languages side by side. Before shipping, I look at it. It catches things I'd miss in production for months.

    Real Bugs I've Hit (So You Don't Have To)

  • A "send message" button with an arrow that pointed the wrong way in Arabic: caught by a user, not by me, three weeks after launch
  • An Arabic font that loaded with a 400 KB file even though the active language was English, because the preload was unconditional
  • A toast notification that animated in from the right in both languages, which made it feel correct in English and wrong in Arabic
  • A form field for phone numbers that flipped its placeholder around the +90 country code, producing "90+ سيلطس" instead of "+90 ..."
  • A dropdown menu that opened to the left of its trigger in Arabic, off-screen, because the absolute positioning used left: 0 instead of inset-inline-start: 0
  • Every one of those bugs was a one-line fix. The work was finding them.

    Key Takeaway

    If you're building for Arabic speakers, RTL support isn't a nice-to-have. It's a requirement. And it's an order of magnitude easier to build in from day one than to retrofit later. Pick logical properties from the very first commit, write a smoke-test page, ship with real Arabic content, and you'll save weeks of post-launch cleanup.

    The Arab market is enormous, underserved, and quietly hungry for products that respect their language. Get this right and you have a real edge.

    بناء تطبيقات React ثنائية اللغة: العربية والإنجليزية مع دعم RTL

    الخلاصة

    إضافة dir="rtl" لعنصر HTML توصلك إلى 60% من تطبيق جاهز للعربية وتعطيك إحساساً مزيّفاً بأنك انتهيت. الـ 40% الباقية هي تخطيط ينعكس صحيحاً، أيقونات تنقلب، نص عربي مختلط بإنجليزي لا يتشظّى عند السطر، خطوط عربية تُحمَّل دون اختناق الصفحة، وحقول نماذج تتعامل مع الأرقام وعلامات الترقيم بمنطق. الحلّ يقوم على خصائص CSS المنطقيّة (ms-، pe-، start-*)، واستخدام متغيّر rtl: في Tailwind لما يعتمد على الاتجاه، وتقييد الخطوط العربية باللغة النشطة، وتغليف المقاطع الإنجليزية المضمّنة بـ <bdi>، والاختبار بمحتوى عربي حقيقي لا بـ Lorem Ipsum معكوس. ابنِ هذا من اليوم الأول. الترميم على تطبيق LTR منجَز يأخذ وقتاً أطول من إعادة بناء طبقة التخطيط من الصفر.

    لماذا RTL أكثر من مجرد direction: rtl

    معظم الدروس تقول لك أن تضيف dir="rtl" لعنصر HTML وتنتهي. هذا يوصلك إلى 60% من الطريق. الـ 40% الباقية هي حيث تصبح الأمور صعبة، وحيث تفشل معظم التطبيقات التي تستهدف المتحدّثين بالعربية.

    شحنت قرابة عشرة تطبيقات عربية-إنجليزية في السنوات الثلاث الأخيرة: لطلّاب قبولي، لـ Maka Media، ولعملاء بين إسطنبول والرياض. الأوّل كلّفني أسابيع ترميم غير مجدولة لأنني تعاملت مع RTL كعَلَم إعداد لا كبُعد تصميم. الأخيرة لم تكلّف أيّ ترميم، لأنني صرت أبني للاتّجاهين من السطر الأوّل.

    هذا ما تمنّيت لو سلّمه لي أحد قبل ثلاث سنوات.

    التحديات الأساسيّة

    1. عكس التخطيط

    كل margin-left يصبح margin-right. كل padding-left يصبح padding-right. كل flex-row ينعكس بصرياً. كل عنصر مطلق الموضع مثبَّت بـ left: 0 ينتهي على الجانب الخطأ من الشاشة. لو ربطت قيماً اتجاهيّة في أي مكان في الكود، ستضطر لتغييرها كلها، وأسوأها ما نسيته، الذي يظهر في لقطة شاشة يرسلها لك العميل بعد الإطلاق بأسبوع.

    الحلّ: استخدم الخصائص المنطقيّة. في Tailwind يعني هذا ms-4 (margin-inline-start) بدل ml-4، وpe-6 بدل pr-6، وstart-0 بدل left-0، وend-2 بدل right-2. هذه تنقلب تلقائياً بحسب الاتجاه. في CSS العادي المكافئات الحديثة هي margin-inline-start وpadding-inline-end وinset-inline-start وما شابه.

    القاعدة التي صرت أتّبعها: كلمتا "left" و"right" أخطاء. كل مرة أراهما في اسم كلاس أو قاعدة style أعاملهما كرائحة كود تحتاج تبريراً.

    2. الأيقونات ذات الاتجاه

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

    الحلّ: أضف rtl:rotate-180 للأيقونات الاتجاهيّة في Tailwind، أو اكتب قاعدة CSS مثل [dir="rtl"] .directional-icon { transform: rotateY(180deg); }. لمكتبات الأيقونات، ابنِ غلافاً صغيراً <DirectionalIcon> يتولّى الأمر مرة واحدة.

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

    3. تحميل الخطوط

    الخطوط العربية أكبر بكثير من الخطوط اللاتينيّة: غالباً 3 إلى 5 أضعاف حجم الملفّ، لأن العربية تحتاج تنويعات Glyph كثيرة للروابط والأشكال السياقيّة. تحميل الخطّين مقدَّماً يؤذي الأداء على كل صفحة، حتى لمستخدمين يرون لغة واحدة.

    الحلّ: حمّل الخطّ العربي فقط حين تكون اللغة النشطة عربيّة، واللاتيني حين تكون إنجليزيّة. استعمل font-display: swap لتفادي قفز التخطيط، وعمل Preload للخطّ النشط في <head>. لو تخدم من CDN، اطلب فقط الـ Subset من الـ Glyphs التي تستخدمها فعلاً, لمعظم التطبيقات يمكنك تقليل الحجم 70% بالـ Subsetting.

    في مشاريعي عادة أستخدم Cairo أو IBM Plex Sans Arabic للعربية، وInter أو Geist للإنجليزيّة. أبدّل بينهما ديناميكياً حسب اللغة النشطة، ولا أشحن الاثنين دفعة واحدة أبداً.

    4. المحتوى المختلَط

    النصّ العربي مع مصطلحات تقنيّة إنجليزيّة مضمّنة ("تطبيق React مع TypeScript") يُنتج نصاً ثنائي الاتجاه. خوارزميّة Bidi في المتصفّح تتعامل مع معظم الحالات بشكل جيّد، لكن الأرقام وعلامات الترقيم والأقواس وعلامات الاقتباس قد تنقلب بطرق مفاجئة، خصوصاً حين تجاور الأقواس أو الشرطات.

    الحلّ: غلّف المقاطع الإنجليزيّة المضمّنة بـ <bdi> ليتعامل معها Bidi كوحدات معزولة. للأرقام في واجهات عربيّة، قرّر صراحةً هل تريد الأرقام الهنديّة (٠١٢٣) أم الغربية (0123) وطبّق قاعدة موحّدة. معظم المستخدمين العرب اليوم يفضّلون الأرقام الغربيّة للمحتوى التقنيّ، لكن الهنديّة ما زالت تبدو أصحّ في المحتوى التحريريّ.

    5. النماذج والحقول والأرقام

    حقول النماذج تستحقّ اهتماماً خاصاً. حقل type="email" يجب أن يبقى LTR دائماً حتى في صفحة عربيّة، لأن عناوين البريد LTR بطبيعتها. نفس الشيء للروابط وأرقام الهواتف وحقول كلمات السرّ. نموذج ثنائي اللغة يخطئ هذا يبدو معطوباً حتى لمستخدمين لا يستطيعون التعبير عن السبب.

    الحلّ: ضع dir="ltr" مباشرة على الحقول التي تحمل محتوى LTR، بصرف النظر عن اتّجاه الصفحة. معظم مكتبات النماذج تدعم هذا حالما تُعلمها.

    6. الحركات والانتقالات

    حركة Slide-in تدخل من اليسار في الإنجليزية يجب أن تدخل من اليمين في العربية، وإلا أحسّ المستخدم أنها مقلوبة. نفس الشيء لحركات السحب والأدراج وتنقّل الـ Carousel.

    الحلّ: استخدم قيم Transform منطقيّة حيث أمكن (transform: translateX() بقيمة مشتقّة من الاتجاه)، أو عرّف نسختين من الـ Keyframes, واحدة لـ LTR وأخرى لـ RTL, واختر عبر سمة [dir].

    مقاربتي

    أستخدم LanguageProvider كسياق React يقوم بـ:

  • تخزين اللغة الحاليّة في حالة React
  • توفير دالّة t() للترجمات
  • ضبط dir وlang على جذر الوثيقة عبر تأثير جانبيّ
  • تحميل الخطّ المناسب عند تغيير اللغة
  • حفظ اختيار المستخدم في localStorage واستعادته عند تحميل الصفحة
  • نسخة مبسَّطة تبدو هكذا:

    function LanguageProvider({ children }: { children: React.ReactNode }) {
      const [language, setLanguage] = useState<'en' | 'ar'>(() => {
        return (localStorage.getItem('lang') as 'en' | 'ar') ?? 'en';
      });
    
      useEffect(() => {
        document.documentElement.lang = language;
        document.documentElement.dir = language === 'ar' ? 'rtl' : 'ltr';
        localStorage.setItem('lang', language);
      }, [language]);
    
      return (
        <LanguageContext.Provider value={{ language, setLanguage, t: makeT(language) }}>
          {children}
        </LanguageContext.Provider>
      );
    }

    الترجمات تعيش في ملفّات JSON (en.json، ar.json) تُحمَّل عند الطلب أو تُستورَد ثابتة حسب كثافة المحتوى. للتطبيقات الصغيرة أستوردها ثابتة، وللكبيرة أحمّلها كسولاً حسب المسار.

    اختبار RTL

    اختبر دائماً بمحتوى عربيّ حقيقيّ. Lorem Ipsum معكوس لن يكشف مشاكل Bidi ولا مشاكل رسم الخطّ ولا انكسار الـ Ligatures ولا فشل التخطيط بسبب كلمات عربيّة أطول فعلاً ("registration" 12 حرفاً؛ "التسجيل" 7 أحرف لكنها تُرسم أعرض في بعض الخطوط).

    مصفوفة اختبار معقولة:

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

    أخطاء حقيقيّة وقعت بها (لتتفاداها)

  • زرّ "إرسال رسالة" بسهم يشير إلى الجهة الخطأ في العربيّة: التقطه مستخدم لا أنا، بعد الإطلاق بثلاثة أسابيع
  • خطّ عربيّ حُمّل بـ 400 كيلوبايت حتى حين كانت اللغة النشطة إنجليزيّة، لأن الـ Preload كان غير مشروط
  • إشعار Toast يدخل من اليمين في اللغتين، فبدا صحيحاً بالإنجليزيّة ومقلوباً بالعربيّة
  • حقل نموذج لأرقام الهواتف انقلب فيه نصّ الـ Placeholder حول رمز البلد +90، فأنتج "90+ سيلطس" بدل "+90 ..."
  • قائمة منسدلة فُتحت إلى يسار محرّكها في العربيّة خارج الشاشة، لأن الموضع المطلق استعمل left: 0 بدل inset-inline-start: 0
  • كلّ خطأ منها كان إصلاح سطر واحد. العمل كان في العثور عليها.

    الخلاصة العمليّة

    لو تبني للمتحدّثين بالعربيّة، دعم RTL ليس رفاهيّة. إنه متطلَّب. وهو أسهل ببناءِه من اليوم الأوّل بمراتب من ترميمه لاحقاً. اعتمد الخصائص المنطقيّة من أوّل Commit، اكتب صفحة اختبار دخان، أطلق بمحتوى عربيّ حقيقيّ، وستوفّر أسابيع من الترميم بعد الإطلاق.

    السوق العربيّ ضخم ومُهمَل وجائع بهدوء لمنتجات تحترم لغته. أتقن هذا تجد ميزة حقيقيّة.

    أسئلة شائعة

    ما الخطّ العربيّ الذي توصي به؟ للواجهات: Cairo أو IBM Plex Sans Arabic. للمحتوى التحريريّ الطويل: Tajawal أو Almarai. تجنّب Naskh التقليدية في واجهات المنتج, جميلة لكنّها متعِبة على الشاشة.

    كيف أتعامل مع لوحة المفاتيح؟ المستخدمون العرب يكتبون بإحدى اللغتين حسب السياق. لا تجبر تخطيط لوحة المفاتيح ولا تعيد توجيه الإدخال. اترك المتصفّح يتولّى.

    هل أحتاج إعادة تصميم كاملة للعربيّة؟ لا. التخطيط ينعكس آلياً مع الخصائص المنطقيّة. ما يحتاج تصميماً عربيّاً مخصّصاً هو الطباعة (أحجام، مسافات بين الأسطر) لا التخطيط.

    كيف أتعامل مع تواريخ التقويم؟ Intl.DateTimeFormat يعطيك تنسيقاً صحيحاً لـ ar-SA أو ar-EG. للأرقام، حدّد بصراحة Arabic-Indic أو Western واحفظ الاتّساق.

    هل اتّجاه RTL يؤثّر على Charts و Maps؟ نعم. مكتبات الرسوم البيانيّة عادة LTR بطبيعتها. اعكس المحاور والتسميات يدوياً، أو اختر مكتبة تدعم RTL أصلياً.

    أين أختبر RTL في CI؟ صفحة "اختبار دخان RTL" مع Playwright + Visual Regression تلتقط معظم التراجعات. شغّلها على كل PR.

    ملاحظة ختاميّة

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

    نصائح إضافيّة من الميدان

  • اختبر مع ناطقين أصليّين قبل الإطلاق. الترجمة الآليّة تعطي نصّاً نحوياً صحيحاً لكنّه غالباً بارد أو رسميّ أكثر من اللازم. مراجعة من ناطق أصليّ ترفع جودة المنتج بشكل ملحوظ.
  • احفظ لغة المستخدم في URL. نمط /ar/page و/en/page يساعد SEO ويتيح للمستخدمين مشاركة الروابط باللغة الصحيحة.
  • انتبه لأحجام الـ Buttons في العربيّة. كلمة "تسجيل الدخول" مثلاً أطول بصرياً من "Login". صمّم الأزرار لتتسع للأطول لا للأقصر.
  • لا تنسَ hreflang في الـ HTML. Google يستخدمها لتقديم النسخة الصحيحة لكلّ مستخدم. غيابها يهبط ترتيب نتائج البحث للنسخة العربيّة بشكل دراماتيكيّ.
  • اختبر أنماط الكتابة الثلاث في العربيّة. Fusha رسميّة للمحتوى التحريريّ، عاميّة للمحادثات، تقنيّة مع مصطلحات إنجليزيّة للمطوّرين. اعرف جمهورك واختر النمط الصحيح.