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-09-10, 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.

Phase 1 codemod series complete.

Phase 2 batch 1 — #430 (merged): Toast logical positions (physical values
kept as deprecated aliases) + FloatingWindow. The ratchet baseline is now
zero — any new physical-direction class in src/components fails CI unless
annotated with // rtl-ignore -- <reason>. The scanner now prints per-offense
logical-equivalent suggestions and inline PR annotations on failure. #430 also
fixed missing Tailwind 4 --animate-* theme tokens (the Toast slide-in
animations never fired under TW4; remaining animation-token gaps are a
candidate follow-up issue).

Phase 2 batch 2 — #438 (merged): Directional navigation icons mirror in RTL
via rtl:-scale-x-100 across 16 components — pagination/breadcrumb chevrons,
back buttons (SiteHeader, PatientHeader, CaseManagementHeader,
ProviderDetailHeader, CodeLookup, ComposerModelSelector), calendar month nav
(DateRangePicker, DateInput), submenu/drill/expand chevrons (Dropdown, Sidebar,
HealthSurveillance, PermissionsEditor), and link arrows (GlossaryTooltip,
SectionSpyNav). Per Material bidirectionality guidance, checkmarks, media
controls, and up/down chevrons deliberately do not flip. Also fixed a
physical inline marginLeft in HealthSurveillance (inline styles evade the
scanner) and rewrote 11 stale story RTL notes that predated Phase 1.

Remaining Phase 2 work (keyboard nav, JS positioning remainder, LTR islands,
transforms remainder, third-party RTL flags) continues below.

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) — ✅ complete (#357–#429)

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)

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, … (Messaging + AI done in feat(rtl): migrate Messaging + AI to logical properties #380; DocumentScanner + MediaEditor + TranscriptView done in feat(rtl): migrate DocumentScanner, MediaEditor, TranscriptView to logical properties #425; billing/invoices done in feat(rtl): migrate billing/invoice components to logical properties #426; services/employer done in feat(rtl): migrate services/employer components to logical properties #427; scheduling + clinical + orders done in feat(rtl): migrate scheduling and clinical components to logical properties #428; shell/misc — SiteHeader, CommandPalette, CookieConsent, SuperChat, FileManager, dialogs, and everything else — done in feat(rtl): migrate shell and misc components to logical properties #429; Toast + FloatingWindow done in feat(rtl): logical Toast positions, FloatingWindow cleanup, zero the ratchet baseline #430; directional icon mirroring done in feat(rtl): mirror directional navigation icons in RTL (Phase 2 batch 2) #438)
  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)

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

enhancementNew feature or request

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions