tafgeet-arabic
v1.4.0
Published
Converts currency digits into written Arabic words
Maintainers
Readme
tafgeet-arabic
Convert numeric currency amounts into written Arabic words — for invoices, receipts, contracts, and legal documents.
new Tafgeet('1234.56', 'EGP').parse();
// → ألف ومائتان وأربعة وثلاثون جنيه مصري وستة وخمسون قرش فقط لا غير- ⚡ ~900,000 ops/sec — pure JS, zero runtime dependencies
- ✅ Grammatically correct Arabic — full gender agreement & noun forms (details)
- 📦 Dual ESM + CJS build, ships TypeScript
.d.tsfiles,sideEffects: false - 🛡 Typed error classes (
InvalidAmountError,AmountOutOfRangeError, …) with discriminatedcodefields - 🔢 Accepts Latin, Arabic-Indic (
١٢٣), Eastern Arabic-Indic (۱۲۳), commas, spaces, underscores - ➗ Configurable rounding (
'round','floor','ceil','bankers','truncate') - 🌍 24 currencies covering the Arab region plus major international codes (see the full table)
Requirements
- Node.js ≥ 18.18.0
Install
npm install tafgeet-arabicUsage
Quick start
// ES Module / TypeScript
import { Tafgeet } from 'tafgeet-arabic';
// CommonJS
const { Tafgeet } = require('tafgeet-arabic');
new Tafgeet('1234.56').parse();
// → 'ألف ومائتان وأربعة وثلاثون جنيه مصري وستة وخمسون قرش فقط لا غير'Tip: prefer passing the amount as a string (
'1234.56') over a JS number. JavaScript'sNumberloses precision above 253 and can mis-render trailing decimals (1.1 + 0.2is1.3000000000000003, not1.3). The package accepts both, but strings round-trip cleanly.
Multiple currencies
new Tafgeet('500', 'SAR').parse();
// → 'خمسمائة ريال سعودي فقط لا غير'
new Tafgeet('1.005', 'KWD').parse(); // Kuwaiti Dinar uses 3 decimals
// → 'دينار كويتي واحد وخمسة فلوس فقط لا غير'
new Tafgeet('1234567', 'USD').parse();
// → 'مليون ومائتان وأربعة وثلاثون ألف وخمسمائة وسبعة وستون دولار أمريكي فقط لا غير'No-currency mode
Pass an empty string to render the number alone, without a currency suffix:
new Tafgeet('7564654', '').parse();
// → 'سبعة ملايين وخمسمائة وأربعة وستون ألف وستمائة وأربعة وخمسون فقط لا غير'Flexible input formats
Beyond plain Latin ('1234.56'), the constructor accepts amounts in many forms — useful for input from Arabic forms, CSVs, copy-pasted spreadsheets, etc.:
new Tafgeet('١٢٣٤.٥٦').parse(); // Arabic-Indic digits
new Tafgeet('۱۲۳۴.۵۶').parse(); // Eastern Arabic-Indic (Farsi/Urdu)
new Tafgeet('1,234.56').parse(); // ASCII comma
new Tafgeet('1_234_567').parse(); // JS underscore separator
new Tafgeet('1 234 567').parse(); // space (regular / NBSP / narrow NBSP)
new Tafgeet('١٬٥٠٠٫٢٥', 'EGP').parse(); // fully Arabic with Arabic separators
// All produce the same output as the plain-Latin equivalent.Rounding
By default decimals beyond the currency's precision are truncated (preserving v1.1 behavior byte-for-byte). Opt in to other modes via the third constructor argument:
new Tafgeet('1.995', 'EGP', { rounding: 'round' }).parse();
// → 'جنيهان مصريان فقط لا غير' (1.995 rounded to 2.00 → dual noun)
new Tafgeet('1.001', 'EGP', { rounding: 'ceil' }).parse();
// → 'جنيه مصري واحد وقرش واحد فقط لا غير'
new Tafgeet('1.225', 'EGP', { rounding: 'bankers' }).parse();
// → 'جنيه مصري واحد واثنان وعشرون قرش فقط لا غير' (half-to-even)Available modes: 'truncate' (default), 'round' (half-up), 'floor', 'ceil', 'bankers' (IEEE 754 half-to-even — recommended for accounting).
Error handling
Invalid input throws a typed error rather than producing garbage output:
try {
new Tafgeet(-100, 'EGP');
} catch (err) {
// err instanceof RangeError
// err.message === 'Tafgeet: amount must be non-negative, got -100'
}
try {
new Tafgeet(1, 'XYZ');
} catch (err) {
// err instanceof Error
// err.message === 'Tafgeet: unknown currency "XYZ". Supported: SDG, SAR, QAR, AED, EGP, KWD, USD, AUD, TND, TRY, BHD, OMR, JOD, IQD, LYD, LBP, MAD, DZD, SYP, YER, EUR, GBP, CHF, CAD'
}See API → Errors for the full list.
TypeScript
All public types are re-exported from the package entry point:
import {
Tafgeet,
SUPPORTED_CURRENCIES,
isTafgeetError,
InvalidAmountError,
AmountOutOfRangeError,
UnsupportedCurrencyError,
} from 'tafgeet-arabic';
import type {
Currency,
Currencies,
CurrencyCode,
CurrencyInput,
NumberProperties,
RoundingMode,
TafgeetOptions,
TafgeetErrorCode,
} from 'tafgeet-arabic';API
new Tafgeet(amount, currency?, options?)
Creates a new Tafgeet instance. The constructor validates the input
eagerly and throws a typed error if anything is malformed.
| Parameter | Type | Default | Notes |
|---|---|---|---|
| amount | string \| number | — | Integer part must be ≥ 1 and ≤ 15 digits. Pass a string to avoid float precision loss. Arabic-Indic digits and thousands separators are accepted. |
| currency | CurrencyInput | 'EGP' | A CurrencyCode, an empty string for no-currency mode, or any string (runtime validated). |
| options | TafgeetOptions | {} | See Options below. |
Options
interface TafgeetOptions {
rounding?: 'truncate' | 'round' | 'floor' | 'ceil' | 'bankers';
}| Option | Default | Behavior |
|---|---|---|
| rounding | 'truncate' | How to handle decimals beyond the currency's natural precision. 'truncate' preserves v1.1 behavior exactly. 'round' is half-up. 'bankers' is IEEE 754 half-to-even (recommended for accounting). Rounding can carry into the integer part (1.995 EGP with 'round' → 2 EGP). |
.parse(): string
Renders the full amount as Arabic words, including the currency suffix
and the closing فقط لا غير.
new Tafgeet('123.45', 'EGP').parse();
// → 'مائة وثلاثة وعشرون جنيه مصري وخمسة وأربعون قرش فقط لا غير'
new Tafgeet('1.995', 'EGP', { rounding: 'round' }).parse();
// → 'جنيهان مصريان فقط لا غير' (rounded up to 2 → dual noun).read(d: number): string
Renders a number in 0–999 as Arabic words (masculine), with no column
suffix and no currency. Useful for custom formatting.
const t = new Tafgeet('1'); // any instance
t.read(42); // → 'اثنان وأربعون'
t.read(100); // → 'مائة'
t.read(999); // → 'تسعمائة وتسعة وتسعون'Errors
Each error class extends the appropriate built-in, so existing
instanceof TypeError / RangeError checks keep working. The
code field allows structured handling:
| Class | Extends | code | When |
|---|---|---|---|
| InvalidAmountError | TypeError | 'INVALID_AMOUNT' | amount is null, undefined, wrong type, non-numeric string, empty string, or scientific notation. Also thrown when currency is not a string. |
| AmountOutOfRangeError | RangeError | 'AMOUNT_OUT_OF_RANGE' | amount is NaN, Infinity, negative, zero (integer part < 1), or has more than 15 integer digits. |
| UnsupportedCurrencyError | Error | 'UNSUPPORTED_CURRENCY' | currency is a string but not one of the codes in SUPPORTED_CURRENCIES. |
import { isTafgeetError } from 'tafgeet-arabic';
try {
new Tafgeet(input, currency).parse();
} catch (e) {
if (isTafgeetError(e)) {
switch (e.code) {
case 'INVALID_AMOUNT': reportToUser('Bad number'); break;
case 'AMOUNT_OUT_OF_RANGE': reportToUser('Out of range'); break;
case 'UNSUPPORTED_CURRENCY': reportToUser('Bad currency'); break;
}
} else {
throw e;
}
}Arabic grammar
Since v1.4 the output is grammatically correct Modern Standard Arabic (فصحى), undiacritized (no tashkīl). The number agrees with the noun it counts — the currency unit for the integer part, the fractional unit for the fraction — and each built-in currency carries its own gender, so you never configure it.
| Count | Rule | Masculine (جنيه) | Feminine (ليرة) |
|---|---|---|---|
| 1 | noun + agreeing adjective | جنيه مصري واحد | ليرة تركية واحدة |
| 2 | the dual noun (no number word) | جنيهان مصريان | ليرتان تركيتان |
| 3–10 | number opposes gender + plural noun | ثلاثة جنيهات | ثلاث ليرات |
| 11–99 | number + singular noun | أحد عشر جنيه | إحدى عشرة ليرة |
| 100/1000+ | scale word + singular noun | مائة جنيه، ألف جنيه | مائة ليرة |
The noun form follows the number that directly governs it, so
103 → مائة وثلاثة جنيهات (plural) and 120 → مائة وعشرون جنيه (singular).
Duals are nominative (اثنان، مائتان، ألفان) and drop their nūn when مضاف
to the counted noun (مائتا جنيه، ألفا جنيه، مليونا جنيه). No-currency mode
renders the bare number in the masculine.
new Tafgeet('3', 'EGP').parse(); // → 'ثلاثة جنيهات مصرية فقط لا غير' (masc)
new Tafgeet('3', 'TRY').parse(); // → 'ثلاث ليرات تركية فقط لا غير' (fem)
new Tafgeet('2', 'EGP').parse(); // → 'جنيهان مصريان فقط لا غير' (dual)A native-speaker review settled these forms. If you spot a regional or stylistic preference that differs, please open an issue.
Common pitfalls
Things that surprise users — most are guarded by typed errors since v1.2.1, but worth knowing up front.
Pass strings for amounts you computed
JavaScript floats lose precision above 2⁵³ and produce surprises like 0.1 + 0.2 = 0.30000000000000004. The package documents string | number input, but strings round-trip cleanly:
// Risky — arithmetic results may render unexpectedly:
const total = subtotal + tax;
new Tafgeet(total, 'EGP').parse();
// Safe — string preserves your computed precision:
new Tafgeet(total.toFixed(2), 'EGP').parse();Amounts must be ≥ 1
new Tafgeet('0', 'EGP') and new Tafgeet('0.50', 'EGP') both throw AmountOutOfRangeError. The Arabic dictionary has no word for zero and the column logic assumes at least one integer digit. If you compute an amount that might round below 1:
const a = Math.max(1, Math.ceil(amount));
new Tafgeet(a.toString(), 'EGP').parse();Very large or very small numbers must be strings
Numbers ≥ 10²¹ and < 10⁻⁶ stringify in scientific notation ('1e+21'), which the package rejects since v1.2.1 (it used to silently corrupt them). Pass them as strings:
// Throws AmountOutOfRangeError:
new Tafgeet(1e21, 'EGP');
// Works:
new Tafgeet('1000000000000000000000', 'EGP');No-currency mode is for integers only
new Tafgeet('1.50', '') throws since v1.2.1 (silently dropped the cents pre-1.2.1). Either pass an integer or specify a currency:
new Tafgeet('150', '').parse(); // ok — pure integer
new Tafgeet('1.50', 'EGP').parse(); // ok — with currency
new Tafgeet('1.50', ''); // throws — ambiguous
new Tafgeet('1.0', '').parse(); // ok — all-zero fractionread() validates strictly
Tafgeet.prototype.read(d) throws on anything outside 0..999 since v1.2.1 (used to silently return ''). Always pass a finite integer in range.
Strict options validation
Unknown option keys throw — including typos like { Rounding: 'round' }. Stick to the documented TafgeetOptions shape; the v1.2.1 strict mode catches typos at runtime that TypeScript catches at compile time.
Supported currencies
24 currencies, sorted alphabetically by ISO code (matches SUPPORTED_CURRENCIES).
| Code | Currency | Decimals |
|---|---|---|
| AED | Emarati Dirham — درهم أماراتي | 2 |
| AUD | Australian Dollar — دولار أسترالي | 2 |
| BHD | Bahraini Dinar — دينار بحريني | 3 |
| CAD | Canadian Dollar — دولار كندي | 2 |
| CHF | Swiss Franc — فرنك سويسري | 2 |
| DZD | Algerian Dinar — دينار جزائري | 2 |
| EGP (default) | Egyptian Pound — جنيه مصري | 2 |
| EUR | Euro — يورو | 2 |
| GBP | British Pound Sterling — جنيه إسترليني | 2 |
| IQD | Iraqi Dinar — دينار عراقي | 3 |
| JOD | Jordanian Dinar — دينار أردني | 3 |
| KWD | Kuwaiti Dinar — دينار كويتي | 3 |
| LBP | Lebanese Pound — ليرة لبنانية | 2 |
| LYD | Libyan Dinar — دينار ليبي | 3 |
| MAD | Moroccan Dirham — درهم مغربي | 2 |
| OMR | Omani Rial — ريال عماني | 3 |
| QAR | Qatari Riyal — ريال قطري | 2 |
| SAR | Saudi Riyal — ريال سعودي | 2 |
| SDG | Sudanese Pound — جنيه سوداني | 2 |
| SYP | Syrian Pound — ليرة سورية | 2 |
| TND | Tunisian Dinar — دينار تونسي | 3 |
| TRY | Turkish Lira — ليرة تركية | 2 |
| USD | US Dollar — دولار أمريكي | 2 |
| YER | Yemeni Rial — ريال يمني | 2 |
Missing a currency? Open an issue or send a PR.
Changelog
See CHANGELOG.md.
v1.4.0 (latest) — a native-speaker grammar pass. Output is now grammatically correct فصحى with full gender agreement (e.g. ثلاث ليرات for the feminine lira, ثلاثة جنيهات for the masculine pound), correct dual/singular/plural noun selection, and the classical forms for 1 (جنيه مصري واحد) and 2 (جنيهان مصريان). Spelling fixes (اثنان، اثنا عشر، آلاف، إماراتي). This deliberately changes output for many inputs — see the CHANGELOG. No new options; gender is derived from the currency.
v1.3.0 — 14 new bundled currencies bring the total to 24: 10 Arab-region (BHD, OMR, JOD, IQD, LYD, LBP, MAD, DZD, SYP, YER) and 4 major international (EUR, GBP, CHF, CAD) for cross-border invoicing. No new APIs, no breaking changes.
v1.2.1 — eight correctness and hardening fixes found across two audit passes. Most notably: '1.5' EGP now correctly renders 50 piasters (not 5); Number.MAX_VALUE and other exponential-notation numbers now throw instead of silently corrupting; rounding can now carry from 0.999 up to 1; strict options validation catches typos. See the v1.2.1 entry in the CHANGELOG for the full list. No new APIs.
v1.2.0 — dual ESM/CJS build, CurrencyCode union type, Arabic-Indic & thousands-separator input, typed error classes, rounding option, 262 snapshot tests.
v1.1.0 — 3.5–4.7× faster, 52% smaller install, fixes for issues #7 / #8 / fraction-plural, KWD added, input validation.
Roadmap
- Optional diacritized / accusative output mode (tashkīl + case endings, e.g.
جنيهًا) — v1.4 ships undiacritized by design - Optional legal mode for unambiguous cheque/contract wording
- Optional: functional API
tafgeet(amount, currency?)alongside the class
Contributing
PRs and issues welcome. To set up locally:
git clone https://github.com/omar-ehab/tafgeet-arabic
cd tafgeet-arabic
npm install
npm test # 1064 tests (726 snapshot + 338 unit/feature/regression)
npm run lint # ESLint
npm run test:js-compat # CJS + ESM smoke tests against built dist/License
MIT © Omar Ehab
