Currency and prices in Arabic interfaces
Learn how to format Arabic prices with Intl.NumberFormat, preserve currency and precision, isolate complete amounts, and test symbols and digit systems.
By Omran Khleifat, Founder of Ritla
Guide14 min read
A camera-hire quote can display the same amount three ways: an Arabic currency abbreviation in the package card, an English code in the booking summary and a manually concatenated suffix in the confirmation. The digits may be correct in all three places, but the customer has to work out whether the currency, notation or price changed.
Start by separating currency identity, locale formatting, numbering system and bidi rendering. A currency code appearing on the left is not automatically a defect in an Arabic price. What matters is whether the reader can identify the amount and currency in the product's chosen notation, and whether that complete formatted value remains readable inside its surrounding message.
This guide uses an equipment-rental portal with a UAE quote and a Kuwait precision fixture. Format the complete price with Intl.NumberFormat, preserve its parts and controls, and keep that price separate from the Arabic label. Do not repair its appearance by rearranging currency strings or reversing numeric text.
Keep currency identity separate from interface language#
The rental quote contains an amount and a currency:
const quote = { amount: 6842.75, currency: "AED"};Switching this quote from an Arabic page to an English page changes how it is presented. It does not convert the amount to another currency. A locale such as ar-AE also does not supply an automatic default currency to a currency-style formatter; the application must provide one.
Use the currency supplied by the authoritative quote, not a guessed code based only on the current language or the visitor's browser. A UAE customer can read English, and an Arabic interface can legitimately display a quote denominated in another currency when the product explicitly supports that.
Keep these decisions visible in the component's contract:
| Decision | Source in the rental portal |
|---|---|
| Amount and currency | The quote data |
| Formatting locale | The selected interface or market presentation |
| Numbering system | The product's declared digit policy |
| Currency display style | The chosen code, symbol or localized-name policy |
| Precision and rounding | Currency defaults plus any explicit product requirement |
| Inline boundary | The renderer containing the complete formatted value |
This separation makes a wrong-currency defect distinguishable from a formatting defect. A visually tidy AED suffix on an amount that actually represents another currency is still wrong. Intl.NumberFormat formats the supplied quantity; it does not perform exchange-rate conversion.
The ECMAScript Internationalization specification defines the formatter. Treat it as the display step after the application has established what the monetary value means.
Replace manual price assembly with a currency formatter#
This quote renderer hard-codes two decimals and puts the code after the number:
Problem
const quote = { amount: 6842.75, currency: "AED" };const priceText = `${quote.amount.toFixed(2)} ${quote.currency}`;It produces one literal notation regardless of locale and currency. It also omits grouping and assumes a universal two-decimal policy. Those are decisions hidden inside string assembly rather than declared formatting requirements.
Better
const quote = { amount: 6842.75, currency: "AED" };const quoteFormat = new Intl.NumberFormat("ar-AE", { style: "currency", currency: quote.currency, currencyDisplay: "code", numberingSystem: "latn"}); const priceText = quoteFormat.format(quote.amount);For these fixtures, the product has chosen Western digits and ISO currency codes in its quote review. That is a declared presentation choice, not a requirement that every Arabic consumer interface use codes.
The formatter chooses the locale's currency pattern, separators, spacing and currency precision. MDN's NumberFormat constructor reference documents the options.
Do not format a decimal number and append a separately translated currency label afterward unless you are deliberately implementing a specified custom notation. Once the product wants locale currency formatting, use the currency operation that owns the complete representation.
Share that policy between the package card, quote summary and confirmation. A formatter in one component cannot correct a second component that still calls toFixed() or assembles its own suffix.
Choose codes, symbols or names for the price's purpose#
The fixture uses currencyDisplay: "code" because a rental quote can be reviewed alongside quotes from another market. The code makes currency identity explicit without relying on a short symbol whose meaning depends on context.
A consumer package page can reasonably choose currencyDisplay: "symbol" instead. The important requirement is that the customer can identify the currency and that the chosen representation is applied deliberately. Do not change a quote from a localized symbol to a Latin code only because the code seems easier to position in CSS.
narrowSymbol is a separate choice, not a general upgrade. A shorter symbol may omit information that distinguished currencies in a comparison. Test whether the surrounding product context supplies that identity before using the shorter presentation everywhere.
For explanatory prose, a localized currency name can be more suitable than a compact price badge. Let currencyDisplay: "name" format that representation rather than assembling a translated noun after a separately formatted number. Inspect the complete output at the quantities the interface uses.
The currency part can therefore contain Latin letters, Arabic text or a symbol depending on the selected policy and locale data. Do not classify a formatted code as untranslated interface prose merely because it is Latin, and do not assume an Arabic abbreviation proves the quote uses the right currency.
These choices also affect width and typography. A name can be longer than a code; a symbol can require a glyph missing from the price font. Keep representation choice separate from the layout repair. Fix the container or font when that is the defect, rather than replacing the currency with a hard-coded abbreviation in one component.
Document which surfaces use which style. A multi-currency comparison and a single-market package tile need not be identical, but two tiles in the same comparison should not choose their notation accidentally through different helpers. Include the representation policy in visual acceptance criteria so reviewers can distinguish a purposeful variation from a stale implementation.
Inspect logical output before judging visual position#
In the runtime used to verify the examples, Node.js 24.16.0 with ICU 78.3, the Arabic UAE code formatter returns this logical string, written with escapes for invisible characters:
"\u200F6,842.75\u00A0AED"The prefix is RIGHT-TO-LEFT MARK and the separator before the code is a nonbreaking space. An en-AE formatter with the same currency, digit choice and display style returns:
"AED\u00A06,842.75"These are verified logical strings, not textual screenshots of how every browser places the Arabic price. Locale data and implementations can change exact output. Inspect the rendered value in the actual page before asserting that a currency belongs on a particular physical side.
A reader of the Arabic quote should encounter the amount and its currency as one coherent price. Comparing only the leftmost item against an English design ignores the reading direction and locale pattern. Conversely, a locale pattern is not an excuse for a currency detached from its amount or associated with the wrong line item.
Unicode's number-formatting guidance describes currency patterns, symbols and spacing in locale data. Those conventions are richer than the rule “put the symbol before the digits.”
If a raw handwritten token looks different from formatter output, compare the characters and formatting options first. Do not delete marks or reorder the code to make the two strings look alike in a console.
Keep the complete formatted price inside its boundary#
The Arabic label belongs to the sentence. The price belongs to the formatter. Express that distinction in the markup:
<p dir="rtl">قيمة حزمة التصوير: <bdi id="package-price"></bdi></p>// Run after the markup exists, using quoteFormat and quote from above.const priceElement = document.getElementById("package-price");priceElement.textContent = quoteFormat.format(quote.amount);The inserted value includes the amount, sign when present, currency and literal formatting characters. The automatic isolate determines its direction from its content, including any strong directional marks supplied by the formatter. Do not infer that direction from the first visible digit alone.
For a renderer with a fixed Arabic-format contract, an explicit RTL boundary can also be appropriate; an English-format renderer may use LTR. Choose the direction for the complete formatted representation, rather than assuming that all prices must be LTR because digits normally run that way within a group.
Do not isolate only the numeric digits and leave the currency outside. That makes the currency part of the surrounding sentence's bidi context even though it belongs to the amount. Likewise, wrapping the entire label and price together does not establish a distinct boundary for the inserted value.
W3C's inline bidi markup guidance explains using boundaries around complete inserted phrases. Keep ordinary bidi processing inside the price; an override is a different operation and can damage localized currency text.
Verify the price both in the Arabic sentence and as a standalone quote value. The exact visual placement of a formatter-generated currency string is not covered by the brief's generic separator rules, so test the actual output in supported browsers.
Digit choice must be explicit when consistency matters#
Arabic is not one universal numbering-system policy. Do not infer that every Arabic locale must emit Arabic-Indic digits, or that using Western digits necessarily means the price was left untranslated.
For the chosen quote notation, numberingSystem: "latn" requests Western digits. A deliberate Arabic-Indic variant can use "arab":
const arabicDigitFormat = new Intl.NumberFormat("ar-AE", { style: "currency", currency: "AED", currencyDisplay: "code", numberingSystem: "arab"}); console.log(arabicDigitFormat.resolvedOptions().numberingSystem);console.table(arabicDigitFormat.formatToParts(6842.75));Inspect the separators too. In the verified runtime, this configuration changes both the digit characters and the grouping and decimal separator characters. It is not equivalent to replacing only the visible digits in the Western representation.
Apply the selected policy across related price surfaces. A package card using one digit system and its total using another deserves investigation, unless the design intentionally distinguishes those contexts. Do not judge isolated screenshots without checking the formatter configuration used by each component.
When the product supports both digit systems, make both fixtures part of regression coverage. Retaining Western digits while changing only the page language does not exercise Arabic-Indic output.
Locale defaults can vary with runtime data, so rely on explicit options when the product requires consistency. Confirm the effective configuration using resolvedOptions().
Two decimal places are not a universal currency rule#
The UAE fixture uses two fraction digits under the currency defaults. The Kuwait fixture needs three:
const kuwaitFormat = new Intl.NumberFormat("ar-KW", { style: "currency", currency: "KWD", currencyDisplay: "code", numberingSystem: "latn"}); console.log(kuwaitFormat.resolvedOptions().maximumFractionDigits); // 3console.log(kuwaitFormat.formatToParts(6842.375));KWD is divided into a thousand fils. In the verified runtime, the fraction part of this fixture is 375. Applying toFixed(2) or a shared maximumFractionDigits: 2 would change that displayed precision.
Do not pad every currency to three decimals in response. Let the currency defaults establish the normal fraction count unless the product specifies another display requirement. Currency precision is not an Arabic-versus-English distinction.
A formatter can also round a value that contains more precision than its configured display allows. The displayed string is therefore not an authoritative source for reconstructing the original amount. Keep the quote system's exact monetary representation for calculations and settlement; use the formatter to present the resulting value.
If an explicit rounding policy is required, specify and test it separately from digit script and symbol position. A direction fix must not silently change the quote's monetary data or introduce a different rounding rule in one component.
Include zero, whole amounts, fractional amounts and a three-decimal currency in the fixture set. A single two-decimal AED amount cannot expose a universal precision assumption.
Use formatToParts without rearranging the price#
A design may show the currency slightly smaller than the amount. formatToParts() allows styling while retaining the formatter's sequence.
<bdi id="styled-price"></bdi>// Run after the markup exists, using quoteFormat and quote from above.const styledPrice = document.getElementById("styled-price");const priceParts = quoteFormat.formatToParts(quote.amount);const fragment = document.createDocumentFragment(); for (const part of priceParts) { const span = document.createElement("span"); span.className = `price-part-${part.type}`; span.textContent = part.value; fragment.append(span);} styledPrice.replaceChildren(fragment);This preserves all parts, including literals. MDN's formatToParts reference documents the part types and their use.
Do not filter the result to integer, fraction and currency. That would discard grouping, decimal separators, signs and literal characters. Do not extract the currency part and prepend it because the English design showed a prefix. Either operation replaces the locale's sequence with a custom one.
Keep these formatting spans as ordinary inline decoration inside the complete price boundary. Applying dir, isolation or an override to each part creates a different set of directional scopes. A part boundary exists for styling; it does not necessarily represent an independent semantic value.
Check text reconstruction before visual styling:
console.assert( priceParts.map(part => part.value).join("") === quoteFormat.format(quote.amount));That assertion verifies preservation of the logical formatted string. It does not prove the CSS leaves its appearance correct. Test the styled component in the actual Arabic and English contexts too.
Preserve spaces and directional marks from the formatter#
The verified AED code output contains a nonbreaking space and a directional mark. They are not visible currency glyphs, but they are still part of the formatted representation.
Do not treat every literal as disposable whitespace. A global cleanup that removes invisible characters can undo information the formatter used to preserve sign or punctuation context. Conversely, do not copy those controls into the stored numeric amount or currency field.
Expose code points while investigating a mismatch:
console.log(Array.from(quoteFormat.format(quote.amount), character => `U+${character.codePointAt(0).toString(16).toUpperCase().padStart(4, "0")}`).join(" "));This distinguishes an ordinary space from a nonbreaking space and makes directional marks visible. Logging two ordinary strings can make an exact comparison failure look inexplicable because the characters responsible are hidden.
MDN's format documentation warns that output can contain such characters and vary between implementations. Avoid manually typed expected strings that only resemble the visible output.
For machine-readable export, use the underlying amount and currency fields. For a copied human-readable quote, define whether the action returns the localized formatted price or another explicit representation. Do not try to recover numeric data by deleting characters from a localized label.
Negative amounts and accounting formats need their own fixtures#
A positive package quote does not exercise a credit or adjustment. Negative prices add a sign, and an accounting format may use a different pattern depending on the locale.
Render a negative fixture through the same formatter:
const creditText = quoteFormat.format(-6842.75);console.table(quoteFormat.formatToParts(-6842.75));In the verified code-format output, the sign is accompanied by additional directional context. Preserve it as part of the complete value rather than moving a hyphen into the Arabic label or adding it after formatting a positive number.
If the product chooses currencySign: "accounting", test the resulting locale pattern. Do not assume that every Arabic locale uses parentheses, or add parentheses manually while also enabling an accounting formatter.
Keep semantic labels clear too. A localized word meaning “credit” belongs to the surrounding message; it does not replace the stored sign or authorize changing the amount. Test whether the full message lets the customer distinguish an amount due from an adjustment.
The same principle applies to a positive sign when the product intentionally shows one. Declare the sign policy in formatting options, and verify zero as well as positive and negative values. Do not make sign visibility depend on whichever helper a component happens to call.
Consistency includes scope, not identical output everywhere#
The Arabic and English views can legitimately use different currency patterns for the same quote. A compact package tile and a detailed multi-currency comparison may also choose different display styles when their purpose is explicit.
Consistency means those choices are declared and applied to the intended context. It does not mean every screen must show a byte-identical price string regardless of locale.
Use one shared formatting policy per presentation context. If formatters are cached, include the locale, currency and relevant options in the cache identity. Reusing an AED formatter after the quote changes to KWD can retain the wrong currency and precision even when the numeric amount updates.
Check transitions, not just initial pages. Switching language should update the locale pattern while retaining the quote currency. Switching to a genuinely different market quote should use that quote's amount and currency. A label changing from one code to another without an authoritative data change is not conversion.
Server and browser formatting also deserve comparison. Different runtime locale data can produce different abbreviations, spaces or symbols. Record the configurations and outputs before blaming the bidi wrapper for a mismatch.
Where exact cross-runtime output is a product requirement, manage the relevant formatting data and runtime assumptions deliberately. Do not repair one side by hard-coding an old abbreviation over the other side's formatter result.
Keep price qualifiers outside the formatted amount#
The rental portal can describe an amount as a starting price, a daily rate or a package total. Those qualifiers change what the customer understands even when the numeric value and currency are identical.
Keep a localized phrase such as “per rental day” outside the complete currency boundary. The currency formatter owns the price representation; the Arabic message owns the rental period and other explanatory wording. Do not move the qualifier into an LTR wrapper solely to make it stay beside the digits.
Review those relationships in the full component. A price separated from its period label by wrapping can be misleading even when every formatter part is correct. Use an appropriate layout to keep the relationship clear, and test the longest Arabic qualifier supported by that surface.
The same applies when a page shows a previous price and a current price. Each price should retain its own complete formatting, while the surrounding labels explain their roles. Styling or DOM position alone should not be the only evidence of which amount the quote actually uses.
Make room for the whole amount and currency#
A correct formatted string can still fail in a narrow price card. A long amount, a currency name or a different symbol width may exceed the space that was designed around one short English example.
Give the complete price enough room. If it should remain on one line, scope white-space: nowrap to that price and test the surrounding card's ability to accommodate it. Applying no-wrap to the entire Arabic sentence can create a different overflow problem.
If the design permits wrapping, inspect the amount-currency association at the actual break. Do not replace the formatter's spacing with arbitrary characters merely to obtain a convenient line break. A responsive layout or a shorter, explicitly chosen display style may be the appropriate repair.
Test the font used for the actual currency representation. A missing symbol glyph can look like a formatting defect even though the formatter returned the intended character. currencyDisplay: "code" can help identify the currency during debugging, but changing to codes is a product notation choice, not a universal glyph repair.
Tabular digits can help compare aligned price columns when the font supports them. They do not choose currency precision, change digit script or establish a bidi boundary. MDN's font-variant-numeric reference describes that typography option.
For the container side of a failure, continue with Arabic text overflow: causes and fixes.
Test the price contract from data to screen#
Capture the quote data, formatter configuration, logical output and rendered component as separate evidence. This keeps an amount error, wrong currency, missing character and bidi boundary defect distinguishable.
| Fixture | What to establish |
|---|---|
AED quote 6842.75 | Amount and currency match the source quote |
| Arabic and English presentations | Locale pattern changes without changing currency identity |
| Explicit Western and Arabic-Indic digit policies | Digits and separators match the selected policy |
KWD amount 6842.375 | Currency precision is not forced to two decimals |
| Zero, whole and fractional amounts | Padding and sign behavior follow the declared options |
| Negative adjustment | Sign and complete formatted value remain associated |
| Styled parts | Every part survives in the formatter's sequence |
| Long value at narrow width | Amount and currency remain readable together |
| Dynamic currency or locale change | All dependent formatting options update |
Use meaningful assertions for the currency part, resolved fraction digits and reconstruction of all part values. Pair them with visual review in the complete Arabic component. A successful text comparison cannot establish that CSS has not reordered or clipped the visible pieces.
Repeat with the browsers and fonts your customers use, and record versions with visual evidence. The local runtime measurements above establish the illustrated logical output, not identical browser layout across engines.
A free Ritla scan can report , foreign currency formatting in a SAR/AED market context. Pair that finding with the quote's actual currency contract, then read nested LTR content inside an RTL page for boundary decisions or Arabic text overflow for price containers that cannot fit the chosen notation.