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.
Recommended pairing (proposal, pending brand identity)¶
| 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: 0wheneverlang="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:
- Set
dirandlangat the highest correct level:<html lang="ar" dir="rtl">, then override per element only when embedding the other language. - 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. - 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 Flutterintlhandle this when values are formatted through the library, never by string concatenation. - Phone numbers: always
<bdi dir="ltr">+965 9876 5432</bdi>. - KWD amounts: the number is LTR; the currency symbol position follows locale:
en:KWD 1,250.500ar:1,250.500 د.ك(symbol after the amount, per Arabic convention) — rendered viaIntl.NumberFormat('ar-KW', { style: 'currency', currency: 'KWD' })/ DartNumberFormat.currency(locale: 'ar_KW'). Never hand-assemble currency strings. KWD always shows 3 decimals.- Punctuation localizes: Arabic comma
،, question mark؟, and percent placement come from the translation file, not code. - Truncation/ellipsis of bidi strings must use CSS
text-overflow/ FlutterTextOverflow.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_localeon the profile (server-side — emails, push, and SMS use it); guests via cookie/local storage. - Web routing: locale-prefixed paths (
/ar/...,/en/...) vianext-intl;aris the default locale. Correcthreflangpairs 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_localeat send time, not at signup time.
6. Content authoring rules¶
- 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.
- Bilingual structured fields. Vendor-facing structured content (business name, service names, category names, policies) has
*_arand*_enfields. Vendor onboarding requires at least one; admins/CMS require both for platform-owned content (categories, plans, CMS pages). - Length budget: designs must tolerate ±40% string length. German-style stress tests apply: verify every button/label with the longer of the two languages.
- 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. - No text in images. All promotional banners produced in both languages via CMS.
- Terminology glossary is law: RFQ = طلب عرض سعر، Quote = عرض سعر، Booking = حجز، Deposit = عربون، Verified = موثّق، Event = مناسبة. Maintained in Glossary; string reviews check against it.
- 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
arat 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/rightCSS physical properties introduced (lint:stylelint-use-logical); noEdgeInsets.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 (
langattributes correct).
Related: Design Principles · Design Tokens · Key Screens