npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

arabicfmt

v0.1.2

Published

Arabic-first formatting for numbers, currency, dates and bidirectional text across all 22 Arab League countries — with correct handling of the 2025–2026 Unicode currency-symbol transition (Saudi Riyal U+20C1, UAE Dirham U+20C3, Omani Rial U+20C4).

Readme

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.

npm version downloads jsDelivr hits gzipped size zero dependencies types included

📦 npm · 🕹 Live demo · ⭐ GitHub

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.

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 | | 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-nsr" | | IBAN / Saudi ID checks | Regex that accepts bad numbers | Real ISO 7064 mod-97 + Luhn checksums |


Install

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 and unpkg CDNs automatically. Import the browser-ready ESM bundle straight from a URL — no install, no bundler:

<script type="module">
  import { formatCurrency, formatHijri } from "https://cdn.jsdelivr.net/npm/arabicfmt/+esm";

  console.log(formatCurrency(1234.5, { currency: "SAR", numerals: "arab" })); // ١٬٢٣٤٫٥٠ ر.س
  console.log(formatHijri(new Date(), { numerals: "arab" }));                 // ٢٧ ذو الحجة ١٤٤٧ هـ
</script>

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. [email protected].


Quick start

import {
  formatCurrency,      // correct symbol + precision for every Arab currency
  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-nsr" (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 ر.س)"
formatCompact(1_500_000, { locale: "ar", numerals: "arab" });    // "١٫٥ مليون"

// 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: "EGP", 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:

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 (September 2026): the AED (U+20C3) and OMR (U+20C4) 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.

Correct decimal precision — all 22 Arab League countries

Generated from CLDR 48.2.0 at build time and verified on every build:

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, EGP, and the rest |

All currency options

// 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: "ريال سعودي"
// }

Webfont guide for U+20C1

/* Scope the Saudi Riyal font to just that codepoint — zero impact on body text */
@font-face {
  font-family: "Riyal";
  src: url("/fonts/saudi-riyal.woff2") format("woff2");
  unicode-range: U+20C1;
}
:root { font-family: "Riyal", "Noto Naskh Arabic", sans-serif; }

Number formatting

import {
  formatNumber, formatCompact, formatPercent,
  toArabicDigits, toLatinDigits,
  parseNumber, parseCurrency,
  arabicToWords,
  formatRelativeTime,
} from "arabicfmt/number";

// Standard
formatNumber(1_234_567.89, { locale: "en" });              // "1,234,567.89"
formatNumber(1234.5,       { numerals: "arab" });           // "١٬٢٣٤٫٥"

// Compact / short notation — dashboards and data cards
formatCompact(1_500_000);                                   // "1.5M"
formatCompact(1_500_000, { locale: "ar" });                 // "1.5 مليون"
formatCompact(1_500_000, { locale: "ar", numerals: "arab" }); // "١٫٥ مليون"

// Percent
formatPercent(0.853, { locale: "en" });                    // "85.3%"

// Transliteration
toArabicDigits("Order #2026");                             // "Order #٢٠٢٦"
toLatinDigits("٢٠٢٦");                                     // "2026"  (handles Persian ۰–۹ too)

// Parsing — round-trip support
parseNumber("١٬٢٣٤٫٥٦");         // 1234.56  (Eastern Arabic digits + separators)
parseNumber("1,234.56");          // 1234.56  (Western)
parseCurrency("١٬٢٣٤٫٥٠ ر.س");  // 1234.5
parseCurrency("(500.00 SAR)");   // -500      (accounting notation)

// Relative time
formatRelativeTime(new Date(Date.now() - 3 * 86400_000));            // "منذ 3 أيام"
formatRelativeTime(new Date(Date.now() + 3600_000), new Date(), { locale: "en" }); // "in 1 hour"

Number to Arabic words (arabicToWords)

Convert integers to their Arabic word representation — handles gender agreement and all six scale levels.

import { arabicToWords } from "arabicfmt";

// Basic
arabicToWords(0)       // "صفر"
arabicToWords(1)       // "واحد"
arabicToWords(2)       // "اثنان"
arabicToWords(11)      // "أحد عشر"
arabicToWords(25)      // "خمسة وعشرون"
arabicToWords(100)     // "مئة"
arabicToWords(350)     // "ثلاثمئة وخمسون"

// Thousands
arabicToWords(1000)    // "ألف"
arabicToWords(2000)    // "ألفان"
arabicToWords(5000)    // "خمسة آلاف"
arabicToWords(11000)   // "أحد عشر ألفاً"
arabicToWords(100000)  // "مئة ألف"

// Millions / billions
arabicToWords(1_000_000)    // "مليون"
arabicToWords(2_000_000)    // "مليونان"
arabicToWords(5_000_000)    // "خمسة ملايين"
arabicToWords(1_000_000_000)// "مليار"

// Large composite
arabicToWords(1_234_567)
// "مليون ومئتان وأربعة وثلاثون ألفاً وخمسمئة وسبعة وستون"

// Gender agreement — feminine noun (ليرة، روبية…)
arabicToWords(3, { gender: "female" })  // "ثلاث"
arabicToWords(5, { gender: "female" })  // "خمس"

// Negative
arabicToWords(-42)  // "سالب اثنان وأربعون"

// Decimals — opt in (default truncates, stays backward compatible)
arabicToWords(3.14, { fraction: "digits" })  // "ثلاثة فاصلة واحد أربعة"
arabicToWords(3.14, { fraction: "number" })  // "ثلاثة فاصلة أربعة عشر"

// Common fractions (denominators 2–10)
import { arabicFraction } from "arabicfmt";
arabicFraction(1, 2)  // "نصف"
arabicFraction(3, 4)  // "ثلاثة أرباع"
arabicFraction(2, 3)  // "ثلثان"

Spell money in words — التفقيط

spellCurrency is the tafqit every Arabic invoice, cheque and contract needs: it turns a numeric amount into its full legal Arabic wording, splitting major and minor units and inflecting every noun for correct grammatical agreement (singular / dual / plural / accusative).

import { spellCurrency } from "arabicfmt";

spellCurrency(1234.5, { currency: "SAR" })
// "ألف ومئتان وأربعة وثلاثون ريالاً وخمسون هللةً"

// Unit agreement is automatic (العدد والمعدود)
spellCurrency(1,   { currency: "SAR" })   // "ريال واحد"      (singular)
spellCurrency(2,   { currency: "SAR" })   // "ريالان"         (dual)
spellCurrency(3,   { currency: "SAR" })   // "ثلاثة ريالات"   (plural, 3–10)
spellCurrency(11,  { currency: "SAR" })   // "أحد عشر ريالاً" (accusative, 11–99)
spellCurrency(100, { currency: "SAR" })   // "مئة ريال"       (genitive singular)

// Minor-unit precision comes from CLDR — KWD = 1000 fils, SAR = 100 halalas
spellCurrency(1.5, { currency: "KWD" })   // "دينار واحد وخمسمئة فلس"
spellCurrency(0.75, { currency: "SAR" })  // "خمس وسبعون هللةً"

// Cheque-ready ending and locale-derived currency
spellCurrency(100, { currency: "EGP", suffix: true }) // "مئة جنيه فقط لا غير"
spellCurrency(-5,  { locale: "ar-AE" })               // "سالب خمسة دراهم"

Full Arabic noun paradigms are bundled for all 22 Arab League currencies (SAR, AED, KWD, BHD, QAR, OMR, JOD, EGP, IQD, LYD, TND, DZD, MAD, SDG, LBP, SYP, YER, SOS, DJF, KMF, MRU). Inspect or extend them via the exported CURRENCY_WORDS table.


Ordinal numbers — الأعداد الترتيبية

import { arabicOrdinal } from "arabicfmt";

arabicOrdinal(1)    // "الأول"
arabicOrdinal(2)    // "الثاني"
arabicOrdinal(10)   // "العاشر"
arabicOrdinal(11)   // "الحادي عشر"
arabicOrdinal(25)   // "الخامس والعشرون"

// Gender agreement
arabicOrdinal(1, { gender: "female" })   // "الأولى"
arabicOrdinal(25, { gender: "female" })  // "الخامسة والعشرون"

// Indefinite (drop the article ال)
arabicOrdinal(3, { definite: false })    // "ثالث"
arabicOrdinal(25, { definite: false })   // "خامس وعشرون"

Duration — spelled Arabic

formatDuration turns a time span into its spoken Arabic form, with correct dual/plural/accusative agreement on every unit — something Intl.DurationFormat (still barely supported) does not give you.

import { formatDuration } from "arabicfmt";

formatDuration(7_500_000)                  // "ساعتان وخمس دقائق"  (2h 5m)
formatDuration(90, { input: "s" })         // "دقيقة واحدة وثلاثون ثانيةً"
formatDuration(3_600_000, { largest: 1 })  // "ساعة واحدة"
formatDuration(2 * 86_400_000)             // "يومان"
formatDuration(500)                        // "أقل من ثانية"

// Restrict the units considered
formatDuration(125 * 60_000, { units: ["minute"], largest: 1 })
// "مئة وخمس وعشرون دقيقةً"

largest (default 2) caps how many units appear, biggest first. Want to drive the noun agreement yourself? countedNoun(n, forms) is exported for any custom counted noun.


File size — Arabic data units

import { formatFileSize } from "arabicfmt";

formatFileSize(0)                          // "0 بايت"
formatFileSize(1536)                       // "1.5 كيلوبايت"
formatFileSize(5 * 1024 * 1024)            // "5 ميجابايت"
formatFileSize(1_500_000, { base: 1000 })  // "1.5 ميجابايت"  (decimal/SI)
formatFileSize(2048, { numerals: "arab" }) // "٢ كيلوبايت"
formatFileSize(2048, { unitStyle: "latin" })// "2 KB"

Units scale through بايت · كيلوبايت · ميجابايت · جيجابايت · تيرابايت · بيتابايت, with base: 1024 (binary, default) or base: 1000 (decimal).


Arabic plural rules (6 forms)

Arabic has six plural forms — more than any other major language. Standard i18n libraries handle 1–2 forms and break for Arabic.

import { arabicPluralForm, arabicPlural } from "arabicfmt";

// Get the CLDR form name
arabicPluralForm(0)    // "zero"
arabicPluralForm(1)    // "one"
arabicPluralForm(2)    // "two"
arabicPluralForm(5)    // "few"   (3–10)
arabicPluralForm(15)   // "many"  (11–99)
arabicPluralForm(100)  // "other"

// Select the right string
const forms = {
  zero:  "لا كتب",
  one:   "كتاب واحد",
  two:   "كتابان",
  few:   "كتب",       // 3–10
  many:  "كتاباً",    // 11–99
  other: "كتاب",
};

arabicPlural(0,   forms)  // "لا كتب"
arabicPlural(1,   forms)  // "كتاب واحد"
arabicPlural(2,   forms)  // "كتابان"
arabicPlural(5,   forms)  // "كتب"
arabicPlural(25,  forms)  // "كتاباً"
arabicPlural(100, forms)  // "كتاب"

Hijri / Islamic calendar dates

Two engines with an identical API:

| | arabicfmt/date | arabicfmt/umalqura | |---|---|---| | Algorithm | Tabular arithmetic | Official Umm al-Qura tables | | Accuracy | ±1–2 days | Exact | | Bundle | Tiny (no tables) | Larger (frozen ICU tables) | | Range | Any year | AH 1300–1599 | | Deterministic | Yes | Yes — same on Node/Chrome/Safari/Hermes |

import { toHijri, fromHijri, formatHijri, umalquraToGregorian } from "arabicfmt/umalqura";

// Convert
toHijri(new Date("2025-09-23"))        // { year: 1447, month: 4, day: 1 }
umalquraToGregorian(1447, 9, 1)        // JavaScript Date — first day of Ramadan 1447

// Format — Arabic
formatHijri(new Date("2025-09-23"))
// "1 ربيع الآخر 1447 هـ"

formatHijri(new Date("2025-09-23"), { numerals: "arab" })
// "١ ربيع الآخر ١٤٤٧ هـ"

// Format — English
formatHijri(new Date("2025-09-23"), { locale: "en" })
// "1 Rabi al-Thani 1447 AH"

// Format — ISO-style numeric
formatHijri(new Date("2025-09-23"), {
  locale: "en", month: "2-digit", day: "2-digit", order: "ymd", era: false,
})
// "1447/04/01"

Month and weekday name tables

import {
  HIJRI_MONTHS_AR,     // Arabic Hijri month names
  HIJRI_MONTHS_EN,     // English Hijri month names
  GREGORIAN_MONTHS_AR, // Arabic Gregorian month names (يناير، فبراير…)
  GREGORIAN_MONTHS_EN,
  ARABIC_WEEKDAYS_AR,  // Arabic weekday names (الأحد، الاثنين…)
  ARABIC_WEEKDAYS_EN,
} from "arabicfmt/date";

HIJRI_MONTHS_AR[8]        // "رمضان"  (index 0 = Muharram)
GREGORIAN_MONTHS_AR[0]    // "يناير"  (index 0 = January)
ARABIC_WEEKDAYS_AR[5]     // "الجمعة" (index 0 = Sunday)

Bidirectional (RTL) text helpers

Stop phone numbers and English words from scrambling Arabic sentences:

import { detectDirection, isolate, isolateForeign, stripBidi } from "arabicfmt/bidi";

// Before fix: "+1 (555) 234-5678" flips the area code in RTL context
// After fix:  the phone number is wrapped in Unicode isolates — sentence intact
isolateForeign("اتصل على +1 (555) 234-5678 الآن");

detectDirection("مرحبا");   // "rtl"
detectDirection("Hello");   // "ltr"

isolate("9:41 AM");         // FSI … PDI isolate around a mixed run
stripBidi(dirtyStr);        // remove every Unicode bidi control character

Text normalization for Arabic search

Match Arabic text despite diacritics, alef variants, hamza and taa marbuta differences:

import {
  stripTashkeel,
  normalizeArabic,
  normalizeForSearch,
  sortArabic,
  compareArabic,
} from "arabicfmt/text";

stripTashkeel("مُحَمَّد")        // "محمد"
normalizeArabic("الأحمد")        // "الاحمد"  (alef variants unified)

// Robust search — these two strings produce the same key:
normalizeForSearch("مُؤسَّسة") === normalizeForSearch("موسسه")  // true

// Arabic-locale collation
sortArabic(["ياسر", "أحمد", "بسام"])   // ["أحمد", "بسام", "ياسر"]
["ج", "أ", "ب"].sort(compareArabic)    // ["أ", "ب", "ج"]

List formatting

Join values into a grammatical Arabic list. Wraps Intl.ListFormat and degrades gracefully on runtimes without it.

import { formatList } from "arabicfmt";

formatList(["أحمد", "محمد", "علي"])                      // "أحمد ومحمد وعلي"
formatList(["تفاح", "موز", "برتقال"], { type: "disjunction" }) // "تفاح أو موز أو برتقال"
formatList([1, 2, 3], { numerals: "arab" })              // "١ و٢ و٣"

Transliteration & URL slugs

Romanize Arabic script to readable Latin, or turn it into URL-safe slugs for routes, filenames and CMS permalinks. Deterministic — short vowels appear only when the text is vowelled (carries tashkeel).

import { transliterate, slugify } from "arabicfmt";

transliterate("مُحَمَّد")    // "muhammad"   (vowelled)
transliterate("محمد")        // "mhmd"       (bare → consonant-only)
transliterate("القاهرة")     // "alqahrh"
transliterate("غرفة ٢٠١")    // "ghrfh 201"  (digits converted)

slugify("مدينة نصر")                      // "mdynh-nsr"
slugify("القاهرة 2026")                   // "alqahrh-2026"
slugify("Hello العالم", { separator: "_" }) // "hello_alalm"
slugify("Hello World", { lowercase: false }) // "Hello-World"

Note: this is a pragmatic, reversible-ish romanization, not a strict academic transliteration (DIN 31635 / ISO 233). It is built for slugs, search keys and readable IDs.


Validation — IBAN & Saudi ID

Real checksums, not regex guesses. isValidIBAN runs the ISO 7064 mod-97 algorithm with SWIFT-registry length checks; isValidSaudiId runs the Luhn check digit and classifies citizen vs. resident.

import { isValidIBAN, formatIBAN, isValidSaudiId, saudiIdType } from "arabicfmt";

isValidIBAN("SA03 8000 0000 6080 1016 7519")  // true
isValidIBAN("SA03 8000 0000 6080 1016 7510")  // false (bad checksum)
formatIBAN("SA0380000000608010167519")        // "SA03 8000 0000 6080 1016 7519"

isValidSaudiId("1012345672")                  // true
saudiIdType("1012345672")                     // "citizen"
saudiIdType("2100000005")                     // "resident"  (Iqama)

Registry lengths are enforced for SA, AE, KW, BH, QA, JO, LB, EG, IQ, PS, TN, MR, LY (plus common partners). Unknown-country IBANs are validated by checksum and the general 15–34 length bound, never accepted on structure alone.


Framework usage

React / Next.js

import { formatCurrency, detectLocale } from "arabicfmt";
import { formatHijri } from "arabicfmt/umalqura";

export function PriceTag({ amount, currency }: { amount: number; currency: string }) {
  const locale = detectLocale();
  return (
    <span dir="rtl">
      {formatCurrency(amount, { currency, locale })}
    </span>
  );
}

export function HijriDate({ date }: { date: Date }) {
  return <time>{formatHijri(date, { numerals: "arab" })}</time>;
}

Vue 3

import { formatCurrency } from "arabicfmt";

// composable
export function useArabicCurrency(currency: string) {
  return (amount: number) =>
    formatCurrency(amount, { currency, numerals: "arab" });
}

Node.js / Express

import { formatCurrency, detectLocale } from "arabicfmt";
import { formatHijri } from "arabicfmt/umalqura";

app.get("/invoice/:id", (req, res) => {
  const locale = req.headers["accept-language"]?.split(",")[0] ?? "ar-SA";
  const total  = formatCurrency(order.total, { locale });
  const date   = formatHijri(order.date, { locale: "ar" });
  res.json({ total, date });
});

Locale auto-detection

import { detectLocale } from "arabicfmt";

// Browser: reads navigator.language
// Node.js: reads LANG / LANGUAGE / LC_ALL / LC_MESSAGES env vars
// Fallback: "ar"

const locale = detectLocale(); // "ar-SA", "ar-EG", "en-US", …
formatCurrency(1234, { locale });

Subpath imports — tree-shakeable

Pick only what you need for the smallest possible bundle:

import { formatCurrency, spellCurrency } from "arabicfmt/currency";
import { formatNumber, arabicToWords, formatDuration, formatFileSize } from "arabicfmt/number";
import { formatHijri, toHijri }  from "arabicfmt/date";       // tabular core (tiny)
import { formatHijri, toHijri }  from "arabicfmt/umalqura";   // accurate, opt-in
import { isolateForeign }        from "arabicfmt/bidi";
import { normalizeForSearch, arabicPlural, slugify } from "arabicfmt/text";
import { isValidIBAN, isValidSaudiId }      from "arabicfmt/validate";

Measured cost of each entry point (esbuild --bundle --minify, gzipped — v0.1.0):

| Import | What you get | min + gzip | |---|---|---| | arabicfmt | everything below | 11.4 kB | | arabicfmt/currency | 22 currencies, تفقيط, Unicode transition data | 5.7 kB | | arabicfmt/number | words, ordinals, fractions, parse, duration, … | 3.5 kB | | arabicfmt/umalqura | 300 years of official Umm al-Qura tables | 2.2 kB | | arabicfmt/text | normalize, plurals, collation, lists, slugs | 1.6 kB | | arabicfmt/date | tabular Hijri core | 1.5 kB | | arabicfmt/bidi | direction detection + isolates | 0.7 kB | | arabicfmt/validate | IBAN + Saudi ID checksums | 0.6 kB |

The complete Arabic formatting stack costs less than a single small image.


Full API reference

Every public function, by module. Full signatures and options are in the sections above and in the bundled TypeScript types.

| Module | Functions | |---|---| | arabicfmt/currency | formatCurrency · spellCurrency · getCurrencyInfo · resolveCurrencySymbol | | arabicfmt/number | formatNumber · formatPercent · formatCompact · parseNumber · parseCurrency · toArabicDigits · toLatinDigits · arabicToWords · arabicOrdinal · arabicFraction · countedNoun · formatDuration · formatFileSize · formatRelativeTime | | arabicfmt/umalqura | formatHijri · toHijri · fromHijri · gregorianToUmalqura · umalquraToGregorian | | arabicfmt/date | formatHijri · toHijri · fromHijri (tabular core) | | arabicfmt/text | stripTashkeel · removeTatweel · normalizeArabic · normalizeForSearch · arabicPlural · arabicPluralForm · sortArabic · compareArabic · createArabicCollator · formatList · transliterate · slugify | | arabicfmt/bidi | isolateForeign · isolate · wrapLTR · wrapRTL · stripBidi · detectDirection · isRTL · charDirection | | arabicfmt/validate | isValidIBAN · formatIBAN · normalizeIBAN · isValidSaudiId · saudiIdType | | arabicfmt (root) | re-exports everything above + detectLocale |


Engineering

| | | |---|---| | Dependencies | Zero runtime dependencies | | Size | ~11.4 kB min+gzip for the whole library; subpath imports from 0.6 kB | | Formats | Dual ESM + CJS, full .d.ts / .d.cts types | | Tree-shaking | "sideEffects": false — pay only for what you import | | Data source | CLDR 48.2.0 + ICU — verified at build time, not hand-typed | | Test coverage | 194 tests — currency transition, precision, Hijri, plurals, words, tafqit, durations, IBAN/ID | | Platforms | Node ≥ 18, all evergreen browsers, React Native / Hermes, Deno, Bun | | Published with | npm provenance (GitHub Actions attestation) |


Unicode currency-sign transition

Live since Unicode 18.0 (September 2026)

The UAE dirham (U+20C3) and Omani rial (U+20C4) signs are now live, and symbolMode: "auto" prefers them — completing the transition that began with the Saudi riyal sign (U+20C1) in Unicode 17.0. Because system-font coverage for brand-new signs still varies, symbolMode: "text" always returns the safe Arabic abbreviation (د.إ, ر.ع.), and the Saudi riyal keeps the text symbol as its auto default by design.

| Currency | Sign | Unicode | auto default | |---|---|---|---| | Saudi riyal (SAR) | U+20C1 | 17.0 (2025) | text ر.س (conservative) | | UAE dirham (AED) | U+20C3 | 18.0 (2026) | sign | | Omani rial (OMR) | U+20C4 | 18.0 (2026) | sign |


🌍 Live demo

arabicfmt.vercel.app — the whole library, interactive and computed live in your browser. Change any input and watch the Arabic update in real time: currency studio, تفقيط, Hijri converter, plurals, RTL fixes and more.

arabicfmt interactive playground

Run it locally:

cd demo && npm install && npm run dev

Contributing

Issues and pull requests are welcome on GitHub.


License

MIT — free for commercial and personal use.


Author & more projects

Built and maintained by cc1a2b.

If arabicfmt saves you time, please ⭐ star it on GitHub — it helps other Arabic developers find it. Explore my other open-source projects, or open an issue with ideas, bugs and feature requests.