Design System · 23 of 25
Internationalization & RTL
Logical properties, mirrored icons, bidi text, and locale-aware formatting.
Logical CSS properties
Write CSS once; it works in LTR and RTL.
Physical properties (margin-left, padding-right, border-top, left, right) assume a writing direction. Logical properties (margin-inline-start, padding-inline-end, border-block-start, inset-inline-start, inset-inline-end) map to the correct physical side based on the element's direction and writing-mode. In LTR, inline-start = left; in RTL, inline-start = right. This single change makes your entire spacing, border, and positioning system bidirectional.
Rules
Never use
left,right,margin-left,margin-right,padding-left,padding-right.Use
inline-start,inline-end,block-start,block-endfor all spacing/positioning.Use
border-inline-start,border-inline-endfor borders.Use
text-align: start/end(notleft/right).Use
float: inline-start/inline-end(avoid float; prefer flex/grid).
```css
/* Physical — breaks in RTL */
.card {
margin-left: 16px;
padding-right: 24px;
border-top: 1px solid var(--color-rule);
}
/* Logical — works in LTR and RTL */
.card {
margin-inline-start: 16px;
padding-inline-end: 24px;
border-block-start: 1px solid var(--color-rule);
}
```RTL layout patterns
Flex and grid reverse automatically; you handle the exceptions.
Setting dir="rtl" on <html> (or a subtree) flips the default flow direction. Flexbox and CSS Grid children automatically reverse order (flex-direction: row becomes right-to-left). Most layouts "just work" — but exceptions need attention: absolute positioning (inset-inline-start), transforms, custom scrollbars, and third-party components that hard-code directions.
Rules
Test every page with
<html dir="rtl">in dev.Flex/Grid: children reverse automatically — no code change needed.
Absolute positioning: use
inset-inline-start/inset-inline-end.Transforms:
translateXflips sign in RTL — use logical or conditional logic.Third-party components: wrap with
dir="ltr"if they don't support RTL.
```css
/* Root RTL switch */
:root {
--direction: ltr;
}
[dir="rtl"] {
--direction: rtl;
}
.layout {
display: flex;
flex-direction: row; /* logical: inline */
}
/* In RTL, flex children automatically reverse */
```Icon mirroring
Directional icons flip; universal icons do not.
Not every icon should mirror in RTL. Directional icons (chevrons, arrows, back/next, carousel controls, progress direction) must flip. Universal icons (home, user, settings, search, heart, star, brand logos, checkmarks, close X) must not. The safest approach: tag each icon in the registry with mirrored: boolean, and let the Icon component apply transform: scaleX(-1) automatically when dir="rtl".
Rules
Registry: every icon has
mirrored: true(directional) orfalse(universal).Icon component reads
dirfrom context and appliesscaleX(-1)when needed.Test every icon in both directions.
Never mirror: logos, hearts, stars, user, home, search, checkmark, close.
Always mirror: chevrons, arrows, back/forward, carousel prev/next, progress.
```tsx
function Icon({ name, mirrored = false }: IconProps) {
const dir = useDirection(); // from context
const shouldMirror = mirrored && dir === 'rtl';
return (
<svg style={{ transform: shouldMirror ? 'scaleX(-1)' : 'none' }}>
<use href={`#icon-${name}`} />
</svg>
);
}
```Bidi text & font fallbacks
Mixed LTR/RTL content needs explicit control.
Real-world content mixes scripts: Arabic UI with English product names, Hebrew with numbers, Japanese with Latin acronyms. The Unicode Bidirectional Algorithm handles most cases, but you need explicit controls for edge cases: unicode-bidi: isolate on user-generated content, direction: rtl on RTL-only containers. Font fallback chains must include fonts for every supported script — a missing glyph shows as ☐ (tofu).
Rules
Wrap user content in
unicode-bidi: isolate.Define font stacks per script: Latin, Arabic, Hebrew, Devanagari, CJK, etc.
Use
font-familyfallback chain: Latin first, then script-specific fonts.Test with real mixed-content strings (e.g., "iPhone 15 Pro سعر").
```css
:root {
--font-latin: 'Inter', system-ui, sans-serif;
--font-arabic: 'Noto Sans Arabic', 'Amiri', system-ui;
--font-hebrew: 'Noto Sans Hebrew', 'David', system-ui;
--font-devanagari: 'Noto Sans Devanagari', 'Hind', system-ui;
}
body {
font-family: var(--font-latin), var(--font-arabic), var(--font-hebrew), var(--font-devanagari);
}
```
```ts
new Intl.NumberFormat('ar-EG').format(1234); // "١٬٢٣٤"
```Locale-aware formatting
Never hard-code date, number, or currency formats.
Every locale has its own conventions for dates (DD/MM/YYYY vs MM/DD/YYYY vs YYYY/MM/DD), numbers (1,234.56 vs 1.234,56 vs 1 234,56), currencies ($1,234.56 vs 1.234,56 € vs ¥1,234), and relative time ("yesterday" vs "ayer" vs "أمس"). The Intl API handles all of this — pass the user's locale and the desired style.
Rules
Dates:
Intl.DateTimeFormat(locale, { dateStyle: "medium" }).Numbers:
Intl.NumberFormat(locale).Currencies:
Intl.NumberFormat(locale, { style: "currency", currency }).Relative time:
Intl.RelativeTimeFormat(locale, { numeric: "auto" }).Plurals:
Intl.PluralRules(locale).select(n)— handle all plural categories.Never concatenate formatted pieces — format the whole message at once.
```ts
// Hard-coded US format
const date = `${month}/${day}/${year}`;
const price = `$${amount.toFixed(2)}`;
// Locale-aware
const date = new Intl.DateTimeFormat(locale, { dateStyle: 'medium' }).format(dateObj);
const price = new Intl.NumberFormat(locale, { style: 'currency', currency }).format(amount);
const relative = new Intl.RelativeTimeFormat(locale, { numeric: 'auto' }).format(-1, 'day'); // "yesterday"
```
```ts
const rules = new Intl.PluralRules(locale);
rules.select(0); // 'zero' in Arabic, 'other' in English
rules.select(1); // 'one'
rules.select(2); // 'two' in Arabic, 'other' in English
```I18n/RTL checklist
Before shipping a feature.
Logical properties
All spacing/borders/positioning uses inline-start/end, block-start/end. No left/right.
RTL layout test
Every page tested with <html dir="rtl">. Flex/Grid auto-reverse works. Exceptions handled.
Icon mirroring
Registry tags mirrored/non-mirrored. Icon component auto-flips in RTL.
Text alignment
start/end used everywhere. No text-align: left/right.
Font fallbacks
Font stack includes Arabic, Hebrew, Devanagari, CJK fallbacks. No tofu.
Intl formatting
Dates, numbers, currencies, relative time use Intl with user locale.
Pluralization
Intl.PluralRules or i18n library handles all plural categories per locale.
Bidi testing
Mixed LTR/RTL content tested. unicode-bidi: isolate on user content.
Numerals
Eastern Arabic numerals for ar/fa/ur locales via Intl.NumberFormat.
RTL QA pass
Arabic, Hebrew, Persian, Urdu QA pass before release.
# I18n/RTL checklist
Before shipping a feature:
- [ ] All spacing uses logical properties (`margin-inline-start`, not `margin-left`)
- [ ] Layout tested in `dir="rtl"` (flip `<html dir="rtl" />` in dev tools)
- [ ] Icons tagged as mirrored/non-mirrored; `Icon` component handles flip
- [ ] Text alignment uses `start`/`end` (not `left`/`right`)
- [ ] Font stack includes fallbacks for all supported scripts
- [ ] Dates, numbers, currencies formatted via `Intl` with user locale
- [ ] Pluralization handled via `Intl.PluralRules` or i18n library
- [ ] Bidi text tested with mixed LTR/RTL content
- [ ] Eastern Arabic numerals used for Arabic locales
- [ ] RTL language QA pass (Arabic, Hebrew, Persian, Urdu) before release
**Supported locales (example)**: en, es, fr, de, ja, zh-CN, zh-TW, ko, ar, he, fa, ur, hi, pt-BR