Posted on Oct 10
The first time I shipped an Arabic locale, I thought the work was translation plus one TextDirection. The translation was the easy half. What took the longest was everything that had quietly assumed text ran left to right.
Here is what actually broke, and what I do now.
Hard-coded sides
This is the bulk of it, and it is boring, mechanical work that you cannot skip.
Anywhere your layout says left or right, it is asserting a physical direction. What you almost always mean is start or end — the side the reader begins on, whichever side that is.
In Flutter that means EdgeInsetsDirectional instead of EdgeInsets, AlignmentDirectional instead of Alignment, and start/end instead of left/right on anything that takes an alignment. On the web it is margin-inline-start instead of margin-left, padding-inline-end, inset-inline-start, and text-align: start.
The failure mode is nasty because it is partial. The screen does not break; it goes subtly wrong. Labels drift away from the fields they belong to. Icons sit on the wrong edge of a row. Nothing throws. You just end up with an interface that feels slightly broken to the people using it and looks fine to you.
The lesson: make it a lint rule, not a code review habit. Ban the physical properties outright in anything that renders UI, and you stop re-fixing this every sprint.
Things that should not mirror
Mirroring everything is as wrong as mirroring nothing, and it is the mistake people make once they have discovered the problem and overcorrect.
Media playback controls do not flip — play still points the way it always did. Clocks run the same direction. Phone numbers, credit card numbers, ISBNs and version strings keep their order. Logos keep their orientation. Charts with a time axis usually keep time running the way your audience reads charts, which is not always the way they read sentences, and is worth asking about rather than assuming.
What does mirror: anything expressing sequence or direction of travel in the reading flow. Back and forward arrows. Progress bars. Breadcrumbs. Indentation. The side a drawer slides in from.
The lesson: the question is never "is this app RTL?" It is "does this particular element encode reading order?" Most do. Some very much do not.
Classify the assets, not the components
A reader suggested a better framing than the one I started with, and I think it is right: instead of deciding directionality at each place an icon is used, classify every asset once, into four buckets — mirror, invariant, custom RTL (a separately drawn version, because mirroring the original produces something wrong), and unknown.
The fourth bucket is the one doing the work. Without it, anything untriaged silently inherits a default, and silent defaults are precisely how a play button ends up pointing the wrong way in production. With it, "unknown" is a state you can fail a build on.
One refinement from doing this across a real icon set: directionality sometimes belongs to the usage rather than the asset. The same arrow glyph mirrors when it means "next" and does not when it means "increase." So let a usage site override the asset's classification — but require that the override be written down explicitly, never inherited.
Keep the classification in asset metadata, a manifest entry or a filename convention, and have the rendering layer read it. Then no individual component is deciding directionality on its own, which was the actual problem.
Mixed-direction strings
This is the one that produces genuinely unreadable output, and it is invisible until it happens.
The moment you interpolate a Latin-script value into an Arabic sentence — a username, a file name, a product code, a URL — the Unicode bidirectional algorithm has to guess where that run begins and ends. When it guesses wrong, trailing punctuation jumps to the other side of the string and the sentence becomes nonsense.
The fix is to isolate the embedded run explicitly, with FSI (U+2068) before and PDI (U+2069) after. Most i18n libraries will do this for you if you let them format the string rather than concatenating it yourself. That is the real rule: never build a sentence with +. Pass the value to the formatter and let it wrap the isolate.
Test this specifically. A good test string is an Arabic sentence with a Latin username and a trailing question mark, because a wrong answer is immediately visible.
Numerals
Arabic locales may use Western digits (0123) or Arabic-Indic digits (٠١٢٣), and which one is correct depends on the region, not just the language. Do not hard-code either. Format numbers through the locale's number formatter and let it decide, and remember that this applies to dates, durations, prices and ordinals too — not only to the obvious integers.
Fonts
Your Latin font almost certainly does not cover Arabic, and the fallback you get when it doesn't is rarely the one you want. Pick the Arabic face deliberately and check it at your smallest size, where the difference between a good and a bad Arabic font is most obvious.
Line height is the other trap. Arabic script generally needs more vertical room than Latin at the same point size. A line height tuned for English will clip descenders and make dense paragraphs look cramped. Set it per script, not globally.
How I test it now
I stopped relying on reading the screens, because I cannot read Arabic and neither can most of the people reviewing a pull request.
Two things that work without language knowledge:
Pseudo-localization with forced RTL. Render the interface with direction forced right-to-left and strings replaced by a marked-up version of the English — padded for length, wrapped in brackets, accented. Every layout bug shows up immediately, in a build anyone on the team can read. This catches the hard-coded-sides class of bug almost completely, before a translator sees anything.
Screenshot diffing. Capture the same screens in both directions and look at them side by side. You are not reading the text; you are checking that the mirror image is structurally sensible. Wrong icons and stranded labels are obvious in that view and invisible in a code diff.
Neither needs you to speak the language. Both find the bugs that embarrass you.
If you only take one thing: ban left and right from your layout code today, before you have an RTL locale. It is a one-hour change now and a two-week change later, and it costs nothing in the meantime.
Top comments (0)
For further actions, you may consider blocking this person and/or reporting abuse
