DEV Community

Kirnu (كرنو)
Kirnu (كرنو)

Posted on AI-assisted

Converting numbers to Arabic words in JavaScript is harder than you think

In English, turning a number into words is close to a lookup table: 15 is fifteen, and 15 dollars is fifteen dollars. In Arabic, the same number is written differently depending on the noun that follows it and on its role in the sentence. Fifteen Saudi riyals is خمسة عشر ريالاً (khamsata ʿashara riyālan), while fifteen halalas (the Saudi riyal's fractional unit) is خمس عشرة هللة (khamsa ʿashrata halalatan): both words of "fifteen" change. That's why most quick number-to-words implementations for Arabic produce text a native speaker immediately spots as wrong.

Writing amounts in words isn't a niche need in the Arab world: cheques, invoices and contracts in Saudi Arabia, the UAE, Egypt and elsewhere write the amount in words, and on a cheque the words usually take precedence over the digits. The practice even has its own name, tafgeet (تفقيط).

This post walks through the rules an Arabic number-to-words engine has to model and what each one means for your code. All examples were generated by @kirnu/arabic-core, an open-source TypeScript library I work on that powers the tools on Kirnu, a free Arabic tools site. The Arabic examples use Modern Standard Arabic; regional spoken Arabic can differ.

Why a lookup table can't work

The correct output depends on information that isn't in the number:

  • The gender of the counted noun. "Three books" and "three pages" use different words for three.
  • Grammatical case. "Twelve" changes form when it's the object of a verb or follows a preposition.
  • The number's range. 3–10, 11–19, the tens and the hundreds each follow different rules.
  • The form of the noun. After 3–10 the noun is plural; after 11–99 it's singular; after 100 it's singular again but in a different case.
  • The currency. Each currency has its own main and fractional unit, each with its own gender, and they don't all have 100 subunits.
  • Fractions and, when needed, cheque format.

So an Arabic numberToWords(n) needs more inputs than n (at least gender and case), and converting amounts needs data about each currency. The library used here covers seven: the Saudi, Qatari and Omani riyal, the UAE dirham, the Egyptian pound, and the Kuwaiti and Bahraini dinar.

Let's go through the rules one at a time.

1. Numbers 3–10 take the opposite gender

The rule people forget most: from three to ten, the number takes the opposite gender of the noun. With a masculine noun it gets the feminine-looking ending -a (ة); with a feminine noun it drops it.

Number With a masculine noun (book) With a feminine noun (page)
3 ثلاثة thalātha ثلاث thalāth
5 خمسة khamsa خمس khams
8 ثمانية thamāniya ثماني thamānī

One and two do the opposite and agree with the noun: one book is كتاب واحد, one page is ورقة واحدة. Consequence for the API: your function needs to know the noun's gender, and a sensible default is masculine.

2. 11–19: two words, two rules

Compound numbers combine both behaviours. In fifteen, the "five" part follows the 3–10 rule (opposite gender) while the "ten" part agrees with the noun:

  • masculine: خمسة عشر khamsata ʿashara
  • feminine: خمس عشرة khamsa ʿashrata

So a single gender flag flips the two words in opposite directions, which means you can't generate each part independently. Eleven and twelve have their own forms: أحد عشر / إحدى عشرة for 11, and اثنا عشر / اثنتا عشرة for 12.

3. Grammatical case changes the word itself

"Twelve" is اثنا عشر in the nominative but اثني عشر in the accusative and genitive. The same goes for "two thousand" (ألفان → ألفين) and "twenty-two" (اثنان وعشرون → اثنين وعشرين). In "I received twelve orders", the nominative form would be a mistake. That's the second input your function needs: case, with nominative as the safe default when the number stands alone.

So far we've only produced the number. Real-world uses like invoices and cheques have a noun after it, and the noun has rules of its own.

4. The noun changes with the number

Even with the number right, the noun (say, Saudi riyal) changes across four ranges:

Range Noun form Example
1 and 2 singular / dual, number after it or omitted ريال سعودي واحد, ريالان سعوديان
3–10 plural ثلاثة ريالات سعودية
11–99 singular, accusative خمسة عشر ريالاً سعودياً
100+ singular, genitive مئة ريال سعودي, ألفا ريال سعودي

The adjective ("Saudi") follows the noun's form too. And the range is decided by the last two digits, not the whole number: 1250 ends in fifty, so the noun is singular accusative, giving ألف ومئتان وخمسون ريالاً سعودياً. Your code has to split the number into groups before it can pick the noun form.

5. Hundreds, thousands and spelling

  • 200 is مئتان (a dual), 300 is ثلاثمئة, written as one word in modern spelling.
  • Thousands follow the counted-noun rules: ألفان (2,000), ثلاثة آلاف (3,000), أحد عشر ألفاً (11,000).
  • "Hundred" has a modern spelling (مئة) and a classic one (مائة). Both are in use, so make it an option: 1250 is ألف ومئتان وخمسون or ألف ومائتان وخمسون.
  • Parts are joined with wa ("and"): 1,001,011 is مليون وألف وأحد عشر.

6. Currencies and fractions

For amounts, every rule above applies twice: once to the main unit and once to the subunit, including the subunit's gender.

  • Saudi riyal: 100 halalas, and halala is feminine, so 0.25 is خمس وعشرون هللة, not the masculine خمسة وعشرون.
  • Kuwaiti dinar: 1,000 fils (three decimal places), so 1.250 is دينار كويتي واحد ومئتان وخمسون فلساً.
  • Egyptian pound: 100 piastres, so 3.03 is ثلاثة جنيهات مصرية وثلاثة قروش.

Plain fractions are read after the word fāṣila ("point"): 12.5 is اثنا عشر فاصلة خمسة. And cheques wrap the amount in فقط … لا غير ("only … nothing more") so nothing can be added before or after it:

فقط ألف ومئتان وخمسون درهماً إماراتياً وخمسون فلساً لا غير

7. Write the tests before the code

With this many interacting rules, a fix for one case easily breaks ten others. What worked for us: a table of hand-written cases reviewed for grammar (number, gender, case, expected output) that runs on every change, plus dedicated tests for the edge cases: 11 and 12, numbers ending in 01 and 02, dual thousands, zero, and Eastern Arabic digits (١٢٣) alongside Western ones (123).

Using @kirnu/arabic-core in JavaScript and TypeScript

All of the above is packaged as an MIT-licensed TypeScript library with no dependencies. It runs in the browser and in Node:

npm install @kirnu/arabic-core
Enter fullscreen mode Exit fullscreen mode
import { tafgeet, currencyToWords } from '@kirnu/arabic-core';

// masculine noun (the default)
tafgeet(15);                                   // خمسة عشر

// feminine noun
tafgeet(15, { gender: 'feminine' });           // خمس عشرة

// grammatical case
tafgeet(12, { case: 'accusative' });           // اثني عشر

// Eastern Arabic digits with a decimal separator
tafgeet('١٢٫٥');                               // اثنا عشر فاصلة خمسة

// an amount in cheque format
currencyToWords('1250.50', 'SAR', { cheque: true });
// فقط ألف ومئتان وخمسون ريالاً سعودياً وخمسون هللة لا غير
Enter fullscreen mode Exit fullscreen mode

Besides the seven currencies, it also removes Arabic diacritics (tashkeel), normalises Arabic text, and converts between Hijri (Umm al-Qura) and Gregorian dates.

If you find a case it gets wrong, or need another currency, please open an issue. Corrections from Arabic speakers are just as welcome.

Top comments (0)