Blog/Behind the Code/Implementing RTL and Bilingual Support for UAE E-Commerce
POST
June 08, 2025
LAST UPDATEDJune 08, 2025

Implementing RTL and Bilingual Support for UAE E-Commerce

How we implemented full RTL layout support and Arabic-English bilingual functionality for a UAE e-commerce platform, covering CSS logical properties, font loading, and content management.

Tags

RTLi18nE-CommerceReact
Implementing RTL and Bilingual Support for UAE E-Commerce
11 min read

Implementing RTL and Bilingual Support for UAE E-Commerce

TL;DR

CSS logical properties and next-intl eliminated the vast majority of RTL-specific style overrides, letting us ship a fully bilingual Arabic-English e-commerce experience with a single codebase and no layout duplication. We handled structural RTL — not just CSS direction flipping — by rethinking component architecture, font loading, number formatting, and cultural UX patterns specific to the UAE market.

The Challenge

The client was a UAE-based e-commerce company expanding their English-only platform to serve the Arabic-speaking market. The UAE has a bilingual population — many users switch between Arabic and English depending on context. The platform needed to support both languages with seamless switching, not as an afterthought toggle but as a first-class experience.

The initial assumption was simple: add dir="rtl" to the HTML element and flip some CSS. That assumption was wrong in almost every way. RTL support for Arabic isn't just about mirroring layouts. Arabic text has different typographic characteristics — ligatures, contextual letter forms, different vertical metrics. Numbers in Arabic can be displayed as either Western Arabic (1, 2, 3) or Eastern Arabic numerals. Dates, currency formatting, and address structures follow different conventions. Even the UX patterns are culturally different — reading flow, visual hierarchy, and interaction expectations vary.

The existing codebase was a Next.js application with Tailwind CSS and around 200 components. Every component used physical CSS properties: margin-left, padding-right, text-align: left, border-left. Converting this to work in both directions without breaking the existing English experience was the core technical challenge.

Beyond CSS, the product catalog, category names, marketing banners, and customer support content all needed Arabic translations. Search had to work with Arabic morphology (Arabic words change form based on grammar). The checkout flow needed to handle Arabic names, UAE addresses, and local payment methods.

The Architecture

CSS Logical Properties Migration

The foundation of RTL support was migrating from physical CSS properties to logical properties. Instead of margin-left, we used margin-inline-start. Instead of padding-right, we used padding-inline-end. These properties automatically adapt based on the document's writing direction.

css
/* Before: Physical properties requiring RTL overrides */
.product-card {
  margin-left: 16px;
  padding-right: 24px;
  text-align: left;
  border-left: 2px solid #e5e7eb;
}
 
[dir='rtl'] .product-card {
  margin-left: 0;
  margin-right: 16px;
  padding-right: 0;
  padding-left: 24px;
  text-align: right;
  border-left: none;
  border-right: 2px solid #e5e7eb;
}
 
/* After: Logical properties — no RTL overrides needed */
.product-card {
  margin-inline-start: 16px;
  padding-inline-end: 24px;
  text-align: start;
  border-inline-start: 2px solid #e5e7eb;
}

We extended the Tailwind configuration to use logical property utilities throughout the codebase:

ts
// tailwind.config.ts
import type { Config } from 'tailwindcss';
import plugin from 'tailwindcss/plugin';
 
const config: Config = {
  theme: {
    extend: {},
  },
  plugins: [
    plugin(function ({ addUtilities }) {
      addUtilities({
        '.ms-0': { 'margin-inline-start': '0' },
        '.ms-1': { 'margin-inline-start': '0.25rem' },
        '.ms-2': { 'margin-inline-start': '0.5rem' },
        '.ms-4': { 'margin-inline-start': '1rem' },
        '.me-0': { 'margin-inline-end': '0' },
        '.me-1': { 'margin-inline-end': '0.25rem' },
        '.me-2': { 'margin-inline-end': '0.5rem' },
        '.me-4': { 'margin-inline-end': '1rem' },
        '.ps-0': { 'padding-inline-start': '0' },
        '.ps-2': { 'padding-inline-start': '0.5rem' },
        '.ps-4': { 'padding-inline-start': '1rem' },
        '.pe-0': { 'padding-inline-end': '0' },
        '.pe-2': { 'padding-inline-end': '0.5rem' },
        '.pe-4': { 'padding-inline-end': '1rem' },
        '.text-start': { 'text-align': 'start' },
        '.text-end': { 'text-align': 'end' },
        '.float-start': { float: 'inline-start' },
        '.float-end': { float: 'inline-end' },
        '.border-s': { 'border-inline-start-width': '1px' },
        '.border-e': { 'border-inline-end-width': '1px' },
        '.rounded-s': {
          'border-start-start-radius': '0.25rem',
          'border-end-start-radius': '0.25rem',
        },
        '.rounded-e': {
          'border-start-end-radius': '0.25rem',
          'border-end-end-radius': '0.25rem',
        },
      });
    }),
  ],
};
 
export default config;

The migration was methodical. We ran a codebase-wide search for physical properties (ml-, mr-, pl-, pr-, left-, right-, text-left, text-right) and replaced them with logical equivalents. This was tedious but straightforward, and the result was a single set of styles that worked in both directions.

Internationalization with next-intl

We chose next-intl for the internationalization framework. It integrated cleanly with Next.js App Router and provided message formatting, locale-aware routing, and React hooks for accessing translations.

ts
// i18n/request.ts
import { getRequestConfig } from 'next-intl/server';
 
export default getRequestConfig(async ({ locale }) => ({
  messages: (await import(`../messages/${locale}.json`)).default,
}));
json
// messages/en.json
{
  "common": {
    "addToCart": "Add to Cart",
    "outOfStock": "Out of Stock",
    "search": "Search products...",
    "currency": "{price, number, ::currency/AED}"
  },
  "product": {
    "description": "Description",
    "specifications": "Specifications",
    "reviews": "{count, plural, =0 {No reviews} one {1 review} other {# reviews}}",
    "deliveryEstimate": "Delivery in {days, plural, one {1 day} other {# days}}"
  },
  "checkout": {
    "title": "Checkout",
    "shippingAddress": "Shipping Address",
    "emirate": "Emirate",
    "area": "Area",
    "buildingName": "Building Name",
    "flatNumber": "Flat / Villa Number"
  }
}
json
// messages/ar.json
{
  "common": {
    "addToCart": "أضف إلى السلة",
    "outOfStock": "غير متوفر",
    "search": "ابحث عن المنتجات...",
    "currency": "{price, number, ::currency/AED}"
  },
  "product": {
    "description": "الوصف",
    "specifications": "المواصفات",
    "reviews": "{count, plural, =0 {لا توجد مراجعات} one {مراجعة واحدة} two {مراجعتان} few {# مراجعات} many {# مراجعة} other {# مراجعة}}",
    "deliveryEstimate": "التوصيل خلال {days, plural, one {يوم واحد} two {يومين} few {# أيام} many {# يومًا} other {# يوم}}"
  },
  "checkout": {
    "title": "الدفع",
    "shippingAddress": "عنوان التوصيل",
    "emirate": "الإمارة",
    "area": "المنطقة",
    "buildingName": "اسم المبنى",
    "flatNumber": "رقم الشقة / الفيلا"
  }
}

Arabic pluralization rules are far more complex than English. Arabic has six plural forms: zero, one, two, few (3-10), many (11-99), and other (100+). ICU MessageFormat handled this natively through the plural syntax. Getting this wrong would have produced grammatically incorrect Arabic text that native speakers would immediately notice.

Font Management

Arabic text requires dedicated fonts with proper glyph support. We couldn't just use Inter or any Latin-optimized font and hope for the best. Arabic has contextual letter forms — the same letter looks different depending on whether it appears at the beginning, middle, or end of a word, or standalone. Fonts need proper ligature tables for Arabic to render correctly.

tsx
// app/layout.tsx
import { Inter } from 'next/font/google';
import localFont from 'next/font/local';
 
const inter = Inter({
  subsets: ['latin'],
  variable: '--font-inter',
  display: 'swap',
});
 
const notoSansArabic = localFont({
  src: [
    {
      path: '../public/fonts/NotoSansArabic-Regular.woff2',
      weight: '400',
      style: 'normal',
    },
    {
      path: '../public/fonts/NotoSansArabic-Medium.woff2',
      weight: '500',
      style: 'normal',
    },
    {
      path: '../public/fonts/NotoSansArabic-Bold.woff2',
      weight: '700',
      style: 'normal',
    },
  ],
  variable: '--font-arabic',
  display: 'swap',
});
 
export default function RootLayout({
  children,
  params: { locale },
}: {
  children: React.ReactNode;
  params: { locale: string };
}) {
  const dir = locale === 'ar' ? 'rtl' : 'ltr';
  const fontClass = locale === 'ar'
    ? notoSansArabic.variable
    : inter.variable;
 
  return (
    <html lang={locale} dir={dir}>
      <body className={`${fontClass} font-sans`}>
        {children}
      </body>
    </html>
  );
}
css
/* globals.css */
:root {
  --line-height-body: 1.6;
  --letter-spacing-body: 0;
}
 
[dir='rtl'] {
  --line-height-body: 1.8; /* Arabic needs more line height */
  --letter-spacing-body: 0; /* Never add letter-spacing to Arabic */
  font-family: var(--font-arabic), sans-serif;
}
 
[dir='ltr'] {
  font-family: var(--font-inter), sans-serif;
}
 
body {
  line-height: var(--line-height-body);
  letter-spacing: var(--letter-spacing-body);
}

Arabic text needs more line height than Latin text because Arabic glyphs extend further above and below the baseline. And critically, letter-spacing should never be applied to Arabic text — it breaks the ligatures that connect letters within words, rendering the text illegible.

Structural RTL: Beyond CSS Direction

Some layout challenges couldn't be solved with CSS logical properties alone. They required structural changes in the component architecture.

Icon direction. Directional icons — arrows, chevrons, back buttons — needed to flip in RTL. A right-pointing arrow meaning "next" in LTR should point left in RTL. But not all icons flip: a checkmark, a phone icon, or a clock should look the same regardless of direction.

tsx
// components/DirectionalIcon.tsx
'use client';
 
import { useLocale } from 'next-intl';
 
interface DirectionalIconProps {
  icon: React.ReactNode;
  flip?: boolean;
  className?: string;
}
 
export function DirectionalIcon({
  icon,
  flip = true,
  className = '',
}: DirectionalIconProps) {
  const locale = useLocale();
  const isRtl = locale === 'ar';
  const shouldFlip = flip && isRtl;
 
  return (
    <span
      className={`inline-flex ${className}`}
      style={{ transform: shouldFlip ? 'scaleX(-1)' : undefined }}
    >
      {icon}
    </span>
  );
}

Number formatting. The platform displayed prices in AED (UAE Dirham). In English, prices appeared as "AED 150.00". In Arabic, the convention is "١٥٠٫٠٠ د.إ" using Eastern Arabic numerals with the currency symbol after the amount. We used Intl.NumberFormat for locale-aware formatting:

tsx
// lib/formatters.ts
export function formatPrice(amount: number, locale: string): string {
  return new Intl.NumberFormat(locale === 'ar' ? 'ar-AE' : 'en-AE', {
    style: 'currency',
    currency: 'AED',
    minimumFractionDigits: 2,
  }).format(amount);
}
 
export function formatDate(date: Date, locale: string): string {
  return new Intl.DateTimeFormat(locale === 'ar' ? 'ar-AE' : 'en-AE', {
    year: 'numeric',
    month: 'long',
    day: 'numeric',
  }).format(date);
}

Swipe direction. Product image carousels and swipeable cards needed reversed gesture direction in RTL. A swipe-left-to-see-next in LTR became swipe-right-to-see-next in RTL. We parameterized the swipe handler:

tsx
// hooks/useDirectionalSwipe.ts
import { useLocale } from 'next-intl';
 
export function useDirectionalSwipe() {
  const locale = useLocale();
  const isRtl = locale === 'ar';
 
  return {
    nextDirection: isRtl ? 'right' : 'left',
    prevDirection: isRtl ? 'left' : 'right',
    getSwipeHandler: (onNext: () => void, onPrev: () => void) => ({
      onSwipedLeft: () => (isRtl ? onPrev() : onNext()),
      onSwipedRight: () => (isRtl ? onNext() : onPrev()),
    }),
  };
}

Locale-Aware Routing

We used Next.js middleware to detect the user's preferred locale and route accordingly:

ts
// middleware.ts
import createMiddleware from 'next-intl/middleware';
 
export default createMiddleware({
  locales: ['en', 'ar'],
  defaultLocale: 'en',
  localeDetection: true,
  localePrefix: 'always', // /en/products, /ar/products
});
 
export const config = {
  matcher: ['/((?!api|_next|.*\\..*).*)'],
};

URLs were prefixed with the locale: /en/products/shoes and /ar/products/shoes. This gave each locale its own URL space, which was important for SEO — Google could index the Arabic and English versions separately and serve the right one based on the user's language preference.

A language switcher component let users toggle between Arabic and English. It preserved the current page path while switching the locale prefix:

tsx
// components/LanguageSwitcher.tsx
'use client';
 
import { useLocale } from 'next-intl';
import { usePathname, useRouter } from 'next-intl/client';
 
export function LanguageSwitcher() {
  const locale = useLocale();
  const pathname = usePathname();
  const router = useRouter();
 
  const toggleLocale = () => {
    const nextLocale = locale === 'en' ? 'ar' : 'en';
    router.replace(pathname, { locale: nextLocale });
  };
 
  return (
    <button
      onClick={toggleLocale}
      className="px-3 py-1 rounded border text-sm"
      aria-label={locale === 'en' ? 'Switch to Arabic' : 'التبديل إلى الإنجليزية'}
    >
      {locale === 'en' ? 'العربية' : 'English'}
    </button>
  );
}

Key Decisions & Trade-offs

CSS logical properties over a mirroring library. Libraries like rtlcss can automatically transform a LTR stylesheet into an RTL version at build time. We considered this but rejected it because it treats RTL as a transformation of LTR rather than a first-class layout mode. Automatic mirroring gets most things right but fails on edge cases — icons that shouldn't flip, horizontal progress bars, specific border treatments. Logical properties gave us correct behavior by default and made the intent clear in the code. The migration cost was high (touching every component), but the ongoing maintenance was minimal.

Locale in the URL path over cookies or headers. Putting the locale in the URL (/ar/products) rather than detecting it from cookies or Accept-Language headers was a deliberate SEO decision. Search engines need crawlable URLs to index language variants. The hreflang tags we added pointed to locale-specific URLs, and Google could index both versions independently. Cookie-based locale detection would have hidden the Arabic content from crawlers.

Conditional font loading over a single multilingual font. A font like Noto Sans supports both Latin and Arabic scripts. We could have used it universally. Instead, we loaded Inter for English and Noto Sans Arabic for Arabic because Inter is a better Latin font (tighter metrics, more refined glyphs) and Noto Sans Arabic is a better Arabic font. The tradeoff was managing two font stacks and ensuring weights matched across both, but the typographic quality was noticeably better.

Eastern Arabic numerals for Arabic locale. We debated whether Arabic UI should display Western numerals (1, 2, 3) or Eastern Arabic numerals. The UAE market uses both, but formal Arabic text traditionally uses Eastern Arabic numerals. We chose to follow Intl.NumberFormat's default for ar-AE, which produces Eastern Arabic numerals. Some users might prefer Western numerals, but staying consistent with the locale's conventions felt more correct than second-guessing the standard.

Manual translation over machine translation. Product descriptions were translated by human translators, not machine-translated. For UI strings, we used professional translators as well. Machine translation of Arabic, especially for e-commerce where product descriptions influence purchase decisions, wasn't reliable enough. The cost of human translation was significant, but the quality difference justified it.

Results & Outcomes

The Arabic version of the site launched without a separate codebase. One React component tree served both languages, eliminating the maintenance burden of keeping two versions in sync. When the English version got a new feature, the Arabic version got it too — the only additional work was adding translation strings.

The CSS logical properties approach proved its value during ongoing development. New components built with logical properties worked correctly in both directions without any RTL-specific testing or fixes. Developers stopped thinking about "LTR vs RTL" and just wrote components that adapted naturally.

Arabic-speaking customers engaged with the platform at rates comparable to English-speaking users. The cultural UX adaptations — proper number formatting, correct pluralization, appropriate typography — contributed to the Arabic experience feeling native rather than translated. Customer support tickets related to Arabic display issues dropped to near zero within the first month after launch.

SEO performance for Arabic search terms improved steadily after launch. Google indexed the Arabic pages with proper hreflang annotations, and the site began appearing in Arabic search results for product-related queries. The locale-in-URL strategy paid off directly in search visibility.

The language switcher saw frequent use, confirming the assumption that UAE users switch between languages regularly. Analytics showed that a meaningful percentage of sessions included at least one language switch, validating the investment in seamless bilingual support.

What I'd Do Differently

I'd adopt Tailwind CSS v3.3+'s built-in logical property utilities from the start. We wrote custom plugin utilities because at the time, Tailwind didn't have native support for ms-, me-, ps-, pe- utilities. Newer versions of Tailwind include these out of the box, which would have eliminated the custom plugin entirely.

I'd build a visual regression testing pipeline that runs every component in both LTR and RTL modes. We did manual RTL testing, which caught most issues but missed subtle problems like truncated text in RTL badges or misaligned icons in specific viewport widths. A tool like Chromatic or Percy configured to screenshot both directions would have caught these automatically.

I'd also explore the dir="auto" attribute more aggressively for user-generated content. Product reviews could be in either language, and we hardcoded the direction based on the page locale. If an English-speaking user left a review on the Arabic version of the site, their English text would be right-aligned. Using dir="auto" on user content containers would have let the browser detect the text direction automatically based on the first strong directional character.

FAQ

What are CSS logical properties and why do they matter for RTL?

CSS logical properties like margin-inline-start and padding-block-end adapt automatically based on text direction. Instead of writing separate LTR and RTL stylesheets, you write once and the browser handles directional flipping, dramatically reducing RTL-specific CSS. In our project, migrating to logical properties eliminated the need for [dir='rtl'] selector overrides on nearly every component. A margin-inline-start: 1rem applies to the left side in LTR and the right side in RTL without any additional code. This isn't just about convenience — it prevents an entire class of bugs where a developer adds a margin-left and forgets to add the corresponding RTL override. The code becomes direction-agnostic by default. Combined with Tailwind utility classes mapped to logical properties, developers didn't need to think about text direction at all during component development.

How do you handle Arabic fonts and typography?

Arabic text requires fonts that support Arabic glyphs with proper ligatures. We loaded Arabic-optimized fonts conditionally based on locale and adjusted line-height and letter-spacing since Arabic script has different vertical metrics than Latin characters. Specifically, we used Noto Sans Arabic for the Arabic locale and Inter for English. Arabic glyphs have taller ascenders and deeper descenders than Latin characters, so we increased the body line-height from 1.6 to 1.8 in RTL mode. Letter-spacing was set to zero for Arabic because any spacing between letters breaks the ligatures that connect characters within Arabic words, making the text unreadable. Font weights also needed careful mapping — Arabic "regular" and Latin "regular" have different visual weights, so we tested each weight pairing to ensure the Arabic and English versions of the same page had comparable visual density. Font files were loaded with display: swap to prevent invisible text during load, and Arabic fonts were only downloaded when the Arabic locale was active, keeping the English version's bundle unaffected.

How does bilingual content management work?

Product content is stored with separate fields for each language in the CMS. The API returns the appropriate language based on the request locale header, and the frontend uses next-intl for UI string translations with JSON message files per locale. Our product documents in MongoDB had title_en, title_ar, description_en, and description_ar fields. The API layer had middleware that read the Accept-Language header (or the locale from the URL path) and projected only the relevant language fields in the response, so the frontend received a clean title and description without needing to know about the bilingual storage structure. For UI strings — button labels, form labels, error messages, navigation items — we maintained JSON translation files per locale. These files were managed by translators through a simple Git-based workflow: developers added English strings, translators added Arabic equivalents in a separate file, and the build verified that all keys existed in both files. The next-intl library handled pluralization, number formatting, and date formatting using ICU MessageFormat syntax, which was essential for Arabic's six plural forms.

Collaboration

Need help with a project?

Let's Build It

I help startups and established companies design, build, and scale world-class digital products. From deep technical architecture to pixel-perfect UI — let's bring your vision to life.

SH

Article Author

Sadam Hussain

Senior Full Stack Developer

Senior Full Stack Developer with over 7 years of experience building React, Next.js, Node.js, TypeScript, and AI-powered web platforms.

Related Articles

Optimizing Core Web Vitals for e-Commerce
Mar 01, 202610 min read
SEO
Performance
Next.js

Optimizing Core Web Vitals for e-Commerce

Our journey to scoring 100 on Google PageSpeed Insights for a major Shopify-backed e-commerce platform.

Building an AI-Powered Interview Feedback System
Feb 22, 20269 min read
AI
LLM
Feedback

Building an AI-Powered Interview Feedback System

How we built an AI-powered system that analyzes mock interview recordings and generates structured feedback on communication, technical accuracy, and problem-solving approach using LLMs.

Migrating from Pages to App Router
Feb 15, 20268 min read
Next.js
Migration
Case Study

Migrating from Pages to App Router

A detailed post-mortem on migrating a massive enterprise dashboard from Next.js Pages Router to the App Router.