{
  "markdown": "<div align=\"center\"> \n\n# arabicfmt\n\n### Arabic-first formatting for JavaScript &amp; TypeScript\n\nCurrency symbols · Hijri/Islamic calendar · number-to-words · تفقيط · 6 plural forms · RTL/bidi —<br/>\ncorrect for all **22 Arab League countries**, with **zero dependencies** and full TypeScript types.\n\n<p lang=\"ar\" dir=\"rtl\"><em>أرقامٌ وعملاتٌ وتواريخُ هجريةٌ ولغةٌ عربيةٌ سليمة — في سطرٍ واحد.</em></p>\n\n[![npm version](https://img.shields.io/npm/v/arabicfmt?style=flat-square&color=brightgreen)](https://www.npmjs.com/package/arabicfmt) [![downloads](https://img.shields.io/npm/dt/arabicfmt?style=flat-square&label=downloads)](https://www.npmjs.com/package/arabicfmt) [![jsDelivr hits](https://img.shields.io/jsdelivr/npm/hm/arabicfmt?style=flat-square)](https://www.jsdelivr.com/package/npm/arabicfmt) [![gzipped size](https://img.shields.io/bundlephobia/minzip/arabicfmt?style=flat-square&label=gzipped)](https://bundlephobia.com/package/arabicfmt) [![zero dependencies](https://img.shields.io/badge/deps-0-brightgreen?style=flat-square)](./package.json) [![types included](https://img.shields.io/npm/types/arabicfmt?style=flat-square)](https://www.npmjs.com/package/arabicfmt)\n\n<a href=\"https://arabicfmt.vercel.app\"><img src=\"https://raw.githubusercontent.com/cc1a2b/arabicfmt/main/assets/hero.png\" alt=\"arabicfmt — interactive Arabic formatting playground\" width=\"820\" /></a>\n\n**[npm](https://www.npmjs.com/package/arabicfmt) · [Live demo](https://arabicfmt.vercel.app) · [GitHub](https://github.com/cc1a2b/arabicfmt)**\n\n</div>\n\n> **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**.\n\n```sh\nnpm install arabicfmt\n```\n\n---\n\n## What other libraries get wrong\n\n| Problem | Other libraries | arabicfmt |\n|---|---|---|\n| Saudi riyal U+20C1 | Emits ﷼ (U+FDFC) — the **Iranian** rial | Correct U+20C1 with a safe text fallback |\n| Iraqi dinar (IQD) decimals | 0 (CLDR practical) | **3 decimals** — ISO 4217 legal standard |\n| Hijri date output | Varies between Node, Chrome, Safari, Hermes | Frozen Umm al-Qura tables — **identical on every engine** |\n| Arabic plurals | 1–2 forms; Arabic legally needs 6 | Full CLDR 6-form system (zero/one/two/few/many/other) |\n| Number to Arabic words | No zero-dep solution | `arabicToWords(1234)` → \"ألف ومئتان وأربعة وثلاثون\" |\n| Spell money for cheques (تفقيط) | Build it yourself, get the grammar wrong | `spellCurrency(1234.5, {currency:\"SAR\"})` → \"ألف ومئتان وأربعة وثلاثون ريالاً وخمسون هللةً\" |\n| Ordinals (ترتيبية) | Missing or gender-blind | `arabicOrdinal(25)` → \"الخامس والعشرون\", gender-aware |\n| Spoken durations | `Intl.DurationFormat` barely supported | `formatDuration(7_500_000)` → \"ساعتان وخمس دقائق\" with full agreement |\n| RTL broken sentences | Phone numbers flip mid-sentence | Unicode isolates wrap LTR runs automatically |\n| Eastern Arabic digit parsing | `parseInt(\"١٢٣\")` → NaN | `parseNumber(\"١٬٢٣٤٫٥٦\")` → 1234.56 |\n| Arabic URL slugs | Strip to empty or mojibake | `slugify(\"مدينة جدة\")` → \"mdynh-jdh\" |\n| IBAN / Saudi ID checks | Regex that accepts bad numbers | Real ISO 7064 mod-97 + Luhn checksums |\n\n---\n\n## Install\n\n```sh\nnpm install arabicfmt\n# or\nyarn add arabicfmt\n# or\npnpm add arabicfmt\n```\n\n**Requirements:** Node.js ≥ 18 · TypeScript ≥ 4.7 (optional) · zero runtime dependencies.\n\n### Browser / CDN — no build step\n\nEvery 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:\n\n```html\n<script type=\"module\">\n  import { formatCurrency, formatHijri } from \"https://cdn.jsdelivr.net/npm/arabicfmt/+esm\";\n\n  console.log(formatCurrency(1234.5, { currency: \"SAR\", numerals: \"arab\" })); // ١٬٢٣٤٫٥٠ ر.س\n  console.log(formatHijri(new Date(), { numerals: \"arab\" }));                 // ٢٧ ذو الحجة ١٤٤٧ هـ\n</script>\n```\n\nSubpaths 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`.\n\n---\n\n## Quick start\n\n```ts\nimport {\n  formatCurrency,      // correct symbol + precision for every Arab currency\n  formatCompact,       // 1,200,000 → \"1.2M\" / \"١٫٢ مليون\"\n  arabicToWords,       // 1234 → \"ألف ومئتان وأربعة وثلاثون\"\n  spellCurrency,       // تفقيط: 1234.5 SAR → \"...ريالاً وخمسون هللةً\"\n  arabicOrdinal,       // 25 → \"الخامس والعشرون\"\n  formatDuration,      // 7_500_000ms → \"ساعتان وخمس دقائق\"\n  formatFileSize,      // 1536 → \"1.5 كيلوبايت\"\n  formatRelativeTime,  // \"منذ ٣ أيام\"\n  formatList,          // [\"أحمد\",\"علي\"] → \"أحمد وعلي\"\n  parseCurrency,       // \"١٬٢٣٤٫٥٠ ر.س\" → 1234.5\n  arabicPlural,        // full 6-form Arabic plural selection\n  sortArabic,          // Arabic-locale collation\n  slugify,             // \"مدينة جدة\" → \"mdynh-jdh\" (URL slugs)\n  isValidIBAN,         // ISO 7064 mod-97 IBAN checksum\n  isValidSaudiId,      // Saudi national ID / Iqama check digit\n  isolateForeign,      // fix broken RTL sentences\n  normalizeForSearch,  // search-key normalization\n  detectLocale,        // auto-detect from browser / Node environment\n} from \"arabicfmt\";\n\nimport { formatHijri, toHijri } from \"arabicfmt/umalqura\"; // deterministic Hijri calendar\n\n// Currency\nformatCurrency(1.2,   { currency: \"KWD\" });                      // \"1.200 د.ك\"\nformatCurrency(1234,  { locale: \"ar-SA\", numerals: \"arab\" });    // \"١٬٢٣٤٫٠٠ ر.س\"\nformatCurrency(-500,  { currency: \"SAR\", accounting: true });    // \"(500.00 ر.س)\"\nformatCompact(1_500_000, { locale: \"ar\", numerals: \"arab\" });    // \"١٫٥ مليون\"\n\n// Number to Arabic words\narabicToWords(1234);                     // \"ألف ومئتان وأربعة وثلاثون\"\narabicToWords(1_000_000);               // \"مليون\"\narabicToWords(5, { gender: \"female\" }); // \"خمس\"\n\n// Spell money for invoices & cheques (التفقيط)\nspellCurrency(1234.5, { currency: \"SAR\" });\n// \"ألف ومئتان وأربعة وثلاثون ريالاً وخمسون هللةً\"\nspellCurrency(100, { currency: \"SAR\", suffix: true }); // \"مئة ريال فقط لا غير\"\n\n// Ordinals — gender-aware\narabicOrdinal(1);                        // \"الأول\"\narabicOrdinal(25);                       // \"الخامس والعشرون\"\narabicOrdinal(1, { gender: \"female\" });  // \"الأولى\"\n\n// Duration & file size\nformatDuration(7_500_000);               // \"ساعتان وخمس دقائق\"\nformatFileSize(1536);                    // \"1.5 كيلوبايت\"\n\n// Lists\nformatList([\"أحمد\", \"محمد\", \"علي\"]);                    // \"أحمد ومحمد وعلي\"\nformatList([\"تفاح\", \"موز\"], { type: \"disjunction\" });   // \"تفاح أو موز\"\n\n// Hijri dates (deterministic — same output on Node, Chrome, Safari, Hermes)\nformatHijri(new Date(\"2025-09-23\"));                            // \"1 ربيع الآخر 1447 هـ\"\nformatHijri(new Date(\"2025-09-23\"), { numerals: \"arab\" });     // \"١ ربيع الآخر ١٤٤٧ هـ\"\ntoHijri(new Date(\"2025-09-23\"));                               // { year: 1447, month: 4, day: 1 }\n\n// Relative time\nformatRelativeTime(new Date(Date.now() - 3 * 86400_000));      // \"منذ 3 أيام\"\n\n// Parse formatted strings back to numbers\nparseCurrency(\"١٬٢٣٤٫٥٠ ر.س\");   // 1234.5\nparseCurrency(\"(500.00 SAR)\");    // -500\n\n// RTL\nisolateForeign(\"اتصل على +1 (555) 234-5678 الآن\"); // phone stays intact in RTL\n\n// Locale auto-detection\ndetectLocale(); // \"ar-SA\" in a Saudi browser, \"ar-EG\" in Node with LANG=ar_EG\n```\n\n---\n\n## Currency formatting\n\n### Symbol strategy: `symbolMode`\n\nThe **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:\n\n```ts\nimport { formatCurrency, resolveCurrencySymbol, getCurrencyInfo } from \"arabicfmt/currency\";\n\nformatCurrency(1234.5, { currency: \"SAR\" });\n// → \"1,234.50 ر.س\"   (auto: safe text symbol, renders everywhere today)\n\nformatCurrency(1234.5, { currency: \"SAR\", symbolMode: \"new\" });\n// → \"1,234.50 ⃁\"     (U+20C1 — use with a webfont; see webfont guide below)\n\nformatCurrency(1234.5, { currency: \"SAR\", symbolMode: \"code\" });\n// → \"1,234.50 SAR\"   (ISO code — for accounting tables)\n```\n\n| `symbolMode` | SAR | AED | OMR | Use when |\n|---|---|---|---|---|\n| `auto` *(default)* | `ر.س` | `⃃` U+20C3 | `⃄` U+20C4 | Default. AED/OMR use the dedicated sign; SAR stays on safe text |\n| `new` | `⃁` U+20C1 | `⃃` U+20C3 | `⃄` U+20C4 | Force the dedicated sign (needs font support) |\n| `text` | `ر.س` | `د.إ` | `ر.ع.` | Always the safe text symbol — renders everywhere |\n| `code` | `SAR` | `AED` | `OMR` | ISO code |\n\n> **Unicode 18.0 (September 2026):** the AED (U+20C3) and OMR (U+20C4) signs are now\n> `live`, and `auto` prefers them. Need maximum compatibility today? Use\n> `symbolMode: \"text\"`. The Saudi riyal keeps its safe text default by design.\n\n### Correct decimal precision — all 22 Arab League countries\n\nGenerated from CLDR 48.2.0 at build time and **verified on every build**:\n\n```ts\nformatCurrency(1.2, { currency: \"KWD\" });  // \"1.200 د.ك\"  ← 3 decimals\nformatCurrency(1.2, { currency: \"BHD\" });  // \"1.200 د.ب\"  ← 3 decimals\nformatCurrency(1.2, { currency: \"IQD\" });  // \"1.200 ع.د\"  ← 3 decimals (ISO 4217, not CLDR's 0)\nformatCurrency(500, { currency: \"KMF\" });  // \"500 ف.ج.ق\"  ← 0 decimals\nformatCurrency(500, { currency: \"SAR\" });  // \"500.00 ر.س\" ← 2 decimals\n```\n\n| Decimals | Currencies |\n|---|---|\n| **3** | KWD, BHD, OMR, JOD, IQD, LYD, TND |\n| **0** | DJF, KMF |\n| 2 | SAR, AED, QAR, and the rest |\n\n### All currency options\n\n```ts\n// Resolve from locale region — no need to know the currency code\nformatCurrency(99.9,  { locale: \"ar-BH\" });                  // \"99.900 د.ب\"\nformatCurrency(1234,  { locale: \"ar-AE\", numerals: \"arab\", symbolMode: \"text\" }); // \"١٬٢٣٤٫٠٠ د.إ\"  (auto → U+20C3 sign)\n\n// Accounting notation (negatives in parentheses)\nformatCurrency(-1234.5, { currency: \"SAR\", accounting: true }); // \"(1,234.50 ر.س)\"\n\n// Hide/override\nformatCurrency(100, { currency: \"SAR\", showSymbol: false, fractionDigits: 0 }); // \"100\"\n\n// Currency metadata\ngetCurrencyInfo(\"SAR\");\n// {\n//   code: \"SAR\", digits: 2,\n//   symbols: { auto: \"ر.س\", text: \"ر.س\", code: \"SAR\", new: \"⃁\" },\n//   unicode: { codepoint: \"U+20C1\", unicodeVersion: \"17.0\", live: true, autoDefault: false },\n//   displayName: \"ريال سعودي\"\n// }\n```\n\n### Webfont guide for U+20C1\n\n```css\n/* Scope the Saudi Riyal font to just that codepoint — zero impact on body text */\n@font-face {\n  font-family: \"Riyal\";\n  src: url(\"/fonts/saudi-riyal.woff2\") format(\"woff2\");\n  unicode-range: U+20C1;\n}\n:root { font-family: \"Riyal\", \"Noto Naskh Arabic\", sans-serif; }\n```\n\n---\n\n## Number formatting\n\n```ts\nimport {\n  formatNumber, formatCompact, formatPercent,\n  toArabicDigits, toLatinDigits,\n  parseNumber, parseCurrency,\n  arabicToWords,\n  formatRelativeTime,\n} from \"arabicfmt/number\";\n\n// Standard\nformatNumber(1_234_567.89, { locale: \"en\" });              // \"1,234,567.89\"\nformatNumber(1234.5,       { numerals: \"arab\" });           // \"١٬٢٣٤٫٥\"\n\n// Compact / short notation — dashboards and data cards\nformatCompact(1_500_000);                                   // \"1.5M\"\nformatCompact(1_500_000, { locale: \"ar\" });                 // \"1.5 مليون\"\nformatCompact(1_500_000, { locale: \"ar\", numerals: \"arab\" }); // \"١٫٥ مليون\"\n\n// Percent\nformatPercent(0.853, { locale: \"en\" });                    // \"85.3%\"\n\n// Transliteration\ntoArabicDigits(\"Order #2026\");                             // \"Order #٢٠٢٦\"\ntoLatinDigits(\"٢٠٢٦\");                                     // \"2026\"  (handles Persian ۰–۹ too)\n\n// Parsing — round-trip support\nparseNumber(\"١٬٢٣٤٫٥٦\");         // 1234.56  (Eastern Arabic digits + separators)\nparseNumber(\"1,234.56\");          // 1234.56  (Western)\nparseCurrency(\"١٬٢٣٤٫٥٠ ر.س\");  // 1234.5\nparseCurrency(\"(500.00 SAR)\");   // -500      (accounting notation)\n\n// Relative time\nformatRelativeTime(new Date(Date.now() - 3 * 86400_000));            // \"منذ 3 أيام\"\nformatRelativeTime(new Date(Date.now() + 3600_000), new Date(), { locale: \"en\" }); // \"in 1 hour\"\n```\n\n---\n\n## Number to Arabic words (`arabicToWords`)\n\nConvert integers to their Arabic word representation — handles gender agreement and all six scale levels.\n\n```ts\nimport { arabicToWords } from \"arabicfmt\";\n\n// Basic\narabicToWords(0)       // \"صفر\"\narabicToWords(1)       // \"واحد\"\narabicToWords(2)       // \"اثنان\"\narabicToWords(11)      // \"أحد عشر\"\narabicToWords(25)      // \"خمسة وعشرون\"\narabicToWords(100)     // \"مئة\"\narabicToWords(350)     // \"ثلاثمئة وخمسون\"\n\n// Thousands\narabicToWords(1000)    // \"ألف\"\narabicToWords(2000)    // \"ألفان\"\narabicToWords(5000)    // \"خمسة آلاف\"\narabicToWords(11000)   // \"أحد عشر ألفاً\"\narabicToWords(100000)  // \"مئة ألف\"\n\n// Millions / billions\narabicToWords(1_000_000)    // \"مليون\"\narabicToWords(2_000_000)    // \"مليونان\"\narabicToWords(5_000_000)    // \"خمسة ملايين\"\narabicToWords(1_000_000_000)// \"مليار\"\n\n// Large composite\narabicToWords(1_234_567)\n// \"مليون ومئتان وأربعة وثلاثون ألفاً وخمسمئة وسبعة وستون\"\n\n// Gender agreement — feminine noun (ليرة، روبية…)\narabicToWords(3, { gender: \"female\" })  // \"ثلاث\"\narabicToWords(5, { gender: \"female\" })  // \"خمس\"\n\n// Negative\narabicToWords(-42)  // \"سالب اثنان وأربعون\"\n\n// Decimals — opt in (default truncates, stays backward compatible)\narabicToWords(3.14, { fraction: \"digits\" })  // \"ثلاثة فاصلة واحد أربعة\"\narabicToWords(3.14, { fraction: \"number\" })  // \"ثلاثة فاصلة أربعة عشر\"\n\n// Common fractions (denominators 2–10)\nimport { arabicFraction } from \"arabicfmt\";\narabicFraction(1, 2)  // \"نصف\"\narabicFraction(3, 4)  // \"ثلاثة أرباع\"\narabicFraction(2, 3)  // \"ثلثان\"\n```\n\n---\n\n## Spell money in words — التفقيط\n\n`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).\n\n```ts\nimport { spellCurrency } from \"arabicfmt\";\n\nspellCurrency(1234.5, { currency: \"SAR\" })\n// \"ألف ومئتان وأربعة وثلاثون ريالاً وخمسون هللةً\"\n\n// Unit agreement is automatic (العدد والمعدود)\nspellCurrency(1,   { currency: \"SAR\" })   // \"ريال واحد\"      (singular)\nspellCurrency(2,   { currency: \"SAR\" })   // \"ريالان\"         (dual)\nspellCurrency(3,   { currency: \"SAR\" })   // \"ثلاثة ريالات\"   (plural, 3–10)\nspellCurrency(11,  { currency: \"SAR\" })   // \"أحد عشر ريالاً\" (accusative, 11–99)\nspellCurrency(100, { currency: \"SAR\" })   // \"مئة ريال\"       (genitive singular)\n\n// Minor-unit precision comes from CLDR — KWD = 1000 fils, SAR = 100 halalas\nspellCurrency(1.5, { currency: \"KWD\" })   // \"دينار واحد وخمسمئة فلس\"\nspellCurrency(0.75, { currency: \"SAR\" })  // \"خمس وسبعون هللةً\"\n\n// Cheque-ready ending and locale-derived currency\nspellCurrency(100, { currency: \"SAR\", suffix: true }) // \"مئة ريال فقط لا غير\"\nspellCurrency(-5,  { locale: \"ar-AE\" })               // \"سالب خمسة دراهم\"\n```\n\nFull 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.\n\n---\n\n## Ordinal numbers — الأعداد الترتيبية\n\n```ts\nimport { arabicOrdinal } from \"arabicfmt\";\n\narabicOrdinal(1)    // \"الأول\"\narabicOrdinal(2)    // \"الثاني\"\narabicOrdinal(10)   // \"العاشر\"\narabicOrdinal(11)   // \"الحادي عشر\"\narabicOrdinal(25)   // \"الخامس والعشرون\"\n\n// Gender agreement\narabicOrdinal(1, { gender: \"female\" })   // \"الأولى\"\narabicOrdinal(25, { gender: \"female\" })  // \"الخامسة والعشرون\"\n\n// Indefinite (drop the article ال)\narabicOrdinal(3, { definite: false })    // \"ثالث\"\narabicOrdinal(25, { definite: false })   // \"خامس وعشرون\"\n```\n\n---\n\n## Duration — spelled Arabic\n\n`formatDuration` turns a time span into its spoken Arabic form, with correct\ndual/plural/accusative agreement on every unit — something `Intl.DurationFormat`\n(still barely supported) does not give you.\n\n```ts\nimport { formatDuration } from \"arabicfmt\";\n\nformatDuration(7_500_000)                  // \"ساعتان وخمس دقائق\"  (2h 5m)\nformatDuration(90, { input: \"s\" })         // \"دقيقة واحدة وثلاثون ثانيةً\"\nformatDuration(3_600_000, { largest: 1 })  // \"ساعة واحدة\"\nformatDuration(2 * 86_400_000)             // \"يومان\"\nformatDuration(500)                        // \"أقل من ثانية\"\n\n// Restrict the units considered\nformatDuration(125 * 60_000, { units: [\"minute\"], largest: 1 })\n// \"مئة وخمس وعشرون دقيقةً\"\n```\n\n`largest` (default `2`) caps how many units appear, biggest first. Want to drive\nthe noun agreement yourself? `countedNoun(n, forms)` is exported for any custom\ncounted noun.\n\n---\n\n## File size — Arabic data units\n\n```ts\nimport { formatFileSize } from \"arabicfmt\";\n\nformatFileSize(0)                          // \"0 بايت\"\nformatFileSize(1536)                       // \"1.5 كيلوبايت\"\nformatFileSize(5 * 1024 * 1024)            // \"5 ميجابايت\"\nformatFileSize(1_500_000, { base: 1000 })  // \"1.5 ميجابايت\"  (decimal/SI)\nformatFileSize(2048, { numerals: \"arab\" }) // \"٢ كيلوبايت\"\nformatFileSize(2048, { unitStyle: \"latin\" })// \"2 KB\"\n```\n\nUnits scale through بايت · كيلوبايت · ميجابايت · جيجابايت · تيرابايت · بيتابايت,\nwith `base: 1024` (binary, default) or `base: 1000` (decimal).\n\n---\n\n## Arabic plural rules (6 forms)\n\nArabic has six plural forms — more than any other major language. Standard i18n libraries handle 1–2 forms and break for Arabic.\n\n```ts\nimport { arabicPluralForm, arabicPlural } from \"arabicfmt\";\n\n// Get the CLDR form name\narabicPluralForm(0)    // \"zero\"\narabicPluralForm(1)    // \"one\"\narabicPluralForm(2)    // \"two\"\narabicPluralForm(5)    // \"few\"   (3–10)\narabicPluralForm(15)   // \"many\"  (11–99)\narabicPluralForm(100)  // \"other\"\n\n// Select the right string\nconst forms = {\n  zero:  \"لا كتب\",\n  one:   \"كتاب واحد\",\n  two:   \"كتابان\",\n  few:   \"كتب\",       // 3–10\n  many:  \"كتاباً\",    // 11–99\n  other: \"كتاب\",\n};\n\narabicPlural(0,   forms)  // \"لا كتب\"\narabicPlural(1,   forms)  // \"كتاب واحد\"\narabicPlural(2,   forms)  // \"كتابان\"\narabicPlural(5,   forms)  // \"كتب\"\narabicPlural(25,  forms)  // \"كتاباً\"\narabicPlural(100, forms)  // \"كتاب\"\n```\n\n---\n\n## Hijri / Islamic calendar dates\n\nTwo engines with an identical API:\n\n| | `arabicfmt/date` | `arabicfmt/umalqura` |\n|---|---|---|\n| Algorithm | Tabular arithmetic | Official Umm al-Qura tables |\n| Accuracy | ±1–2 days | Exact |\n| Bundle | Tiny (no tables) | Larger (frozen ICU tables) |\n| Range | Any year | AH 1300–1599 |\n| Deterministic | Yes | Yes — same on Node/Chrome/Safari/Hermes |\n\n```ts\nimport { toHijri, fromHijri, formatHijri, umalquraToGregorian } from \"arabicfmt/umalqura\";\n\n// Convert\ntoHijri(new Date(\"2025-09-23\"))        // { year: 1447, month: 4, day: 1 }\numalquraToGregorian(1447, 9, 1)        // JavaScript Date — first day of Ramadan 1447\n\n// Format — Arabic\nformatHijri(new Date(\"2025-09-23\"))\n// \"1 ربيع الآخر 1447 هـ\"\n\nformatHijri(new Date(\"2025-09-23\"), { numerals: \"arab\" })\n// \"١ ربيع الآخر ١٤٤٧ هـ\"\n\n// Format — English\nformatHijri(new Date(\"2025-09-23\"), { locale: \"en\" })\n// \"1 Rabi al-Thani 1447 AH\"\n\n// Format — ISO-style numeric\nformatHijri(new Date(\"2025-09-23\"), {\n  locale: \"en\", month: \"2-digit\", day: \"2-digit\", order: \"ymd\", era: false,\n})\n// \"1447/04/01\"\n```\n\n### Month and weekday name tables\n\n```ts\nimport {\n  HIJRI_MONTHS_AR,     // Arabic Hijri month names\n  HIJRI_MONTHS_EN,     // English Hijri month names\n  GREGORIAN_MONTHS_AR, // Arabic Gregorian month names (يناير، فبراير…)\n  GREGORIAN_MONTHS_EN,\n  ARABIC_WEEKDAYS_AR,  // Arabic weekday names (الأحد، الاثنين…)\n  ARABIC_WEEKDAYS_EN,\n} from \"arabicfmt/date\";\n\nHIJRI_MONTHS_AR[8]        // \"رمضان\"  (index 0 = Muharram)\nGREGORIAN_MONTHS_AR[0]    // \"يناير\"  (index 0 = January)\nARABIC_WEEKDAYS_AR[5]     // \"الجمعة\" (index 0 = Sunday)\n```\n\n---\n\n## Bidirectional (RTL) text helpers\n\nStop phone numbers and English words from scrambling Arabic sentences:\n\n```ts\nimport { detectDirection, isolate, isolateForeign, stripBidi } from \"arabicfmt/bidi\";\n\n// Before fix: \"+1 (555) 234-5678\" flips the area code in RTL context\n// After fix:  the phone number is wrapped in Unicode isolates — sentence intact\nisolateForeign(\"اتصل على +1 (555) 234-5678 الآن\");\n\ndetectDirection(\"مرحبا\");   // \"rtl\"\ndetectDirection(\"Hello\");   // \"ltr\"\n\nisolate(\"9:41 AM\");         // FSI … PDI isolate around a mixed run\nstripBidi(dirtyStr);        // remove every Unicode bidi control character\n```\n\n---\n\n## Text normalization for Arabic search\n\nMatch Arabic text despite diacritics, alef variants, hamza and taa marbuta differences:\n\n```ts\nimport {\n  stripTashkeel,\n  normalizeArabic,\n  normalizeForSearch,\n  sortArabic,\n  compareArabic,\n} from \"arabicfmt/text\";\n\nstripTashkeel(\"مُحَمَّد\")        // \"محمد\"\nnormalizeArabic(\"الأحمد\")        // \"الاحمد\"  (alef variants unified)\n\n// Robust search — these two strings produce the same key:\nnormalizeForSearch(\"مُؤسَّسة\") === normalizeForSearch(\"موسسه\")  // true\n\n// Arabic-locale collation\nsortArabic([\"ياسر\", \"أحمد\", \"بسام\"])   // [\"أحمد\", \"بسام\", \"ياسر\"]\n[\"ج\", \"أ\", \"ب\"].sort(compareArabic)    // [\"أ\", \"ب\", \"ج\"]\n```\n\n---\n\n## List formatting\n\nJoin values into a grammatical Arabic list. Wraps `Intl.ListFormat` and degrades gracefully on runtimes without it.\n\n```ts\nimport { formatList } from \"arabicfmt\";\n\nformatList([\"أحمد\", \"محمد\", \"علي\"])                      // \"أحمد ومحمد وعلي\"\nformatList([\"تفاح\", \"موز\", \"برتقال\"], { type: \"disjunction\" }) // \"تفاح أو موز أو برتقال\"\nformatList([1, 2, 3], { numerals: \"arab\" })              // \"١ و٢ و٣\"\n```\n\n---\n\n## Transliteration & URL slugs\n\nRomanize 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).\n\n```ts\nimport { transliterate, slugify } from \"arabicfmt\";\n\ntransliterate(\"مُحَمَّد\")    // \"muhammad\"   (vowelled)\ntransliterate(\"محمد\")        // \"mhmd\"       (bare → consonant-only)\ntransliterate(\"الرياض\")      // \"alryad\"\ntransliterate(\"غرفة ٢٠١\")    // \"ghrfh 201\"  (digits converted)\n\nslugify(\"مدينة جدة\")                      // \"mdynh-jdh\"\nslugify(\"الرياض 2026\")                    // \"alryad-2026\"\nslugify(\"Hello العالم\", { separator: \"_\" }) // \"hello_alalm\"\nslugify(\"Hello World\", { lowercase: false }) // \"Hello-World\"\n```\n\n> Note: this is a pragmatic, reversible-ish romanization, not a strict academic\n> transliteration (DIN 31635 / ISO 233). It is built for slugs, search keys and\n> readable IDs.\n\n---\n\n## Validation — IBAN & Saudi ID\n\nReal checksums, not regex guesses. `isValidIBAN` runs the ISO 7064 **mod-97**\nalgorithm with SWIFT-registry length checks; `isValidSaudiId` runs the Luhn\ncheck digit and classifies citizen vs. resident.\n\n```ts\nimport { isValidIBAN, formatIBAN, isValidSaudiId, saudiIdType } from \"arabicfmt\";\n\nisValidIBAN(\"SA03 8000 0000 6080 1016 7519\")  // true\nisValidIBAN(\"SA03 8000 0000 6080 1016 7510\")  // false (bad checksum)\nformatIBAN(\"SA0380000000608010167519\")        // \"SA03 8000 0000 6080 1016 7519\"\n\nisValidSaudiId(\"1012345672\")                  // true\nsaudiIdType(\"1012345672\")                     // \"citizen\"\nsaudiIdType(\"2100000005\")                     // \"resident\"  (Iqama)\n```\n\nRegistry lengths are enforced for SA, AE, KW, BH, QA, JO, LB, EG, IQ, PS, TN, MR,\nLY (plus common partners). Unknown-country IBANs are validated by checksum and\nthe general 15–34 length bound, never accepted on structure alone.\n\n---\n\n## Framework usage\n\n### React / Next.js\n\n```tsx\nimport { formatCurrency, detectLocale } from \"arabicfmt\";\nimport { formatHijri } from \"arabicfmt/umalqura\";\n\nexport function PriceTag({ amount, currency }: { amount: number; currency: string }) {\n  const locale = detectLocale();\n  return (\n    <span dir=\"rtl\">\n      {formatCurrency(amount, { currency, locale })}\n    </span>\n  );\n}\n\nexport function HijriDate({ date }: { date: Date }) {\n  return <time>{formatHijri(date, { numerals: \"arab\" })}</time>;\n}\n```\n\n### Vue 3\n\n```ts\nimport { formatCurrency } from \"arabicfmt\";\n\n// composable\nexport function useArabicCurrency(currency: string) {\n  return (amount: number) =>\n    formatCurrency(amount, { currency, numerals: \"arab\" });\n}\n```\n\n### Node.js / Express\n\n```ts\nimport { formatCurrency, detectLocale } from \"arabicfmt\";\nimport { formatHijri } from \"arabicfmt/umalqura\";\n\napp.get(\"/invoice/:id\", (req, res) => {\n  const locale = req.headers[\"accept-language\"]?.split(\",\")[0] ?? \"ar-SA\";\n  const total  = formatCurrency(order.total, { locale });\n  const date   = formatHijri(order.date, { locale: \"ar\" });\n  res.json({ total, date });\n});\n```\n\n---\n\n## Locale auto-detection\n\n```ts\nimport { detectLocale } from \"arabicfmt\";\n\n// Browser: reads navigator.language\n// Node.js: reads LANG / LANGUAGE / LC_ALL / LC_MESSAGES env vars\n// Fallback: \"ar\"\n\nconst locale = detectLocale(); // \"ar-SA\", \"ar-EG\", \"en-US\", …\nformatCurrency(1234, { locale });\n```\n\n---\n\n## Subpath imports — tree-shakeable\n\nPick only what you need for the smallest possible bundle:\n\n```ts\nimport { formatCurrency, spellCurrency } from \"arabicfmt/currency\";\nimport { formatNumber, arabicToWords, formatDuration, formatFileSize } from \"arabicfmt/number\";\nimport { formatHijri, toHijri }  from \"arabicfmt/date\";       // tabular core (tiny)\nimport { formatHijri, toHijri }  from \"arabicfmt/umalqura\";   // accurate, opt-in\nimport { isolateForeign }        from \"arabicfmt/bidi\";\nimport { normalizeForSearch, arabicPlural, slugify } from \"arabicfmt/text\";\nimport { isValidIBAN, isValidSaudiId }      from \"arabicfmt/validate\";\n```\n\nMeasured cost of each entry point (esbuild `--bundle --minify`, gzipped — v0.1.0):\n\n| Import | What you get | min + gzip |\n|---|---|---|\n| `arabicfmt` | **everything below** | **11.4 kB** |\n| `arabicfmt/currency` | 22 currencies, تفقيط, Unicode transition data | 5.7 kB |\n| `arabicfmt/number` | words, ordinals, fractions, parse, duration, … | 3.5 kB |\n| `arabicfmt/umalqura` | 300 years of official Umm al-Qura tables | 2.2 kB |\n| `arabicfmt/text` | normalize, plurals, collation, lists, slugs | 1.6 kB |\n| `arabicfmt/date` | tabular Hijri core | 1.5 kB |\n| `arabicfmt/bidi` | direction detection + isolates | 0.7 kB |\n| `arabicfmt/validate` | IBAN + Saudi ID checksums | 0.6 kB |\n\nThe complete Arabic formatting stack costs less than a single small image.\n\n---\n\n## Full API reference\n\nEvery public function, by module. Full signatures and options are in the\nsections above and in the bundled TypeScript types.\n\n| Module | Functions |\n|---|---|\n| `arabicfmt/currency` | `formatCurrency` · `spellCurrency` · `getCurrencyInfo` · `resolveCurrencySymbol` |\n| `arabicfmt/number` | `formatNumber` · `formatPercent` · `formatCompact` · `parseNumber` · `parseCurrency` · `toArabicDigits` · `toLatinDigits` · `arabicToWords` · `arabicOrdinal` · `arabicFraction` · `countedNoun` · `formatDuration` · `formatFileSize` · `formatRelativeTime` |\n| `arabicfmt/umalqura` | `formatHijri` · `toHijri` · `fromHijri` · `gregorianToUmalqura` · `umalquraToGregorian` |\n| `arabicfmt/date` | `formatHijri` · `toHijri` · `fromHijri` *(tabular core)* |\n| `arabicfmt/text` | `stripTashkeel` · `removeTatweel` · `normalizeArabic` · `normalizeForSearch` · `arabicPlural` · `arabicPluralForm` · `sortArabic` · `compareArabic` · `createArabicCollator` · `formatList` · `transliterate` · `slugify` |\n| `arabicfmt/bidi` | `isolateForeign` · `isolate` · `wrapLTR` · `wrapRTL` · `stripBidi` · `detectDirection` · `isRTL` · `charDirection` |\n| `arabicfmt/validate` | `isValidIBAN` · `formatIBAN` · `normalizeIBAN` · `isValidSaudiId` · `saudiIdType` |\n| `arabicfmt` *(root)* | re-exports everything above + `detectLocale` |\n\n---\n\n## MCP server — use arabicfmt from AI agents\n\nAI agents (Claude Desktop, Claude Code, Cursor) can call arabicfmt directly through the\n[`arabicfmt-mcp`](https://www.npmjs.com/package/arabicfmt-mcp) Model Context Protocol server —\n17 tools (`format_currency`, `spell_currency`, `format_hijri`, `arabic_to_words`,\n`isolate_foreign`, `validate_iban`, …). Add it to your client's `mcpServers` config:\n\n```json\n{\n  \"mcpServers\": {\n    \"arabicfmt\": { \"command\": \"npx\", \"args\": [\"-y\", \"arabicfmt-mcp\"] }\n  }\n}\n```\n\nSource and full tool list: [`mcp/`](./mcp).\n\n## Examples\n\nRunnable scripts for every feature live in [`examples/`](./examples):\n\n```sh\ncd examples && npm install\nnode currency.mjs   # or numbers / words / dates / text / bidi / validate\n```\n\n---\n\n## Engineering\n\n| | |\n|---|---|\n| **Dependencies** | Zero runtime dependencies |\n| **Size** | ~11.4 kB min+gzip for the whole library; subpath imports from 0.6 kB |\n| **Formats** | Dual ESM + CJS, full `.d.ts` / `.d.cts` types |\n| **Tree-shaking** | `\"sideEffects\": false` — pay only for what you import |\n| **Data source** | CLDR 48.2.0 + ICU — verified at build time, not hand-typed |\n| **Test coverage** | 194 tests — currency transition, precision, Hijri, plurals, words, tafqit, durations, IBAN/ID |\n| **Platforms** | Node ≥ 18, all evergreen browsers, React Native / Hermes, Deno, Bun |\n| **Published with** | npm provenance (GitHub Actions attestation) |\n\n---\n\n## Unicode currency-sign transition\n\n**Live since Unicode 18.0 (September 2026)**\n\nThe UAE dirham (U+20C3) and Omani rial (U+20C4) signs are now **live**, and\n`symbolMode: \"auto\"` prefers them — completing the transition that began with\nthe Saudi riyal sign (U+20C1) in Unicode 17.0. Because system-font coverage for\nbrand-new signs still varies, `symbolMode: \"text\"` always returns the safe\nArabic abbreviation (`د.إ`, `ر.ع.`), and the Saudi riyal keeps the text symbol\nas its `auto` default by design.\n\n| Currency | Sign | Unicode | `auto` default |\n|---|---|---|---|\n| Saudi riyal (SAR) | `⃁` U+20C1 | 17.0 (2025) | text `ر.س` (conservative) |\n| UAE dirham (AED) | `⃃` U+20C3 | 18.0 (2026) | **sign** |\n| Omani rial (OMR) | `⃄` U+20C4 | 18.0 (2026) | **sign** |\n\n---\n\n## Live demo\n\n**[arabicfmt.vercel.app](https://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.\n\n[![arabicfmt interactive playground](https://raw.githubusercontent.com/cc1a2b/arabicfmt/main/assets/playground.png)](https://arabicfmt.vercel.app)\n\nRun it locally:\n\n```sh\ncd demo && npm install && npm run dev\n```\n\n---\n\n## Contributing\n\nIssues and pull requests are welcome on [GitHub](https://github.com/cc1a2b/arabicfmt/issues).\n\n---\n\n## License\n\n[MIT](./LICENSE) — free for commercial and personal use.\n\n---\n\n## Author &amp; more projects\n\nBuilt and maintained by **[cc1a2b](https://github.com/cc1a2b)**.\n\nIf arabicfmt saves you time, please **[star it on GitHub](https://github.com/cc1a2b/arabicfmt)** — it helps other Arabic developers find it. Explore my **[other open-source projects](https://github.com/cc1a2b?tab=repositories)**, or open an [issue](https://github.com/cc1a2b/arabicfmt/issues) with ideas, bugs and feature requests.\n\n<div align=\"center\">\n<sub>Built for Arabic-first software</sub>\n</div>\n",
  "bytes": 31161,
  "sha": "dcddabf336d6db549637fb9621bda937047ca238f6d7965b7d2476e459d3404b",
  "repo_slug": "cc1a2b/arabicfmt",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_cc1a2b_arabicfmt_mcp_0ec33fef/readme"
}