# arabicfmt
### Arabic-first formatting for JavaScript & TypeScript
Currency symbols · Hijri/Islamic calendar · number-to-words · تفقيط · 6 plural forms · RTL/bidi —
correct for all **22 Arab League countries**, with **zero dependencies** and full TypeScript types.
أرقامٌ وعملاتٌ وتواريخُ هجريةٌ ولغةٌ عربيةٌ سليمة — في سطرٍ واحد.
[](https://www.npmjs.com/package/arabicfmt) [](https://www.npmjs.com/package/arabicfmt) [](https://www.jsdelivr.com/package/npm/arabicfmt) [](https://bundlephobia.com/package/arabicfmt) [](./package.json) [](https://www.npmjs.com/package/arabicfmt)

**[npm](https://www.npmjs.com/package/arabicfmt) · [Live demo](https://arabicfmt.vercel.app) · [GitHub](https://github.com/cc1a2b/arabicfmt)**
> **arabicfmt** is the only JavaScript library that handles the entire Arabic formatting stack in one zero-dependency package — currency symbols, number precision, Hijri/Islamic calendar dates, RTL bidirectional text, Arabic number-to-words and تفقيط — with full TypeScript types. Works in **Node, the browser, Deno, Bun and React Native**.
>
> **New in 0.1.5:** every currency sign from the Unicode 17.0/18.0 transition — SAR `U+20C1`, MVR `U+20C2`, AED `U+20C3`, OMR `U+20C4` — plus the tooling to actually render them: [typed parts](#rendering-the-new-signs), a [generated `@font-face`](#rendering-the-new-signs), [price ranges](#price-ranges) and a [date-aware transition registry](#the-transition-as-data).
```sh
npm install arabicfmt
```
---
## What other libraries get wrong
| Problem | Other libraries | arabicfmt |
|---|---|---|
| Saudi riyal U+20C1 | Emits ﷼ (U+FDFC) — the **Iranian** rial | Correct U+20C1 with a safe text fallback |
| The Unicode 18.0 signs (MVR/AED/OMR) | Unencoded, or still the old abbreviation | All four signs, plus `transitionStatus()` and a generated `@font-face` |
| Styling a new sign | Regex the symbol back out of a formatted string | `formatCurrencyToParts()` — the sign is its own typed part |
| Iraqi dinar (IQD) decimals | 0 (CLDR practical) | **3 decimals** — ISO 4217 legal standard |
| Hijri date output | Varies between Node, Chrome, Safari, Hermes | Frozen Umm al-Qura tables — **identical on every engine** |
| Arabic plurals | 1–2 forms; Arabic legally needs 6 | Full CLDR 6-form system (zero/one/two/few/many/other) |
| Number to Arabic words | No zero-dep solution | `arabicToWords(1234)` → "ألف ومئتان وأربعة وثلاثون" |
| Spell money for cheques (تفقيط) | Build it yourself, get the grammar wrong | `spellCurrency(1234.5, {currency:"SAR"})` → "ألف ومئتان وأربعة وثلاثون ريالاً وخمسون هللةً" |
| Ordinals (ترتيبية) | Missing or gender-blind | `arabicOrdinal(25)` → "الخامس والعشرون", gender-aware |
| Spoken durations | `Intl.DurationFormat` barely supported | `formatDuration(7_500_000)` → "ساعتان وخمس دقائق" with full agreement |
| RTL broken sentences | Phone numbers flip mid-sentence | Unicode isolates wrap LTR runs automatically |
| Eastern Arabic digit parsing | `parseInt("١٢٣")` → NaN | `parseNumber("١٬٢٣٤٫٥٦")` → 1234.56 |
| Arabic URL slugs | Strip to empty or mojibake | `slugify("مدينة جدة")` → "mdynh-jdh" |
| IBAN / Saudi ID checks | Regex that accepts bad numbers | Real ISO 7064 mod-97 + Luhn checksums |
---
## Install
```sh
npm install arabicfmt
# or
yarn add arabicfmt
# or
pnpm add arabicfmt
```
**Requirements:** Node.js ≥ 18 · TypeScript ≥ 4.7 (optional) · zero runtime dependencies.
### Browser / CDN — no build step
Every release is mirrored on the [jsDelivr](https://www.jsdelivr.com/package/npm/arabicfmt) and [unpkg](https://unpkg.com/browse/arabicfmt/) CDNs automatically. Import the browser-ready ESM bundle straight from a URL — no install, no bundler:
```html
```
Subpaths work too — e.g. `https://cdn.jsdelivr.net/npm/arabicfmt/dist/currency/index.js` for just the currency module. Pin a version for production, e.g. `arabicfmt@0.1`.
---
## Quick start
```ts
import {
formatCurrency, // correct symbol + precision for every Arab currency
formatCurrencyRange, // "1,000.00 – 5,000.00 ر.س"
formatCurrencyToParts, // typed parts — font-scope just the new Unicode sign
transitionStatus, // "none" | "announced" | "encoded" for any date
signFontFaceCSS, // the scoped @font-face the new signs need
formatCompact, // 1,200,000 → "1.2M" / "١٫٢ مليون"
arabicToWords, // 1234 → "ألف ومئتان وأربعة وثلاثون"
spellCurrency, // تفقيط: 1234.5 SAR → "...ريالاً وخمسون هللةً"
arabicOrdinal, // 25 → "الخامس والعشرون"
formatDuration, // 7_500_000ms → "ساعتان وخمس دقائق"
formatFileSize, // 1536 → "1.5 كيلوبايت"
formatRelativeTime, // "منذ ٣ أيام"
formatList, // ["أحمد","علي"] → "أحمد وعلي"
parseCurrency, // "١٬٢٣٤٫٥٠ ر.س" → 1234.5
arabicPlural, // full 6-form Arabic plural selection
sortArabic, // Arabic-locale collation
slugify, // "مدينة جدة" → "mdynh-jdh" (URL slugs)
isValidIBAN, // ISO 7064 mod-97 IBAN checksum
isValidSaudiId, // Saudi national ID / Iqama check digit
isolateForeign, // fix broken RTL sentences
normalizeForSearch, // search-key normalization
detectLocale, // auto-detect from browser / Node environment
} from "arabicfmt";
import { formatHijri, toHijri } from "arabicfmt/umalqura"; // deterministic Hijri calendar
// Currency
formatCurrency(1.2, { currency: "KWD" }); // "1.200 د.ك"
formatCurrency(1234, { locale: "ar-SA", numerals: "arab" }); // "١٬٢٣٤٫٠٠ ر.س"
formatCurrency(-500, { currency: "SAR", accounting: true }); // "(500.00 ر.س)"
formatCurrencyRange(1000, 5000, { currency: "SAR" }); // "1,000.00 – 5,000.00 ر.س"
formatCompact(1_500_000, { locale: "ar", numerals: "arab" }); // "١٫٥ مليون"
// The Unicode currency-sign transition, as data
transitionStatus("AED"); // "encoded" (Unicode 18.0)
transitionStatus("AED", new Date("2026-01-01")); // "announced"
transitionStatus("KWD"); // "none" — Kuwait has no sign
signFontFaceCSS({ src: "/fonts/signs.woff2" }); // @font-face … unicode-range: U+20C1…U+20C4
// Number to Arabic words
arabicToWords(1234); // "ألف ومئتان وأربعة وثلاثون"
arabicToWords(1_000_000); // "مليون"
arabicToWords(5, { gender: "female" }); // "خمس"
// Spell money for invoices & cheques (التفقيط)
spellCurrency(1234.5, { currency: "SAR" });
// "ألف ومئتان وأربعة وثلاثون ريالاً وخمسون هللةً"
spellCurrency(100, { currency: "SAR", suffix: true }); // "مئة ريال فقط لا غير"
// Ordinals — gender-aware
arabicOrdinal(1); // "الأول"
arabicOrdinal(25); // "الخامس والعشرون"
arabicOrdinal(1, { gender: "female" }); // "الأولى"
// Duration & file size
formatDuration(7_500_000); // "ساعتان وخمس دقائق"
formatFileSize(1536); // "1.5 كيلوبايت"
// Lists
formatList(["أحمد", "محمد", "علي"]); // "أحمد ومحمد وعلي"
formatList(["تفاح", "موز"], { type: "disjunction" }); // "تفاح أو موز"
// Hijri dates (deterministic — same output on Node, Chrome, Safari, Hermes)
formatHijri(new Date("2025-09-23")); // "1 ربيع الآخر 1447 هـ"
formatHijri(new Date("2025-09-23"), { numerals: "arab" }); // "١ ربيع الآخر ١٤٤٧ هـ"
toHijri(new Date("2025-09-23")); // { year: 1447, month: 4, day: 1 }
// Relative time
formatRelativeTime(new Date(Date.now() - 3 * 86400_000)); // "منذ 3 أيام"
// Parse formatted strings back to numbers
parseCurrency("١٬٢٣٤٫٥٠ ر.س"); // 1234.5
parseCurrency("(500.00 SAR)"); // -500
// RTL
isolateForeign("اتصل على +1 (555) 234-5678 الآن"); // phone stays intact in RTL
// Locale auto-detection
detectLocale(); // "ar-SA" in a Saudi browser, "ar-EG" in Node with LANG=ar_EG
```
---
## Currency formatting
### Symbol strategy: `symbolMode`
The **Saudi riyal** received its own Unicode symbol (U+20C1) in September 2025. Most libraries either emit the wrong ligature (U+FDFC, the Iranian rial) or fall back to `SAR`. arabicfmt gives you full control:
```ts
import { formatCurrency, resolveCurrencySymbol, getCurrencyInfo } from "arabicfmt/currency";
formatCurrency(1234.5, { currency: "SAR" });
// → "1,234.50 ر.س" (auto: safe text symbol, renders everywhere today)
formatCurrency(1234.5, { currency: "SAR", symbolMode: "new" });
// → "1,234.50 " (U+20C1 — use with a webfont; see webfont guide below)
formatCurrency(1234.5, { currency: "SAR", symbolMode: "code" });
// → "1,234.50 SAR" (ISO code — for accounting tables)
```
| `symbolMode` | SAR | AED | OMR | Use when |
|---|---|---|---|---|
| `auto` *(default)* | `ر.س` | `` U+20C3 | `` U+20C4 | Default. AED/OMR use the dedicated sign; SAR stays on safe text |
| `new` | `` U+20C1 | `` U+20C3 | `` U+20C4 | Force the dedicated sign (needs font support) |
| `text` | `ر.س` | `د.إ` | `ر.ع.` | Always the safe text symbol — renders everywhere |
| `code` | `SAR` | `AED` | `OMR` | ISO code |
> **Unicode 18.0 (16 September 2026):** the AED (U+20C3), OMR (U+20C4) and MVR
> (U+20C2) signs are now `live`, and `auto` prefers them. Need maximum
> compatibility today? Use `symbolMode: "text"`. The Saudi riyal keeps its safe
> text default by design. See
> [the transition section](#unicode-currency-sign-transition) for the full table.
### Correct decimal precision — all 22 Arab League countries
Generated from CLDR 48.2.0 at build time and **verified on every build**:
```ts
formatCurrency(1.2, { currency: "KWD" }); // "1.200 د.ك" ← 3 decimals
formatCurrency(1.2, { currency: "BHD" }); // "1.200 د.ب" ← 3 decimals
formatCurrency(1.2, { currency: "IQD" }); // "1.200 ع.د" ← 3 decimals (ISO 4217, not CLDR's 0)
formatCurrency(500, { currency: "KMF" }); // "500 ف.ج.ق" ← 0 decimals
formatCurrency(500, { currency: "SAR" }); // "500.00 ر.س" ← 2 decimals
```
| Decimals | Currencies |
|---|---|
| **3** | KWD, BHD, OMR, JOD, IQD, LYD, TND |
| **0** | DJF, KMF |
| 2 | SAR, AED, QAR, and the rest |
### All currency options
```ts
// Resolve from locale region — no need to know the currency code
formatCurrency(99.9, { locale: "ar-BH" }); // "99.900 د.ب"
formatCurrency(1234, { locale: "ar-AE", numerals: "arab", symbolMode: "text" }); // "١٬٢٣٤٫٠٠ د.إ" (auto → U+20C3 sign)
// Accounting notation (negatives in parentheses)
formatCurrency(-1234.5, { currency: "SAR", accounting: true }); // "(1,234.50 ر.س)"
// Hide/override
formatCurrency(100, { currency: "SAR", showSymbol: false, fractionDigits: 0 }); // "100"
// Currency metadata
getCurrencyInfo("SAR");
// {
// code: "SAR", digits: 2,
// symbols: { auto: "ر.س", text: "ر.س", code: "SAR", new: "" },
// unicode: { codepoint: "U+20C1", unicodeVersion: "17.0", live: true, autoDefault: false },
// displayName: "ريال سعودي"
// }
```
### Price ranges
```ts
import { formatCurrencyRange } from "arabicfmt/currency";
formatCurrencyRange(1000, 5000, { currency: "SAR" })
// "1,000.00 – 5,000.00 ر.س" ← one symbol, the way ranges are actually set
formatCurrencyRange(1000, 5000, { currency: "SAR", symbolEach: true })
// "1,000.00 ر.س – 5,000.00 ر.س"
formatCurrencyRange(1.2, 2, { currency: "KWD" })
// "1.200 – 2.000 د.ك" ← both ends share the currency's precision
formatCurrencyRange(1000, 5000, { currency: "SAR", numerals: "arab" })
// "١٬٠٠٠٫٠٠ – ٥٬٠٠٠٫٠٠ ر.س"
formatCurrencyRange(50, 50, { currency: "AED" }) // "50.00 " — collapses
formatCurrencyRange(500, 100, { currency: "SAR" }) // throws: reversed range
```
Pass `rangeSeparator` for anything other than the default spaced en dash
(`{ rangeSeparator: " إلى " }`), and `isolate: true` to wrap the whole range
when it sits inside a mixed-direction sentence.
### Rendering the new signs
A brand-new sign needs two things a string cannot give you: a webfont scoped to
its codepoint, and a way to wrap *only the symbol* in the element that uses that
font. Both are one call each.
**1. The parts API** — the currency counterpart to
`Intl.NumberFormat.prototype.formatToParts`:
```ts
import { formatCurrencyToParts } from "arabicfmt/currency";
formatCurrencyToParts(1234.5, { currency: "SAR", symbolMode: "new" });
// [ { type: "integer", value: "1" },
// { type: "group", value: "," },
// { type: "integer", value: "234" },
// { type: "decimal", value: "." },
// { type: "fraction", value: "50" },
// { type: "literal", value: " " },
// { type: "currency", value: "", symbolMode: "new",
// codepoint: "U+20C1", needsFont: true } ]
```
`needsFont` is `true` exactly when the part holds a dedicated Unicode sign, so a
component never has to pattern-match a finished string (which is how bidi bugs
start):
```tsx