RTL Is Not a Translation Problem
If you serve Persian speakers — or any second language — i18n is a system problem, not a string problem. Most teams translate the strings and hope the layout survives. That is how you get a product that feels broken in your own market.
When I asked for Persian support on this site, I asked for it wrong at first. I said: add the language, flip the direction, use the right font. The result was a site where Persian pages existed but the design system did not. Breadcrumbs pointed the wrong way. Flex rows refused to mirror. Icons faced the wrong direction. It felt translated, not built.
Translation is a content problem. RTL is a design-system problem. Fix the second or the first is invisible.
IRANSansX: Not a Font, a Requirement
Persian has visual density English does not. The font had to carry it.
The site already ran Outfit for Latin and JetBrains Mono for code. Persian needed its own variable font, IRANSansX, loaded from local assets with a specific configuration: the DOTS axis set to 7.
The font was restricted to Arabic and Persian unicode ranges so it never competes with Outfit. Two fonts, zero overlap, one design system.
If the font is a bolt-on, the text will look like a bolt-on. Persian is denser; it needs the heavier axis.
Direction Is a Token, Not a Hack
Zero hardcoded ml/mr/pl/pr. Everything logical, everything mirrored.
The audit rule was absolute: no physical utilities anywhere. No ml, no mr, no pl, no pr, no left, no right. Every margin and padding uses the logical variants — ms, me, ps, pe — so direction flips automatically with dir="rtl".
Logical utilities
ms/me/ps/pe/start/end everywhere. The same class works in both directions.
Nav centering
Absolute centering with left-1/2 -translate-x-1/2 that survives direction changes.
Row reversal
sm:flex-row becomes row-reverse in RTL so justify-between lands correctly.
The day you hardcode a margin for one direction is the day you ship a second site. Logical properties are the only path.
The Audit That Found the Cracks
Double-flips, unmirrored arrows, and code blocks that broke under bidi.
A line-by-line audit across all pages found the bugs that live in the details. The nav had a double flip: flex-row-reverse plus dir="rtl" cancelling each other. The footer had the same bug. Slider arrows pointed the wrong way. Code blocks — which must always stay LTR — were getting mangled by bidirectional text.
RTL done right is invisible. When it is wrong, every Persian visitor feels the site was built for someone else.
The fixes were systematic, not guessed: every RTL-sensitive rule audited, every directional icon flipped with a shared rtl-flip class, every flex row verified in both directions. If you localize with AI, remember: the model can translate your strings, but it cannot redesign your layout. That part is yours.
Ship RTL the Right Way
If you are localizing for the Iranian market, start at the system, not the strings.
Design the font first. Persian needs its own variable font with the correct dot mode and weight mapping. A Latin font stretched into Persian is a bug.
Ban physical utilities. Grep for ml/mr/pl/pr/left/right and eliminate them. Logical properties are the contract.
Audit the details. Nav, footer, arrows, code blocks, breadcrumbs. The layout bugs hide in the components you touch least.
Persian support is a design-system project with a translation deliverable. Build it that way.
The site does not support Persian. The design system does.
A variable font, logical utilities, and an audit that found the double-flips. RTL is the default, not the exception.
See the bilingual architecture