Skip to content

Add RTL (right-to-left) language support across the component library #319

Description

@garrity-miepub

Add RTL (right-to-left) language support across the component library

Summary

Support RTL languages (Arabic, Hebrew, Farsi, Urdu) at the component level.
Components should be direction-agnostic by default using CSS logical
properties, with a Storybook toolbar switch (alongside brand / theme / density /
language) to preview RTL.

PR series status (updated 2026-08-07, supersedes #320)

Work lands as a series of small targeted PRs. Each merge cuts a 0.7.2-dev.x
release for in-the-wild validation; full release after the series completes.

Then Phase 2 (icon flips, JS positioning, LTR islands, third-party RTL flags) as follow-ups.

Current state (measured 2026-07-21)

  • ~206 component .tsx files; 137 contain physical-direction Tailwind classes
    (ml-, pr-, left-, text-left, rounded-l, border-l, space-x-, …)
  • Only 3 logical-property usages exist today
  • Tailwind 4 — native logical utilities (ms-/me-/ps-/pe-/start-/end-/
    text-start/rounded-s) and rtl:/ltr: variants are available
  • Storybook toolbar globals (brand/theme/density/locale) live in
    .storybook/preview.tsx with a shared applyGlobalTheme() — the right place
    to also set dir
  • No dir/direction infrastructure exists anywhere yet

Guiding principle

Logical properties first, rtl: variants last. Make components
direction-agnostic so RTL "just works" from dir="rtl". Reserve rtl:
overrides for true exceptions (icon flips, transforms). Avoid doubling the
class surface with per-direction overrides.

Phase 0 — Infrastructure (one PR, do first) — ✅ shipped in #356

  • Add direction Storybook global (ltr / rtl / auto) next to theme/density/locale
    • auto derives direction from the locale global (rtl for ar, he, fa, ur)
    • Wire into applyGlobalTheme() to set dir on document.documentElement (works on docs-only MDX pages, same pattern as dark mode)
    • Add an RTL sample locale (e.g. العربية (sample)) to the locale toolbar for real RTL text
  • Runtime direction API: useDirection() hook + isRtlLocale/RTL_LOCALES for JS-level direction logic (keyboard arrows, popover placement, drag math). Default: read from closest dir attribute — zero setup for consumers who set ``
  • Enforcement: CI script (scripts/rtl-scan.mjs ratchet, like ccme-scan.mjs) that fails on new physical-direction classes in src/components — prevents regression while migration is in flight

Phase 1 — Mechanical codemod (bulk of the 137 files)

Scripted, reviewable find/replace over className strings, run per component
batch
(small PRs — see PR series status above):

Physical Logical
ml-* / mr-* ms-* / me-*
pl-* / pr-* ps-* / pe-*
left-* / right-* start-* / end-*
text-left / text-right text-start / text-end
rounded-l* / rounded-r* / corners rounded-s* / rounded-e* / rounded-ss
border-l* / border-r* border-s* / border-e*
space-x-* prefer flex gap-*; else rtl:space-x-reverse
divide-x-* rtl:divide-x-reverse

Phase 2 — Manual/semantic pass (needs judgment, per component)

  • Icons & chevrons — back/forward arrows, breadcrumb/accordion/pagination chevrons flip (rtl:-scale-x-100); checkmarks, media buttons, logos do NOT flip
  • Keyboard navigation — ArrowLeft/ArrowRight invert in RTL: Tabs, Dropdown, Slider, DateRangePicker, ScheduleCalendar (via useDirection())
  • Transforms & animationstranslate-x-*, slide-in-from-left, Switch thumb travel, Sheet/Sidebar slide directions
  • JS positioning — FloatingWindow, Tooltip, Dropdown, Toast placement (top-right → logical "top-end")
  • LTR islands — phone numbers, dates, codes, URLs stay LTR inside RTL: PhoneInput, DateInput, WebsiteInput, CountryCodeDropdown, numeric table cells get explicit dir="ltr"
  • Third-party integrations (each may become its own ticket): AG Grid (enableRtl), DataVis/charts, RichTextEditor/Kerebron, ESheet renderer, YChart

Phase 3 — Rollout order

  1. Primitives: Button, Input, Badge, Checkbox, Radio, Switch, Select, Label, Text, Tooltip, … (feat(rtl): migrate primitives to logical properties — Badge, Input, Label, Text, Switch #357; components without physical classes needed no changes)
  2. Layout & navigation: Card, Modal, Sheet, Sidebar, Tabs, Breadcrumb, Pagination, Table, AppHeader, … (feat(rtl): migrate layout + nav to logical properties — AppHeader, Card, Sheet, Sidebar, Table #358; Modal/Tabs/Breadcrumb/Pagination had no physical classes — Sheet/Sidebar slide transforms handled with rtl: mirrors)
  3. Composite/feature modules: OrderEditor, ScheduleCalendar, Messaging, Dashboard, FileManager, …
  4. Third-party wrappers: AGGrid, DataVis, ESheet, RichEditor, YChart (individually, last)

Phase 4 — Verification & guardrails

  • Extend Playwright visual suite: snapshot a curated story set in both dir values (2× matrix only for direction-sensitive components)
  • Per-component RTL checklist in PR template: mirrored layout / icons flip correctly / keyboard inverted / LTR islands preserved / no horizontal scrollbar leaks
  • Docs: add "direction-agnostic styling" section to copilot-instructions / lessons so future components are born RTL-safe

First milestone

Phase 0 + codemod on ~10 primitives, verified with the new toolbar switch.
Proves the full pipeline (switch → logical classes → visual check → lint guard)
before scaling to the remaining ~125 files.

Metadata

Metadata

Labels

enhancementNew feature or request

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions