The io.github.cc1a2b/arabicfmt-mcp MCP server provides Arabic-first formatting capabilities for JavaScript and TypeScript. Its scope includes currency formatting for 22 Arab League countries (including Saudi riyal U+20C1), Hijri (Islamic calendar) dates, تفقيط, and RTL/bidi-related fixes, with support for number-to-words and multiple plural forms.
🛠️ Key Features
Formats 22 Arab League countries’ currency symbols
Handles Hijri/Islamic calendar dates
Supports تفقيط
Provides RTL/bidi corrections
Includes number-to-words output
Mentions 6 plural forms
Includes full TypeScript types and zero dependencies
🚀 Use Cases
Localizing Arabic language (i18n/l10n) output
Displaying Arabic currencies, including Saudi riyal (U+20C1)
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.
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:
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// CurrencyformatCurrency(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 datatransitionStatus("AED"); // "encoded" (Unicode 18.0)transitionStatus("AED", newDate("2026-01-01")); // "announced"transitionStatus("KWD"); // "none" — Kuwait has no signsignFontFaceCSS({ src: "/fonts/signs.woff2" }); // @font-face … unicode-range: U+20C1…U+20C4// Number to Arabic wordsarabicToWords(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-awarearabicOrdinal(1); // "الأول"arabicOrdinal(25); // "الخامس والعشرون"arabicOrdinal(1, { gender: "female" }); // "الأولى"// Duration & file sizeformatDuration(7_500_000); // "ساعتان وخمس دقائق"formatFileSize(1536); // "1.5 كيلوبايت"// ListsformatList(["أحمد", "محمد", "علي"]); // "أحمد ومحمد وعلي"formatList(["تفاح", "موز"], { type: "disjunction" }); // "تفاح أو موز"// Hijri dates (deterministic — same output on Node, Chrome, Safari, Hermes)formatHijri(newDate("2025-09-23")); // "1 ربيع الآخر 1447 هـ"formatHijri(newDate("2025-09-23"), { numerals: "arab" }); // "١ ربيع الآخر ١٤٤٧ هـ"toHijri(newDate("2025-09-23")); // { year: 1447, month: 4, day: 1 }// Relative timeformatRelativeTime(newDate(Date.now() - 3 * 86400_000)); // "منذ 3 أيام"// Parse formatted strings back to numbersparseCurrency("١٬٢٣٤٫٥٠ ر.س"); // 1234.5parseCurrency("(500.00 SAR)"); // -500// RTLisolateForeign("اتصل على +1 (555) 234-5678 الآن"); // phone stays intact in RTL// Locale auto-detectiondetectLocale(); // "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 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:
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:
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):
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:
ts
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:
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.
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).
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.
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.
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.
Stop phone numbers and English words from scrambling Arabic sentences:
ts
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 intactisolateForeign("اتصل على +1 (555) 234-5678 الآن");
detectDirection("مرحبا"); // "rtl"detectDirection("Hello"); // "ltr"isolate("9:41 AM"); // FSI … PDI isolate around a mixed runstripBidi(dirtyStr); // remove every Unicode bidi control character
Text normalization for Arabic search
Match Arabic text despite diacritics, alef variants, hamza and taa marbuta differences:
ts
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 collationsortArabic(["ياسر", "أحمد", "بسام"]) // ["أحمد", "بسام", "ياسر"]
["ج", "أ", "ب"].sort(compareArabic) // ["أ", "ب", "ج"]
List formatting
Join values into a grammatical Arabic list. Wraps Intl.ListFormat and degrades gracefully on runtimes without it.
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).
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.
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.
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:
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
auto default
Saudi riyal (SAR)
U+20C1
17.0 (9 Sep 2025)
SAMA, 20 Feb 2025
text ر.س (conservative)
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:
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.
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.