All lessons

Lesson 12·Unit 4 · Scripts and layout·Unicode bidi algorithm & directional isolates·Lv 301·~10 min

Bidirectional text: RTL + LTR mixed strings

Half a billion people read right-to-left. The Unicode bidirectional algorithm mixes LTR and RTL gracefully, until you interpolate a brand name, price, or timestamp and the order comes out wrong. Invisible control characters earn their keep here.

UnicodebidiFSI/PDI

By the end

  • ·Explain how a neutral character like $ ends up on the wrong side of a number in RTL text.
  • ·Isolate an interpolated value with FSI/PDI, <bdi>, or dir="auto", and say why the older LRE/PDF embeddings lost.
  • ·Predict what dir="auto" will decide: the first strongly-directional character wins.

The problem

Arabic, Hebrew, Farsi, and Urdu read from right to left. Numbers and embedded English (brand names, prices, codes) keep their own direction, which the browser weaves into the surrounding paragraph. That weaving is fine for static text, but when your app interpolates a dynamic value ($999) into a translated string, the order can come out wrong. Most teams discover the bug only when a Saudi or Israeli user files a screenshot showing the price in the wrong place.

How it works

Why the dollar sign wanders

The Unicode bidirectional algorithm gives every character a directional class. Letters are strong: Arabic and Hebrew letters run right-to-left, Latin letters left-to-right. Digits are weak: they have a direction but little influence over their surroundings. Punctuation, spaces, and currency symbols are neutral: they take the direction of whatever surrounds them.

That last rule is the entire bug class. Interpolate "$999" into an Arabic sentence and the $ sits between an Arabic letter and a digit. Because it is neutral, it resolves toward its strong neighbors and lands on the wrong side of the number. The algorithm did its designed job with neighbors you did not plan for. That is also why the bug is content-dependent: a username that starts with a Latin letter and one that starts with a digit can lay out the same template differently.

The fix is to change the neighborhood, not the algorithm. Isolates (FSI…PDI characters, HTML's <bdi> or dir="auto", CSS unicode-bidi: isolate) tell the engine: lay this span out by its own rules, and treat the whole thing as one neutral block from the outside. Wrap every interpolated variable and no user-supplied value can restructure your sentence again.

See it yourself

Try these, in order

Each step triggers a specific failure you should recognize on sight.

  1. 1
    Keep the preset “Arabic with English brand name” and set the strategy to “Plain interpolation”. Compare the two output boxes: without isolation the price and date scramble into the Arabic sentence, and the $ drifts to the wrong side of 999.
  2. 2
    Switch the strategy to “FSI / PDI (recommended)”. Same template, fixed rendering. The raw-bytes panel shows [FSI]…[PDI] fencing each variable: invisible characters doing the layout work.
  3. 3
    Load the preset “Code snippet inside RTL paragraph” with plain interpolation. The rendering mangles getValue('user.name'), and the parentheses appear to flip: Unicode mirrors brackets in RTL context. Code inside RTL prose always needs isolation.
  4. 4
    Edit the {price} field: try “$999”, then “999 USD”. Un-isolated, the rendering changes depending on which neighbor is strong: proof that the variable's display depends on content you do not control.

Output · with FSI / PDI (recommended)

اشتر ⁨iPhone 14 Pro⁩ الآن بسعر ⁨$999⁩ فقط، عرض حتى ⁨12/31⁩.

This box renders with dir="rtl", lang="ar". The browser runs Unicode TR9.

For comparison · without isolation

اشتر iPhone 14 Pro الآن بسعر $999 فقط، عرض حتى 12/31.

Watch the order of brand/price/date flip against the box above. Translator-delivered strings that lack U+2068 fail this way.

The raw byte sequence

اشتر [FSI]iPhone 14 Pro[PDI] الآن بسعر [FSI]$999[PDI] فقط، عرض حتى [FSI]12/31[PDI].

Cheat sheet

FSI/PDI: the modern "first strong character" isolation. Use this for any user-supplied or interpolated value. Some frameworks wrap values automatically (Android's BidiFormatter, Closure Templates). Most web MessageFormat runtimes do not, so insert the marks yourself.

LRE/RLE: legacy embedding marks. They appear in older codebases. New code should use FSI/PDI.

LRM/RLM: single invisible direction hints. Place them around punctuation at the edge of an RTL sentence. Trailing periods and parentheses then land on the correct side.

If you remember one thing

Wrap every interpolated variable in FSI/PDI isolates. Neutral characters ($, digits, punctuation) take direction from their neighbors. Never leave them to guess.

What to do about it

  • Wrap every interpolated value in a <bdi> element in HTML; that's a one-line fix and the browser handles the rest.
  • For non-HTML contexts (push notifications, log lines, generated images), wrap the variable in FSI / PDI control characters: `\u2068${value}\u2069`.
  • Set dir="auto" on user-generated content fields. The browser will pick the right direction per item without you needing to guess.
  • For mirroring layout (margins, icons, gutters), use logical CSS properties (padding-inline-start, margin-inline-end) instead of padding-left.

Use this with

Stakeholders

EngineeringDesignQA

Moments

  • ·RTL launch readiness review
  • ·When design hands off a flow that includes pricing or branded text
  • ·Component library audit

Field note

The typical bug is a currency symbol on the wrong side: “$999” rendering as “999$” inside an Arabic sentence. The dollar sign and the digits are directionally weak, so the surrounding RTL text captures them. It renders fine in your English screenshots and breaks only in the locales the review never opened.

W3C: Inline markup and bidirectional text

Quick check

3 questions · pass at 2+

  1. Question 1/3

    Why does “$999” often render as “999$” when interpolated into Arabic text without protection?

  2. Question 2/3

    Which mechanism does Unicode recommend for embedding an opposite-direction fragment (like a brand name) in text today?

  3. Question 3/3

    How does dir="auto" decide the direction of an element?

Words you'll hear

Where to read more

Related lessons