"Arabic-first" is not a language setting added at the end of a project. It is an architectural decision that touches the presentation layer, the data layer, and the calculation layer at once. The difference between a localised product and one built Arabic-first shows up in seven specific technical places, each of which breaks an assumption baked into standard Western tooling.
This article walks through those seven, with concrete examples from building a property management product for the Saudi market. They apply to any financial or administrative product targeting the region.
1. Direction is not direction: rtl
The most common RTL mistake is assuming dir="rtl" is sufficient. The real problem is physical versus logical properties.
Consider a card with an icon in normal flow and a badge pinned via position: absolute using left: 1rem. In Arabic everything looks correct — icon on the right, badge on the left. In English, normal flow moves to the left while the badge stays pinned at the same left, and the two collide.
The fix is logical properties: inset-inline-start, inset-inline-end, margin-inline, padding-inline instead of left/right/margin-left. These mirror automatically with document direction. In Tailwind the equivalents are start-*, end-*, ms-*, me-*.
The practical rule: any physical value in CSS is a time bomb that detonates when direction flips. Test every screen in both languages — and specifically test a dense reporting screen rather than the landing page, because tables are where the problem surfaces.
2. The Hijri calendar: do not write your own converter
Saudi lease contracts are frequently written in Hijri dates while financial systems run on Gregorian. The first temptation is to write an arithmetic conversion function. That is a mistake.
The officially adopted Hijri calendar in Saudi Arabia is the Umm al-Qura calendar, and its month lengths do not follow a simple mathematical formula. Any hand-rolled converter will drift from the official calendar on certain dates, and a one-day drift in an instalment due date means a dispute with a tenant.
The solution already ships with the platform — the Intl API:
const hijri = date.toLocaleDateString('ar-SA-u-ca-islamic-umalqura', { year: 'numeric', month: 'long', day: 'numeric'});The u-ca-islamic-umalqura extension yields the Umm al-Qura calendar directly from the ICU data bundled in the runtime. Beware: plain u-ca-islamic is an astronomical calculation that differs from Umm al-Qura by a day in some months, and bare ar-SA now resolves to the Gregorian calendar in recent runtimes. No external dependency, no conversion table to maintain.
The rule: store dates as Gregorian in the database and render both. Storing Hijri turns every range query into a nightmare.
3. Money: Decimal, never Float
0.1 + 0.2 !== 0.3 is not a joke — it is the source of reconciliation differences in financial systems. Floating-point numbers represent fractions in binary, and many decimal fractions have no exact binary representation.
The correct decision is for every monetary field to be Decimal at the database level, not only in application code. In one production schema, amounts are modelled as Decimal(20, 8):
amount Decimal @db.Decimal(20, 8)paidAmount Decimal @default(0) @db.Decimal(20, 8)Why eight decimal places when the riyal is written with two? Because the product supports eight currencies and stores the original amount as entered before conversion. Exchange rates need more precision than display does, and rounding at storage time accumulates across thousands of records.
A practical warning: stay in Decimal all the way through. Casting to a JavaScript Number for a single calculation discards everything you gained.
The legitimate exception for floating point is non-monetary data — geographic coordinates and areas, for example — where absolute precision is not required.
4. Partial payments: the problem that never appears in requirements
This is the most commonly missed case in financial system design: what happens when a customer pays an amount that does not cover a full instalment?
The naive handling leaves the amount "unallocated", accumulating phantom debt. Correct handling requires an explicit allocation strategy. The most common is FIFO: the oldest outstanding instalment is settled first.
Reality is more complex. When a carried-over balance from a previous contract exists, cases arise that FIFO alone does not resolve. Supporting more than one strategy helps:
FIFO— the default: oldest first, starting with carryover then current instalmentsCARRYOVER_ONLY— settles only the carried-over balanceIGNORE_CARRYOVER— skips carryover, and requires a recorded reason
That last constraint matters most from an engineering standpoint: any deviation from the default rule must leave an auditable trace.
Another pattern worth adopting: a preview endpoint that simulates allocation without persisting, so the user sees the effect of a payment before committing it. That is what Amlakire does with payment allocation preview, and it prevents input errors better than any warning message.
5. Tenant isolation: the default must be deny
In a product serving multiple real estate agencies, each managing multiple owners, isolation is not a feature — it is a precondition.
The common failure is treating isolation as a UI concern, or as a condition added manually to each query. Any query that forgets the condition becomes a leak. Sound architecture makes scoping mandatory at the data access layer: every entity carries the owning organisation's identifier, and every query passes through a layer that enforces filtering by default.
In the product referenced above, agencyId appears in 73 places in the database schema and is used as a filter across 36 services. The count itself is not the achievement — the achievement is zero unfiltered queries.
A practical test: write a test that attempts to read a record belonging to another organisation using a valid identifier, and expect 404 rather than 403. Returning 403 confirms the record exists, which is itself an information leak.
6. Tax belongs to the unit, not the account
In Saudi Arabia, commercial rent is subject to 15% VAT while residential rent is exempt (0%). The wrong model puts the tax rate as a setting at the account or agency level, which breaks the moment an agency manages two properties of different types.
The correct model makes the tax category an attribute of the property unit itself, with the taxable base computed at invoice time from the unit's category rather than a global setting. The general principle: regulatory attributes belong to the smallest entity they apply to, and lifting them to a higher level is deferred technical debt.
7. Arabic text at the system boundary: input and output
One challenge remains, and it appears at both edges of the system — where text enters and where it leaves.
On input: an Arabic keyboard produces Arabic-Indic digits ٠١٢٣٤٥٦٧٨٩, and a Persian one produces ۰۱۲۳۴۵۶۷۸۹. Passing either to parseFloat yields NaN. Worse, the Arabic decimal separator is its own character entirely — ٫ (U+066B) — neither a comma nor a period.
Normalise input before any numeric parsing:
const normalizeArabicNumbers = (value) => value .replace(/[٠-٩]/g, d => String('٠١٢٣٤٥٦٧٨٩'.indexOf(d))) .replace(/[۰-۹]/g, d => String('۰۱۲۳۴۵۶۷۸۹'.indexOf(d)));Then collapse every decimal-separator variant (, ، ٫) into a single period. Without this step, users report that "the system rejects the amount" while you look at a field that appears perfectly valid on screen.
On output: generating Arabic PDFs is the larger trap. Most low-level PDF libraries draw glyphs one by one without shaping (letter joining) and without the bidi algorithm, producing text that is disconnected and reversed. A library that works flawlessly with Latin script can emit a completely unreadable Arabic document.
The more reliable path is to delegate rendering to a browser engine: build HTML with dir="rtl" and an embedded Arabic font, then convert it to PDF via headless Chromium. The browser handles shaping and direction because it already solves both to render web pages.
The rule: never test your PDF pipeline with Latin text. Test it with an Arabic sentence containing digits and two dates — the hardest possible case.
Summary
The seven challenges share one pattern: each arises from an implicit assumption in standard tooling — that direction runs left to right, that the calendar is Gregorian, that there is one currency, that tax is a fixed rate, and that every user sees everything.
Building an Arabic-first product means surfacing these assumptions early and resolving them in the architecture rather than the presentation layer. The cost difference between the two approaches is measured in full rewrites.