arabicfmt-mcp
This server exposes Arabic formatting, parsing, and validation utilities via 15 MCP tools for AI agents.
Format currency: locale-aware Arabic currency formatting with correct symbols, precision, and Eastern Arabic numerals.
Spell currency: convert amounts to full Arabic tafqit words with grammatical agreement.
Hijri dates: format or convert Gregorian dates to Hijri/Islamic calendar dates.
Arabic words & ordinals: convert integers to Arabic cardinal or ordinal words, with gender support.
Number formatting: format numbers, compact notation, and parse Arabic/Latin numeric strings.
Duration & relative time: express durations or dates in natural Arabic phrases.
Plural selection: pick the correct Arabic plural form from the six CLDR categories.
RTL/bidi handling: isolate foreign/LTR runs inside Arabic text.
Transliteration & slugs: romanize Arabic text and generate URL-safe slugs.
Validation: validate IBANs and Saudi national/Iqama ID numbers.
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.
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, MVRU+20C2, AEDU+20C3, OMRU+20C4— plus the tooling to actually render them: typed parts, a generated@font-face, price ranges and a date-aware transition registry.
npm install arabicfmtWhat 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 |
Styling a new sign | Regex the symbol back out of a formatted string |
|
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 |
|
Spell money for cheques (تفقيط) | Build it yourself, get the grammar wrong |
|
Ordinals (ترتيبية) | Missing or gender-blind |
|
Spoken durations |
|
|
RTL broken sentences | Phone numbers flip mid-sentence | Unicode isolates wrap LTR runs automatically |
Eastern Arabic digit parsing |
|
|
Arabic URL slugs | Strip to empty or mojibake |
|
IBAN / Saudi ID checks | Regex that accepts bad numbers | Real ISO 7064 mod-97 + Luhn checksums |
Related MCP server: arabic-dict-mcp
Install
npm install arabicfmt
# or
yarn add arabicfmt
# or
pnpm add arabicfmtRequirements: 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. arabicfmt@0.1.
Quick start
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_EGCurrency 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)
| SAR | AED | OMR | Use when |
|
|
|
| Default. AED/OMR use the dedicated sign; SAR stays on safe text |
|
|
|
| Force the dedicated sign (needs font support) |
|
|
|
| Always the safe text symbol — renders everywhere |
|
|
|
| ISO code |
Unicode 18.0 (16 September 2026): the AED (U+20C3), OMR (U+20C4) and MVR (U+20C2) signs are now
live, andautoprefers them. Need maximum compatibility today? UsesymbolMode: "text". The Saudi riyal keeps its safe text default by design. See the transition section 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:
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 decimalsDecimals | Currencies |
3 | KWD, BHD, OMR, JOD, IQD, LYD, TND |
0 | DJF, KMF |
2 | SAR, AED, QAR, 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: "ريال سعودي"
// }Price ranges
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 rangePass 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:
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):
<span dir="rtl">
{formatCurrencyToParts(total, { currency: "SAR", symbolMode: "new" }).map(
(part, i) =>
part.needsFont
? <span key={i} className="riyal-sign">{part.value}</span>
: <span key={i}>{part.value}</span>,
)}
</span>Part types mirror Intl (integer, group, decimal, fraction,
minusSign, plusSign, literal) plus four arabicfmt adds: currency,
parenthesis (accounting), isolate (bidi controls) and rangeSeparator.
2. The @font-face rule — generated, with the unicode-range that makes it
free on pages that never print a sign:
import { signFontFaceCSS } from "arabicfmt/currency";
signFontFaceCSS({ src: "/fonts/currency-signs.woff2" });
// @font-face {
// font-family: "Arabicfmt Signs";
// src: url("/fonts/currency-signs.woff2") format("woff2");
// font-display: swap;
// unicode-range: U+20C1, U+20C2, U+20C3, U+20C4;
// }
// Saudi riyal only, into a family name you already use:
signFontFaceCSS({ src: "/fonts/riyal.woff2", family: "Riyal", currencies: ["SAR"] });
// unicode-range: U+20C1;Because the range is scoped to those codepoints, the browser downloads the font only when one of them is actually painted — body text is untouched. Put the family first in your stack and the rest falls through:
:root { font-family: "Arabicfmt Signs", "Noto Naskh Arabic", sans-serif; }signUnicodeRange() returns just the range string ("U+20C1, U+20C2, U+20C3, U+20C4") for CSS-in-JS. Both helpers reject anything that would break out of
the rule, so a URL from config can't inject CSS.
Number formatting
import {
formatNumber, formatCompact, formatPercent,
toArabicDigits, toExtendedArabicDigits, 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%"
// Three numeral systems: latn (default), arab, arabext
formatNumber(1234.5, { numerals: "arab" }); // "١٬٢٣٤٫٥" Eastern Arabic
formatNumber(1234.5, { numerals: "arabext" }); // "۱٬۲۳۴٫۵" Persian/Urdu
// Transliteration
toArabicDigits("Order #2026"); // "Order #٢٠٢٦"
toExtendedArabicDigits("2026"); // "۲۰۲۶"
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: "SAR", 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:
|
| |
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 characterText 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("الرياض") // "alryad"
transliterate("غرفة ٢٠١") // "ghrfh 201" (digits converted)
slugify("مدينة جدة") // "mdynh-jdh"
slugify("الرياض 2026") // "alryad-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.5):
Import | What you get | min + gzip |
| everything below | 13.2 kB |
| 22 currencies, تفقيط, parts, ranges, transition registry | 7.6 kB |
| words, ordinals, fractions, parse, duration, … | 3.6 kB |
| 300 years of official Umm al-Qura tables | 2.4 kB |
| normalize, plurals, collation, lists, slugs | 1.7 kB |
| tabular Hijri core | 1.6 kB |
| direction detection + isolates | 0.8 kB |
| IBAN + Saudi ID checksums | 0.7 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 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| re-exports everything above + |
MCP server — use arabicfmt from AI agents
AI agents (Claude Desktop, Claude Code, Cursor) can call arabicfmt directly through the
arabicfmt-mcp Model Context Protocol server —
21 tools (format_currency, format_currency_range, currency_transition,
spell_currency, format_hijri, arabic_to_words, isolate_foreign,
validate_iban, …). Add it to your client's mcpServers config:
{
"mcpServers": {
"arabicfmt": { "command": "npx", "args": ["-y", "arabicfmt-mcp"] }
}
}Source and full tool list: mcp/.
Examples
Runnable scripts for every feature live in examples/:
cd examples && npm install
node currency.mjs # or numbers / words / dates / text / bidi / validateEngineering
Dependencies | Zero runtime dependencies |
Size | ~13.2 kB min+gzip for the whole library; subpath imports from 0.7 kB |
Formats | Dual ESM + CJS, full |
Tree-shaking |
|
Data source | CLDR 48.2.0 + ICU — verified at build time, not hand-typed |
Test coverage | 242 tests — currency transition, parts, ranges, 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
Complete as of Unicode 18.0 — released 16 September 2026
Four currencies moved from an ad-hoc abbreviation to a dedicated Unicode sign in
this cycle, and arabicfmt carries all four. Note what is not on this list:
Kuwait has never issued a dinar sign — KWD is still د.ك / KD, and
transitionStatus("KWD") returns "none".
Currency | Sign | Codepoint | Unicode | Announced |
|
Saudi riyal (SAR) |
| U+20C1 | 17.0 (9 Sep 2025) | SAMA, 20 Feb 2025 | text |
Maldivian rufiyaa (MVR) |
| U+20C2 | 18.0 (16 Sep 2026) | MMA, 3 Jul 2022 | sign |
UAE dirham (AED) |
| U+20C3 | 18.0 (16 Sep 2026) | CBUAE, 27 Mar 2025 | sign |
Omani rial (OMR) |
| U+20C4 | 18.0 (16 Sep 2026) | CBO, 19 Nov 2025 | sign |
The rufiyaa is not an Arab League currency, but it was encoded in the same batch
and sits between the dirham and rial in the Currency Symbols block — carrying it
keeps the range contiguous, so one unicode-range covers the whole transition.
Because system-font coverage for brand-new signs still varies, symbolMode: "text" always returns the safe abbreviation, and the Saudi riyal keeps the text
symbol as its auto default by design.
The transition as data
Encoding is not rendering. A sign exists in the standard months or years before system fonts draw it, and every product shipping in that window has to decide what to print. That timeline is queryable:
import {
transitionStatus,
listCurrencyTransitions,
getCurrencyTransition,
} from "arabicfmt/currency";
transitionStatus("SAR") // "encoded"
transitionStatus("SAR", new Date("2025-03-01")) // "announced" — SAMA had it, Unicode didn't
transitionStatus("AED", new Date("2026-01-01")) // "announced"
transitionStatus("KWD") // "none" — no sign exists
listCurrencyTransitions({ unicodeVersion: "18.0" }).map((t) => t.code);
// ["MVR", "AED", "OMR"]
getCurrencyTransition("OMR");
// {
// code: "OMR", sign: "", codepoint: "U+20C4", name: "OMANI RIAL SIGN",
// unicodeVersion: "18.0", unicodeReleased: "2026-09-16",
// announced: "2025-11-19", authority: "Central Bank of Oman (CBO)",
// autoDefault: true, textSymbol: "ر.ع.",
// }transitionStatus takes a moment in time, so a historical invoice can be
re-rendered with the symbol that was correct on its own date.
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.

Run it locally:
cd demo && npm install && npm run devContributing
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.
Available Tools
17 toolsarabic_ordinalA
Convert an integer to its Arabic ordinal words (e.g. 25 -> "الخامس والعشرون").
Inputs: n (required), feminine.
arabic_pluralA
Select the correct Arabic plural form for a count from the six CLDR categories (e.g. 5 books -> "كتب"). Returns the chosen word.
Inputs: n (required), forms (required).
arabic_to_wordsA
Convert an integer to its full Arabic cardinal words (e.g. 1234567 -> "مليون ومئتان وأربعة وثلاثون ألفاً وخمسمئة وسبعة وستون").
Inputs: n (required), feminine.
format_compactA
Format a number in compact notation with Arabic scale words (e.g. 1200000 with locale "ar" -> "1.2 مليون").
Inputs: value (required), locale, numerals, compactDisplay.
format_currencyA
Format a number as an Arabic currency amount with the correct symbol, grouping, and decimal precision (e.g. 1234.5 SAR -> "1,234.50 ر.س"). Handles the 2025-2026 Unicode currency-symbol transition (Saudi Riyal U+20C1, etc.).
Inputs: amount (required), locale, currency, numerals.
format_durationA
Format a duration in milliseconds as natural Arabic words (e.g. 7500000 -> "ساعتان وخمس دقائق").
Inputs: value (required), locale, numerals.
format_hijriB
Format a Gregorian date as a Hijri (Islamic) date string with Arabic month names and era (e.g. 2025-09-23 -> "٢٣ رمضان ١٤٤٧ هـ").
Inputs: date (required), locale, numerals.
format_numberA
Format a number with locale-aware grouping, decimal separators, and optional Eastern Arabic numerals (e.g. 1234567.89 with numerals "arab" -> "١٬٢٣٤٬٥٦٧٫٨٩").
Inputs: value (required), locale, numerals.
format_relative_timeA
Format a date relative to a base date in Arabic (e.g. three days ago -> "منذ ٣ أيام"). Defaults the base to now.
Inputs: date (required), base, locale, numerals.
isolate_foreignA
Wrap foreign/LTR runs (phone numbers, Latin text, URLs) inside Arabic text with Unicode bidi isolates so they render correctly in RTL contexts (e.g. "اتصل على +1 (555) 234-5678 الآن").
Inputs: text (required).
parse_numberA
Parse a string containing Latin or Eastern Arabic digits (with Arabic grouping/decimal separators) into a JavaScript number (e.g. "١٬٢٣٤٫٥٦" -> 1234.56). Returns the number as a string.
Inputs: input (required).
slugifyA
Convert Arabic (or mixed) text into a URL-safe slug (e.g. "مدينة جدة" -> "mdynh-jdh").
Inputs: text (required), separator.
spell_currencyA
Spell out a currency amount in full Arabic words, including the major and minor units with correct grammatical agreement (e.g. 1234.5 SAR -> "ألف ومئتان وأربعة وثلاثون ريالاً وخمسون هللة").
Inputs: amount (required), locale, currency.
to_hijriA
Convert a Gregorian date to its Hijri components. Returns a JSON object string like {"year":1447,"month":9,"day":23}.
Inputs: date (required).
transliterateA
Transliterate Arabic text to Latin script (e.g. "مُحَمَّد" -> "muhammad").
Inputs: text (required).
validate_ibanA
Validate an IBAN (International Bank Account Number) using its checksum and country length (e.g. "SA03 8000 0000 6080 1016 7519" -> "true"). Returns "true" or "false".
Inputs: iban (required).
validate_saudi_idA
Validate a Saudi national/iqama ID number using its Luhn-style checksum (e.g. "1012345672" -> "true"). Returns "true" or "false".
Inputs: id (required).
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 17 tools
Most tools have clearly distinct purposes, though some overlap exists (e.g., format_number vs format_compact, format_currency vs spell_currency). Descriptions effectively differentiate them, so confusion is minimal.
All names use snake_case, but patterns vary: some are verb_noun (format_currency), some descriptive noun (arabic_ordinal), and some single verbs (slugify). This mild inconsistency prevents a higher score.
17 tools cover a wide range of Arabic formatting and validation tasks without feeling excessive. The count is appropriate for the server's scope, though it could potentially be trimmed by merging some overlapping functions.
The set covers number conversion, date formatting (Hijri), currency, duration, relative time, transliteration, and validation (IBAN, Saudi ID). Missing a general Gregorian date formatter, but the core domain is well-served.
Maintenance
Related MCP Connectors
Lingo.dev MCP Server - World-class i18n implementation with ICU MessageFormat.
MCP server for Quaderno — tax-rate calculation, invoices, contacts, products, receipts & expenses.
MCP server for Modern Treasury — payment orders, transactions, counterparties and ledgers.
Related MCP Servers
- AlicenseAqualityDmaintenanceMCP server exposing Arabic text utilities: normalisation, tashkeel stripping, transliteration, heuristic dialect detection, and token counting.5MIT
- AlicenseAqualityDmaintenanceMCP server that provides grounded Arabic dictionary lookup, enabling AI agents to search word roots, inflections, and meanings from real dictionary data.32MIT
- AlicenseBqualityAmaintenanceAn MCP server for elite Arabic and bilingual RTL/LTR UI/UX design, providing tools for design pipelines, anti-AI-slop auditing, RTL layout conversion, Arabic typography optimization, and accessibility checks.14MIT
- AlicenseNot gradedqualityCmaintenanceProvides Arabic UI/UX design tools as an MCP server, offering RTL conversion, Arabic typography validation, cultural palettes, and anti-AI-slop auditing for AI-assisted design workflows.MIT