Numbers inside RTL text
Learn which numeric separators preserve order in Arabic text, when ranges differ from identifiers, and how to isolate values without forcing all numbers LTR.
By Omran Khleifat, Founder of Ritla
Guide13 min read
A scan-batch identifier can show its groups in the wrong order beside an Arabic label while the page count immediately below it displays correctly. Both contain Western digits. The difference is the separator and what the value represents, not whether the whole page is RTL.
Under normal bidi processing, a digit sequence keeps its internal order, but several numeric groups can be arranged according to the surrounding sentence. Slashes, colons, commas and full stops can keep numeric content together in the Arabic contexts described here. Hyphens and spaces do not provide the same guarantee.
Do not force every number LTR as a blanket repair. A reference has a fixed group order; an Arabic range can correctly present its starting endpoint at the right. Decide what the reader needs to identify before choosing a directional boundary.
Start with the value's meaning#
The examples use an archive digitization dashboard. It shows scan-batch references, page counts, processing times, scanner versions and page ranges. Those fields share numeric characters but do not share one direction policy.
Consider this batch reference:
<p dir="rtl">معرف دفعة المسح: 84720635-9184</p>The identifier's recorded group order is 84720635-9184. A staff member comparing it with the archive's batch record should see that sequence. After the Arabic label, the displayed groups appear as 9184-84720635, which changes the visible identifier.
Now consider a page-count range defined as starting at the smaller value:
<p dir="rtl">نطاق عدد الصفحات: 240-760</p>The range displays from the leftmost to rightmost edge as 760-240. An Arabic reader starting at the right encounters 240 first, then 760. That can correctly express the intended range. It is not the same requirement as preserving an identifier's groups from left to right.
These concrete display rules come from the brief's measurements in Chromium 151, last checked on 2026-09-24. Repeat the fixtures in supported Firefox and Safari versions rather than assuming those measurements cover every engine.
Write the acceptance criterion in semantic terms. “The batch identifier matches its authoritative reference” is testable. “Every hyphen must look like English” would incorrectly group ranges and identifiers together.
Digits have weak direction, not a universal LTR boundary#
Western digits have the Unicode bidi type EN, European Number. Arabic-Indic digits use a different numeric bidi type. Neither behaves like a strong Latin word that automatically establishes an LTR context for everything around it.
In normal processing, each numeric group retains its internal digit order. The browser can still reorder separate groups and punctuation around them. That is why an eight-digit group can remain intact while appearing after another group.
The original data is not rewritten by this display operation. A copied value or a DOM text value can retain the recorded order even when the screen shows a different group sequence. W3C's bidi basics explains numeric runs and the distinction between logical and visual order.
Avoid the absolute claim that digits “always” display left to right. A directional override can suppress their normal behavior. If even the characters inside an ordinary digit group look reversed, inspect overrides and source data before diagnosing the familiar separator problem.
Alignment is separate too. Moving a number to the right edge of its box does not tell the browser which characters form one independent value. A right-aligned identifier can still have reversed groups.
Arabic context changes processing, not the digit script#
After Arabic letters, Unicode rule W2 can change Western digits' working bidi type from EN to AN, Arabic Number. This is directional processing, not conversion to Arabic-Indic characters.
The batch group 84720635 remains the same Western-digit string. The type name does not mean a font has substituted a different numbering system, and it does not mean the API returned Arabic-Indic digits.
The Unicode Bidirectional Algorithm defines the weak-type and separator rules. For debugging, the distinction matters because three different operations can be mistaken for each other: formatting a number, choosing its digit script and laying out the resulting text.
If the product changes a numbering system through a formatter, the resulting characters can change. If only the surrounding paragraph changes from English to Arabic, the working directional context can change without changing those characters at all.
The W2 condition is specifically about Arabic-type letters. It should not be described as an identical effect after every RTL script. Do not generalize a fixture with an Arabic label to a Hebrew label solely because both paragraphs are RTL.
Separators that keep these values together#
The following fixtures use Western digits after Arabic text, ordinary ASCII separators and no overrides. In that context, their numeric pieces stay together in written order:
| Dashboard field | Written value | Displayed numeric sequence |
|---|---|---|
| Completion date | 08/03/2029 | 08/03/2029 |
| Processing time | 07:46 | 07:46 |
| Grouped page count | 8,736 | 8,736 |
| Scanner software version | 6.3.8 | 6.3.8 |
These are fixture values, not a recommendation for one date convention or software version. The point is that slash, colon, comma and full-stop separators behave differently from the hyphen in the batch identifier.
Test them with actual labels:
<section dir="rtl"> <p>تاريخ الإنجاز: 08/03/2029</p> <p>وقت المعالجة: 07:46</p> <p>عدد الصفحات: 8,736</p> <p>إصدار الماسح: 6.3.8</p></section>Do not add an override to these already intact numeric sequences. If the date is culturally inappropriate, the time format is wrong or the version is stale, those are different content issues. A direction change will not correct them.
The table also does not establish that every similar-looking punctuation character behaves identically. A localized thousands separator, a nonbreaking space or another dash is a different input. Inspect the characters the formatter actually emitted before applying a rule written for ASCII punctuation.
Hyphens need a meaning-aware decision#
After Arabic text, the hyphen between the batch reference's groups does not hold them as one left-to-right identifier. A plain span changes nothing:
Problem
<p dir="rtl">معرف دفعة المسح: <span>84720635-9184</span></p>The reference displays as 9184-84720635. That conflicts with its defined identifier order.
Better
<p dir="rtl">معرف دفعة المسح: <span dir="ltr">84720635-9184</span></p>The reference now displays as 84720635-9184. The value owns an explicit direction boundary; the Arabic label remains outside it. W3C's inline markup guidance covers this use of dir on inserted content.
Do not apply the same change automatically to the page-count range. Ask whether its intended reading order is Arabic prose, a technical notation with fixed presentation or some other product convention. The same visible 760-240 is not sufficient evidence for the same bug.
A range can also be expressed through localized words rather than a compact hyphen notation if that is clearer for the product. That is a message-design decision. It should not require reversing the stored endpoints or changing which value is minimum and which is maximum.
Moving the identifier into a bare RTL cell may make a hyphenated fixture look different: without preceding Arabic in the paragraph, its groups can display in written order. That is a changed context, not proof that every read-only presentation of the reference is fixed.
Spaces split numeric groups even without an Arabic label#
An alternative presentation of the same batch groups uses a space:
<p dir="rtl">84720635 9184</p>It displays as 9184 84720635. Unlike the Arabic-label condition for the hyphen example, ordinary spaced Western-digit groups reverse their group order in an RTL paragraph even when no Arabic letters precede them.
This catches a misleading reduction of a bug report. A developer removes the label, sees that the number remains reordered and concludes the surrounding paragraph cannot be responsible. The missing factor is the space-separated shape.
If the space-separated representation is required, isolate it as one value:
<p dir="rtl">معرف دفعة المسح: <span dir="ltr">84720635 9184</span></p>Do not isolate each group separately. The complete representation is the field's unit. Breaking it into independent directional pieces can preserve the very inter-group uncertainty the component needs to remove.
Do not remove the space merely to make a fixture pass unless the product wants a different format. Readability, identifier grammar and direction are separate decisions. Likewise, do not assume a nonbreaking space is automatically a directional fix; use a fixture with its actual code point and verify the result.
Phone numbers combine this group problem with a leading sign. Their wider component requirements belong in phone numbers in RTL interfaces, rather than being inferred from a generic numeric span.
Signs and percent symbols need the whole value's context#
The archive dashboard shows a signed change in its processed-page count. The fixture token +386 is intended to keep its plus before the digits.
<p dir="rtl">التغير في عدد الصفحات: +386</p>The numeric token displays as 386+. A leading minus in the corresponding -386 fixture displays as 386-. The signs and digits are not damaged in storage; their visible relationship follows the RTL context.
For a defined LTR signed token, keep the sign and digits together inside the boundary:
<p dir="rtl">التغير في عدد الصفحات: <span dir="ltr">+386</span></p>Do not wrap only the digits and leave the sign outside. The component's data value includes the sign. Isolating an incomplete piece gives the browser the wrong ownership information.
The same review is needed for the digitization viewer's zoom token:
<p dir="rtl">تكبير الصورة: 1600%</p>After Arabic text, that token displays as %1600, while 1600% alone in an RTL paragraph displays as written in the supplied measurements. If the viewer defines its zoom badge as an LTR 1600% token, put the whole token in an LTR boundary.
This does not establish a universal rule that Arabic percentages must put the percent sign on one particular side. A localized formatter or product convention can choose a representation. Test against that declared representation rather than using an English screenshot as the only authority.
Keep the mathematical value separate. Reordering the source characters or changing the sign to compensate for appearance would alter data that calculations and exports may use.
Isolate fixed representations, not every numeric character#
A known-format identifier benefits from explicit LTR direction. An ordinary count inside Arabic prose often needs no intervention. A localized range may need to retain Arabic reading order. A single helper that wraps every digit match cannot express those distinctions.
Give a component the complete field value rather than asking a regular expression to discover semantic units inside a finished sentence. It should know whether it is rendering a count, reference, signed token or range before deciding where direction belongs.
For the Western-digit identifier, bdi is another valid wrapper:
<p dir="rtl">معرف دفعة المسح: <bdi>84720635-9184</bdi></p>Without strong letters, the automatic isolate resolves to LTR. Explicit dir="ltr" still communicates the identifier's fixed format more directly. For the underlying behavior, see MDN's bdi reference.
Automatic direction is not a rule saying “follow the surrounding Arabic.” If the label is moved inside the same automatic element, its Arabic letters can choose RTL for the combined label and value. Keep scope as deliberate as the direction itself.
For a full formatted measurement with a unit, decide which text is part of the formatted value. Do not isolate only the digits if the formatter intends a sign, percent symbol or unit to remain attached. Conversely, do not pull an unrelated Arabic noun into the numeric boundary just because it is adjacent in the sentence.
The same ownership rule applies when a Latin scanner name precedes a count. The count belongs to the surrounding message unless it is actually part of that name. Review the message's structured fields before applying wrappers to its apparent words.
A count can join the Latin name before it#
Not every numeric display failure involves a separator inside the number. A count can retain its digits perfectly and still appear associated with the wrong part of a sentence.
The archive's activity feed says that a named scanner produced a quantity of files. The scanner name is one field; the quantity belongs with the Arabic noun for files.
Problem
<p dir="rtl">أنتج AsterScan 9 ملفات</p>The Western number joins the preceding Latin word's LTR run. Visually, it falls between the Arabic verb and the scanner name, away from the noun it counts. Inspecting only whether the number is still 9 would miss the defect.
Better
<p dir="rtl">أنتج <bdi>AsterScan</bdi> 9 ملفات</p>The scanner name is now separate from the surrounding message. The count remains in the Arabic sentence. Wrapping the name and count together would declare the wrong unit, while applying a generic “isolate every number” helper would miss the underlying ownership decision.
This recommendation stops being true if a number is part of the scanner's official model name. Then that number belongs inside the name boundary. Keep model text and the independently supplied file count as separate data fields so the renderer can tell the cases apart.
Test the same message with an Arabic display name and a Latin one, and keep the quantity outside the name in both renderings. Do not split a model name into words and guess which trailing digits belong to it. The component should receive that structure from its data or message template.
This case also explains why a simple digit-order assertion is insufficient. The product's requirement includes the relationship between the count and its noun, not merely the characters in the count. Review the complete message with its actual Arabic grammar before deciding that the number needs a different direction.
Arabic-Indic digit groups are not the same fixture#
The identifier examples above use Western digits. A spaced Arabic-Indic representation has different internal bidi behavior:
٨٤٧٢٠٦٣٥ ٩١٨٤The supplied checks show spaces between Arabic-Indic digit groups resolving RTL even inside bdi or an element with dir="ltr", including in English text. Isolation removes external influence; it does not erase the directional properties of the content inside it.
This is why “add LTR and preserve source order” is too broad a promise. It describes the working Western-digit fixture, not every numeric script and separator.
If the product requires this digit script for a fixed-order identifier, define and test its complete display representation. A tightly scoped override can force order, but it is a different operation from isolation and can damage letters included in its scope. Do not place it around an entire Arabic label-value row.
Also decide whether the identifier is allowed to change digit script at all. A human-readable count can be localized through a formatter. A machine reference may need to match an external catalogue exactly. Treating both as ordinary numbers can remove leading zeros or change the representation that staff need to compare.
Use realistic fixtures in the numbering systems the product supports. Switching only the interface language while retaining Western digits does not exercise the Arabic-Indic group case.
Formatting and bidi rendering need separate checks#
Intl.NumberFormat can choose grouping, digit script and other presentation details. Direction markup determines how that resulting string participates in surrounding text. The two controls solve different parts of the display pipeline.
For the archive's grouped count, declare the numbering-system choice:
const pageCountFormat = new Intl.NumberFormat("ar-EG", { numberingSystem: "latn", useGrouping: true}); const pageCountText = pageCountFormat.format(8736);const parts = pageCountFormat.formatToParts(8736);console.log(pageCountFormat.resolvedOptions().numberingSystem);console.assert(parts.map(part => part.value).join("") === pageCountText);This is a formatting fixture, not a formatter for batch identifiers. MDN's NumberFormat reference documents the options and related methods.
Do not infer a digit script solely from a locale string and then hard-code tests around that assumption. Runtime locale data can change. Specify options when the representation matters and inspect the actual result in the runtime you ship.
Signed and percentage outputs can also contain invisible directional marks or different separator characters. Inspect formatToParts() values and code points instead of assuming the output is the same as a manually typed ASCII fixture. Preserve literal parts when reconstructing the formatter's result.
Keep numeric data numeric for calculations and machine-readable exports. Keep identifiers as identifiers. Parsing a localized display string back into either one creates avoidable ambiguity around grouping, signs and digit scripts.
Inspect context before changing the string#
When a numeric display looks wrong, first capture its source text and surrounding DOM. The same characters can behave differently beside an Arabic label, alone in a paragraph or inside a boundary.
Select the actual value element in DevTools:
const value = $0; // Select the displayed numeric value element.const style = getComputedStyle(value);console.table({ text: value.textContent, dir: value.getAttribute("dir"), direction: style.direction, unicodeBidi: style.unicodeBidi, parentDirection: value.parentElement ? getComputedStyle(value.parentElement).direction : null});A computed LTR value does not prove an ordinary inline span establishes isolation. Inspect the markup and bidi mode together. If a wrapper was intended to isolate the value, confirm that a shared stylesheet has not removed or overridden that behavior.
Then inspect characters rather than relying on their apparent shape:
console.log(Array.from(value.textContent, character => `U+${character.codePointAt(0).toString(16).toUpperCase().padStart(4, "0")}`).join(" "));Run this after the preceding block, which defines value. It can distinguish ordinary spaces from other spacing characters and expose directional marks introduced elsewhere.
Compare the stored value before modifying it. If only the display differs, changing the API representation is the wrong layer. If the source is already reversed or malformed, a directional wrapper cannot restore the intended data contract.
For problems involving larger mixed-direction regions, continue with how browsers render mixed RTL and LTR text.
Test patterns and meanings together#
Use a fixture matrix that includes both values that should remain intact and values whose interpretation needs a decision:
| Fixture | Expected review |
|---|---|
08/03/2029, 07:46, 8,736, 6.3.8 after Arabic labels | Numeric sequences remain in written order under the stated context |
Batch identifier 84720635-9184 | Fixed group order matches the catalogue |
Page-count range 240-760 | Starting endpoint is encountered first in the intended reading direction |
Spaced groups 84720635 9184 | Isolation preserves the defined identifier presentation |
Signed count change +386 and -386 | Sign placement matches the declared formatted-token contract |
Viewer zoom 1600% | Symbol position matches the viewer's chosen representation |
| Latin scanner name followed by a file count | The count remains associated with the Arabic message rather than the name |
| Arabic-Indic spaced groups | Separate internal behavior is tested explicitly |
| Formatter-generated output | Actual separators and controls are inspected, not assumed |
Keep the original failing identifier next to its repaired version until the test demonstrates the defect. Then verify the same rendering component with a range so a blanket fix cannot quietly change its meaning.
Repeat after dynamic updates and at narrow widths. If a component wraps, masks or abbreviates the value differently on mobile, inspect the complete result. A line wrap is not a new data field, and a copy button should still follow the value's raw-data contract.
Keep visual checks alongside DOM assertions. Copying or reading textContent can confirm logical order while missing a visual group reversal. Conversely, an attractive screenshot does not prove that exports or lookup actions retained the authoritative identifier.
In a bug report, identify the value's role and the exact surrounding text. Include the digit script, separator code points, browser version and whether the value has its own boundary. A report saying only “numbers are backwards” leaves the implementer unable to distinguish an identifier, a correctly read range and a count attached to the wrong name.
State both the logical source and the required visible relationship. For the batch reference, that means preserving its catalogue group order. For the range, it means encountering the starting endpoint first in the intended direction. For the activity message, it means associating the quantity with the file noun. These are different acceptance criteria even when all the source data consists of correct digits.
A free Ritla scan can report , Arabic-Indic and Western digits mixed in the same context. Pair that consistency check with the separator fixtures above, then read how the Unicode Bidirectional Algorithm works for the processing rules or nested LTR content inside an RTL page for fixed-format boundaries.