Skip to content

Arabic-First & RTL Guide

The definitive reference for building Yam3at's bilingual (ar/en), bidirectional UI across Next.js (App Router + next-intl) and Flutter. Arabic is the design-first language; English is fully equal. Every rule here applies to MVP unless tagged otherwise.

1. Layout mirroring: what flips, what doesn't

The entire layout mirrors in Arabic: reading order, navigation, alignment, and flow all run right→left. But mirroring is not a blanket transform: scaleX(-1) — some elements keep their LTR form because their meaning is not directional text.

Flips in RTL

Element RTL behavior
Page grid, columns, cards Mirror. First column is rightmost.
Navigation drawers, bottom-nav order Drawer opens from the right; nav items ordered right→left.
Back/forward chevrons and arrows Flip. "Back" points right in Arabic.
Breadcrumbs, steppers, progress bars Flow right→left. Step 1 of the RFQ wizard is on the right; progress fills right→left.
Text alignment Right-aligned body text; form labels right-aligned above/beside fields.
Icons implying reading/flow direction Flip: send (paper plane), reply, list indent, undo/redo, "next" arrows.
Sliders (e.g. budget range) Min value at the right, increases leftward.
Table column order Mirrors; the "name" column is rightmost.
Padding/margin logic Use logical properties only: margin-inline-start, padding-inline-end, border-start-start-radius. Never left/right in CSS; in Flutter use EdgeInsetsDirectional, AlignmentDirectional.

Does NOT flip

Element Why
Numbers and math Digits always read left→right, even inside Arabic text. 1,250.500 never mirrors.
Phone numbers Always LTR: +965 9876 5432. Wrap in a bidi isolate (see §3).
Media playback controls Play ▶, timeline scrubbers, and elapsed time run left→right worldwide; users' muscle memory comes from YouTube/Instagram. Volume sliders keep LTR.
Clocks, timers, countdowns 02:45:10 stays LTR.
Progress that encodes quantity, not sequence A rating bar chart (5★ ████) may mirror position but the fill direction of numeric meters (upload %, password strength) is acceptable LTR — pick one and be consistent: Yam3at mirrors fill direction with layout, but the numeric label stays LTR.
Brand logos and wordmarks Yam3at logo, KNET, Visa, Apple Pay — never mirrored.
Checkmarks ✓, physical-world icons Check, clock face, search magnifier, camera, map pin — universal, not directional.
Maps Geography doesn't mirror. Map UI controls reposition to the mirrored corner, but the map itself is untouched.
Code, URLs, email addresses Always LTR, isolated.
Charts (vendor analytics) Time axes stay left→right (time convention); labels and legends localize. Revisit per-chart in Scale analytics work.

Icon audit rule

Every icon added to the library gets a mirrors: yes/no flag in the design tokens/icon manifest. Default is no; only direction-of-flow icons opt in.

2. Arabic typography

Arabic script is proportionally wider, has no capitals, carries diacritic marks above/below the baseline, and needs more vertical air.

Role Arabic Latin fallback / pairing Notes
UI & body IBM Plex Sans Arabic IBM Plex Sans → system-ui Neutral, excellent legibility at 14–16 px, 7 weights, open license, harmonized Latin in the same family — one family token covers both scripts.
Display / headings (optional flavor) Noto Kufi Arabic Same size-adjusted Latin (IBM Plex Sans SemiBold) Geometric Kufi warmth for hero/marketing surfaces only; never for body or forms.
Numerals in data tables Tabular figures of the UI face Enable font-feature-settings: "tnum" where offered for price columns.

CSS stack: font-family: var(--font-family-arabic), var(--font-family-latin), system-ui, sans-serif; — the Arabic face leads so Latin fragments inside Arabic strings still harmonize. Flutter: bundle both families; set per-locale fontFamilyFallback.

Typography rules

  • Line height: Arabic body needs 1.7–1.8 (vs 1.5 for English) to clear ascenders/descenders and optional diacritics. Headings ≥ 1.4. Tokens carry per-locale values (Design Tokens).
  • No italics in Arabic — the style doesn't exist in the script. Emphasis = weight or color. If English copy uses italics, the Arabic equivalent uses SemiBold.
  • No letter-spacing on Arabic — tracking breaks the connected script. letter-spacing: 0 whenever lang="ar".
  • No ALL-CAPS equivalent — English caps-style labels (e.g. section eyebrows) render in Arabic as regular case with weight/color treatment.
  • Slightly larger minimum size: Arabic body minimum 15–16 px where English tolerates 14 px; small print (legal, timestamps) minimum 13 px Arabic.
  • Underlines collide with descenders and dots — avoid; links use color + weight.
  • Test every text style with the string لتجربة النص العربي مع التشكيل: مُبَارَك to verify diacritic clearance.

3. Bidirectional text handling

Mixed-direction strings are Yam3at's daily reality: Arabic messages containing English vendor names, KWD amounts, phone numbers, URLs.

Rules:

  1. Set dir and lang at the highest correct level: <html lang="ar" dir="rtl">, then override per element only when embedding the other language.
  2. User-generated content (messages, reviews, RFQ notes, vendor descriptions): render with dir="auto" (first-strong detection) — a customer may type English inside the Arabic app.
  3. Isolate every injected variable. Interpolated values (names, amounts, phones, emails) get bidi isolation — HTML <bdi> or Unicode FSI/PDI (U+2068/U+2069) — so neighbor punctuation doesn't jump. next-intl/ICU and Flutter intl handle this when values are formatted through the library, never by string concatenation.
  4. Phone numbers: always <bdi dir="ltr">+965 9876 5432</bdi>.
  5. KWD amounts: the number is LTR; the currency symbol position follows locale:
  6. en: KWD 1,250.500
  7. ar: 1,250.500 د.ك (symbol after the amount, per Arabic convention) — rendered via Intl.NumberFormat('ar-KW', { style: 'currency', currency: 'KWD' }) / Dart NumberFormat.currency(locale: 'ar_KW'). Never hand-assemble currency strings. KWD always shows 3 decimals.
  8. Punctuation localizes: Arabic comma ،, question mark ؟, and percent placement come from the translation file, not code.
  9. Truncation/ellipsis of bidi strings must use CSS text-overflow / Flutter TextOverflow.ellipsis — never substring-cutting, which can strand direction marks.

4. Dates, numbers, and digits

Decision Choice Rationale
Calendar Gregorian primary everywhere (event dates, bookings, invoices). Kuwait business runs Gregorian.
Hijri awareness Show Hijri as secondary annotation where culturally load-bearing: event date picker header and event dashboard show 15 مارس 2026 (25 رمضان 1447); flag Ramadan/Eid windows in vendor calendars. MVP: annotation on date picker; Scale (KSA launch): full dual-calendar picker. Religious occasions and Ramadan scheduling drive GCC event timing.
Digits Latin (Western Arabic) digits 0-9 in both locales by default. Kuwaiti convention: prices, phones, and plates use Latin digits; mixing Arabic-Indic ٠-٩ with KNET receipts and phone dialers creates errors. Implement via explicit NumberFormat with nu-latn; keep the door open for a per-user Arabic-Indic preference in Scale (KSA users may expect ٠-٩).
Date format ar: ١٥ مارس 2026 → rendered 15 مارس 2026 (Latin digits, Arabic month names); en: 15 Mar 2026. Relative dates for activity ("منذ ٣ ساعات" → "منذ 3 ساعات" / "3h ago").
Week start Sunday in Kuwait; weekend = Fri–Sat. Calendars, date pickers, and vendor availability grids must reflect this in both locales.
First day/format source Always from CLDR via Intl / Dart intl with locale ar-KW / en-KW — not hardcoded. Multi-country readiness (KSA, UAE differ).

5. Locale switching UX

  • Language choice is offered at first launch (mobile) / first visit (web), defaulting from device/browser locale; Arabic listed first (العربية / English, each label in its own language).
  • Switcher lives in Profile → Settings and in the web header/footer. One tap, instant, no restart; scroll position preserved where feasible.
  • Persistence: authenticated users store preferred_locale on the profile (server-side — emails, push, and SMS use it); guests via cookie/local storage.
  • Web routing: locale-prefixed paths (/ar/..., /en/...) via next-intl; ar is the default locale. Correct hreflang pairs on every indexable page (vendor profiles, categories).
  • Switching language never changes data: prices, dates, and vendor content re-render in the same truth. Vendor-authored content displays in the language the vendor wrote (with both-language fields where structured — see §6); it is not machine-translated silently.
  • Notifications/emails follow preferred_locale at send time, not at signup time.

6. Content authoring rules

  1. Author, don't translate. UI copy is written natively in each language against a shared intent brief. Arabic microcopy uses Modern Standard Arabic with a warm, light Gulf flavor for celebratory moments (e.g. "يمعة" itself); English is plain and direct. No transliterated Arabish in UI text.
  2. Bilingual structured fields. Vendor-facing structured content (business name, service names, category names, policies) has *_ar and *_en fields. Vendor onboarding requires at least one; admins/CMS require both for platform-owned content (categories, plans, CMS pages).
  3. Length budget: designs must tolerate ±40% string length. German-style stress tests apply: verify every button/label with the longer of the two languages.
  4. ICU everywhere. Arabic has six plural categories (zero/one/two/few/many/other). Every count string uses ICU plurals — {count, plural, zero{لا عروض} one{عرض واحد} two{عرضان} few{# عروض} many{# عرضًا} other{# عرض}}. Never "عروض: " + n.
  5. No text in images. All promotional banners produced in both languages via CMS.
  6. Terminology glossary is law: RFQ = طلب عرض سعر، Quote = عرض سعر، Booking = حجز، Deposit = عربون، Verified = موثّق، Event = مناسبة. Maintained in Glossary; string reviews check against it.
  7. Keys are English, semantic, and namespaced (rfq.wizard.step2.title); no key reuse across contexts even if today's strings match.

7. RTL testing checklist

Run per feature, per platform, before merge:

  • Full flow walked in ar at 360 px and desktop; layout mirrors, nothing overlaps or truncates meaning.
  • Icons audit: directional icons flipped, non-directional (media, check, brand, maps) untouched.
  • Numbers, phone numbers, KWD amounts render LTR and unbroken inside Arabic sentences (test: تم دفع العربون 1,250.500 د.ك على الرقم +965 9876 5432).
  • Mixed-direction user content: paste an English sentence into an Arabic message/review field and vice versa; dir="auto" holds.
  • Plurals: counts 0, 1, 2, 3, 11, 100 all render correct Arabic forms.
  • Date picker: Arabic month names, Latin digits, Fri–Sat weekend shading, Hijri annotation where specified.
  • Keyboard/focus order follows RTL visual order (web); swipe-back gesture direction correct (Flutter iOS).
  • Line height and diacritics: no clipped Arabic ascenders in buttons, chips, or single-line list items.
  • No left/right CSS physical properties introduced (lint: stylelint-use-logical); no EdgeInsets.only(left:...) in Flutter (lint rule).
  • Language switch mid-flow (e.g. mid-RFQ-wizard) preserves entered data.
  • Push notification, email, and SMS templates verified in both languages with a bidi amount inside.
  • Screen reader pass (VoiceOver/TalkBack) reads Arabic labels with Arabic voice (lang attributes correct).

Related: Design Principles · Design Tokens · Key Screens