Design system/Fundamentals
RTL and languages
Flowra speaks nine languages, and two of them, Persian and Arabic, are written right to left. Layout is written once, with logical properties, so it sits correctly in both directions on its own.
In codelib/locales/langs.tscomponents/lang-gate.tsxcomponents/step-arrow.tsx
Nine languages, two directions
Languages come from one list in lib/locales/langs.ts: English, German, Turkish, Persian, Arabic, French, Armenian, Portuguese, and Russian. Each language keeps its direction, number locale, and tab title right there, and no other part of the app keeps a separate list of languages.
The server always renders Persian, because it can't read the choice saved in the browser. LangGate keeps the browser's first render in Persian too, then switches to the saved language; that's how the page changes without a hydration error.
Right to left
Left to right
One markup, two directions: spacing is logical, the arrow mirrors, and the progress bar starts where the text starts.
Logical properties
Instead of left and right, we use inline-start and inline-end: padding-inline, margin-inline-start, inset-inline-end. These are measured from where the text starts, so one rule is correct in both directions.
Do
margin-inline-start and inset-inline-end for anything that moves with the text.
Don't
margin-left for spacing between an icon and text; in Persian it lands on the wrong side.
Do
Physical left and right for images; a cluster of objects doesn't flip with the text.
Don't
Mirroring a chart or logo with the page direction.
Icons and arrows
An icon with a direction, like a "Next" or "Back" arrow, gets the .rtl-flip class and is mirrored in right-to-left. For a previous and next pair, StepArrow takes the direction of movement, not the side of the screen, and the buttons always come in previous, next order in the DOM.
Icons without a direction, like a clock or search, aren't mirrored.
<Icon d={paths.arrowRight} size={14} className="rtl-flip" />
<StepArrow dir="next" />Mixed text
Code, URLs, email addresses, hex colors, and card numbers always get dir="ltr", even in the middle of a Persian sentence. A Latin name or phrase inside Persian text goes inside bdi so the punctuation around it doesn't shift. Six-digit verification code boxes are left to right in both languages, because digits are read left to right in Persian too.
Pitfalls
- Gradients have no logical direction; in a mirrored layout, flip the angle by hand under html[dir="rtl"].
- html[dir="rtl"] .a .b is more specific than .a .b:nth-child(1); for every directional rule, write both the right-to-left and left-to-right versions explicitly.
- In right-to-left, scrollLeft is zero at the start edge and goes negative toward the left.
- Nothing should read getLang() during render; trLang() and uiDir() are safe for rendering.
What mirrors and what doesn't
[dir="rtl"] .rtl-flip { transform: scaleX(-1) }getComputedStyle(anchor).directionIn right-to-left, “start” is the right edgertl ? r.right - width : r.leftdata-dir="rtl" | "ltr"Physical left/right with arrow keysdir="ltr"In both languagesinset-inline-start: 20px- A floating field gets its direction from uiDir() or the dir prop, and lays out the icon, label, and trailing control with physical left and right under data-dir, so an LTR field inside a Persian page is correct too.
- The on/off switch (.toggle) doesn't mirror: the knob, with left: 2.5px and a positive translateX, moves right in both directions.
- The toast is centered with left: 50% and translateX(-50%); it's symmetric and needs no direction.
- The bottom sheet stretches with inset-inline: 0 and is identical in both directions.
Do
Position the floating panel based on the anchor's own direction.
Don't
From the page's dir; an LTR field in the middle of a Persian page sends the menu the wrong way.
Do
One icon with .rtl-flip.
Don't
A language check in JSX to pick a left or right arrow.
Related pages