RTL That Actually Works
Mirroring a layout is the easy half. These are the parts that broke.
Adding Arabic to this site meant supporting right-to-left, and I assumed that was a direction attribute and an afternoon. The attribute took a minute. The afternoon went to everything that direction attribute does not touch, and to the handful of things it flips that should have stayed put.
01Stop Writing Left and Right
The single highest-value change was replacing every physical direction in my styles with a logical one. Margin-left becomes margin-inline-start. Padding-right becomes padding-inline-end. Text-align left becomes text-align start. In Tailwind this is the difference between ml-4 and ms-4, between pr-6 and pe-6, between text-left and text-start. Once a rule is written in logical terms, the browser resolves it against the document direction and the mirroring happens for free. No duplicate stylesheet, no direction-prefixed variants, no conditional class names threaded through components. The reason this matters more than it sounds is maintenance. A codebase with direction-aware overrides scattered through it needs every new component reviewed twice, once per direction, forever. A codebase written in logical properties needs the review once. I did the conversion with a find-and-replace and a careful read of the diff, and the number of places that genuinely needed a physical direction afterward was three. Borders on a decorative divider, and two icons I will come back to.
02Not Everything Should Mirror
Direction flips the reading axis, not the meaning of every symbol. A back arrow points toward where you came from, so it mirrors. A play button points forward in time regardless of script, so it does not. Volume sliders, progress bars for media, clock faces, and anything representing a real-world object that does not itself mirror should stay exactly where they are. I got this wrong on the card links first: the arrow that means read more correctly flips to point left in Arabic, so I swapped the icon component based on direction. But I also had a chevron on an external link badge that means opens elsewhere rather than continues forward, and flipping that one just made it look broken. Numbers are the other trap. Digits inside right-to-left text still run left to right, and mixed content like a version number or a date inside an Arabic sentence can render in a surprising order if the surrounding markup does not isolate it. The bidirectional isolation element exists for exactly this and is worth knowing before you need it.
03Typography Is Not a Detail
Arabic and Latin scripts have genuinely different metrics, and using one font stack for both is the fastest way to make a bilingual site look unfinished. Arabic letterforms are connected, they carry more vertical complexity, and they need noticeably more line height to stay readable at the same font size. A line height that feels generous in English will look cramped in Arabic. I load a separate font per language and swap the class at the layout level, where the locale is already known, so the entire tree gets the right stack without any component knowing about it. The same applies to weight. A font weight that reads as confident in a Latin heading can read as heavy and cluttered in Arabic, because the connected strokes fill more of the available space. I ended up dialing Arabic headings down a step. None of this is visible if you only ever test with placeholder text, which is the real lesson: test with actual sentences, at actual lengths, in both languages, or you will ship a layout that only works in the language you wrote it in.
04Catching Regressions
The failure mode with bidirectional support is not a dramatic break, it is slow decay. You add a component in a hurry, reach for ml-2 because it is what your fingers know, and nothing looks wrong because you were testing in English. Three weeks later the Arabic side has a dozen small misalignments and no single commit to blame. Two things helped. First, a lint rule that rejects physical direction utilities outright, which turns a code review comment into a build error and removes the judgment call. Second, a habit rather than a tool: every visual change gets checked in both directions before it is committed, and switching languages is one click in the navbar precisely because I made it one click. The friction of verifying has to be lower than the temptation to skip it. I would not claim this is rigorous. A real test suite would screenshot both directions and diff them. For a portfolio, a lint rule and a habit have caught everything so far.
Takeaways
Right-to-left support is not a translation problem, it is a layout discipline. Write logical properties by default, be deliberate about the few things that should not mirror, give each script its own typography, and make it trivial to check both directions. Do that and the second direction stops being a feature you maintain and becomes something the browser handles for you.