When a chat product is built in English first and Arabic is added later, the usual first step is to set dir="rtl" on the html element when the locale is Arabic. That flips the layout and gets the interface most of the way to looking right in a screenshot. It does not survive real conversations, because real conversations in the Gulf mix Arabic with English product names, order numbers, email addresses, links and code. The problems show up inside individual messages, and they are what users notice.
Set direction per message, not only per page
Page direction and message direction are different things. An Arabic-speaking user may write in English, an English interface may receive an Arabic message, and the assistant may reply in either. Each message bubble needs its own direction, decided from its own content.
The simplest tool is dir="auto", which tells the browser to take the direction from the first strong directional character in the element. That works for most typed messages. It is also why the composer, the box the user types into, should carry dir="auto": it then flips to right-to-left as soon as the user types an Arabic letter.
<li className="message" dir={message.dir ?? 'auto'}>
<p>{message.text}</p>
</li>
<textarea dir="auto" aria-label="Message" />The first-character trap
dir="auto" decides from the first strong character, and that is not always the language of the message. An Arabic reply that opens with a product name in Latin script will be laid out left to right. Digits and punctuation are weak or neutral, so they do not decide it, but a leading English word does.
For user messages this is usually acceptable. For assistant messages we can do better, because we know more: the conversation's language, the language the model was asked to reply in, or the balance of Arabic and Latin letters across the whole message. We compute the direction from that and set dir explicitly, instead of leaving it to the first character.
Streaming makes it worse
Chat interfaces stream tokens as they are generated, and a direction computed from the first few tokens can be wrong by the end of the message. Recomputing on every token makes the bubble jump from one side to the other mid-sentence, which is worse than either answer.
What works for us is to decide direction once, early, from the strongest signal available, which is usually the language the reply was requested in, and hold it for the life of the message. If a heuristic is needed, wait for a minimum number of letters before deciding, and never flip after the first decision.
Isolate the embedded runs
Inside a right-to-left paragraph, a left-to-right run such as an email address, an order reference with hyphens or a phone number can reorder the punctuation around it in surprising ways. The Unicode bidirectional algorithm is doing what it was designed to do, but the result can put a full stop or a bracket at the wrong end of the run.
The fix is isolation. Wrap anything that is not part of the sentence's own language in an element that isolates its direction: the bdi element in HTML, or unicode-bidi: isolate on an inline element in CSS. Mentions, file names, order numbers and inline code are the usual candidates. If the model's output is rendered from Markdown, the renderer is the right place to apply it.
.message :is(code, .mention, .ref) {
unicode-bidi: isolate;
}
.message pre {
direction: ltr;
text-align: left;
}Code blocks are a special case. Source code reads left to right whatever language surrounds it, so pre blocks should be forced to ltr explicitly.
Use logical properties everywhere
Physical CSS properties cause most of the layout bugs that remain after the flip. margin-left, padding-right, left: 0 and text-align: left mean the same thing in both directions, which is exactly wrong. Their logical equivalents, margin-inline-start, padding-inline-end, inset-inline-start and text-align: start, follow the direction of the element they are on. In Tailwind these are the ms-, me-, ps-, pe-, start- and end- utilities.
The benefit is not only at page level. Because direction is set per message, a bubble's avatar, timestamp and tail should follow that message's direction, and logical properties do that without any extra conditionals.
Mirror the right icons, and only those
Icons that point in the direction of reading should mirror: back and forward chevrons, the send arrow, reply arrows and anything that suggests progress along a line. Icons that depict real-world objects or carry a fixed meaning should not: a clock, a tick, a microphone, media playback controls and brand logos. A per-icon flag is cleaner than a global transform, and a rule such as [dir="rtl"] .icon-directional { transform: scaleX(-1); } keeps the decision visible in one place.
Digits are a choice, not a default
Arabic text can be written with Western Arabic digits (0 to 9) or Eastern Arabic digits (٠ to ٩), and usage varies by country, by context and by the client's brand. The digits you get by default from Intl.NumberFormat or toLocaleString for an Arabic locale depend on the exact locale tag and on the browser's locale data, which has changed between versions.
Do not leave it to the default. Agree the digit style with the client and request it explicitly, with the numberingSystem option or a Unicode extension on the locale tag, so the same build renders the same digits in every browser.
const amount = new Intl.NumberFormat('ar-AE-u-nu-latn', {
style: 'currency',
currency: 'AED',
}).format(1250);-u-nu-latn extension pins Western Arabic digits; -u-nu-arab pins Eastern Arabic ones.Apply the same rule to dates and times. A timestamp in one digit system next to an amount in the other looks like a bug, because it is one.
Type and spacing
Arabic script is cursive: letters join, and their shapes change with position in the word. Letter-spacing breaks those joins, so any tracking applied to labels and buttons in the Latin design must be removed for Arabic. Use a typeface with proper Arabic shaping rather than relying on whatever the system falls back to, and give Arabic text a little more line height, since the marks above and below the letters need room.
Test with real text
Placeholder Arabic and machine-translated strings hide most of these problems. The cases that break interfaces are the mixed ones: an Arabic sentence with an English brand at the start, a phone number at the end, an order reference in brackets. We collect real examples, with permission, from the client's existing support channels before building, and ask a native reader to review the interface with them.

