Convert numbers into their written words across multiple locales — zero dependencies, fully typed, tree-shakeable, and shipped as both ESM and CommonJS.
numberToWords(1205, 'en'); // "one thousand two hundred five"
numberToWords(1205, 'fa'); // "یک هزار و دویست و پنج"
numberToWords(25.34, 'en'); // "twenty-five point three four"
await, safe under concurrency."point three four") or fractional ("thirty-four hundredths").npm install number-to-locale-text
# or
pnpm add number-to-locale-text
# or
yarn add number-to-locale-text
Requires Node ≥ 18. Works in any modern bundler (Vite, webpack, Rollup, esbuild) and in the browser.
import { numberToWords } from 'number-to-locale-text';
numberToWords(42, 'en'); // "forty-two"
numberToWords(-42, 'en'); // "minus forty-two"
numberToWords(1000000, 'en'); // "one million"
No setup, no await, no global state — just call it.
numberToWords(value, locale, options?)The primary, stateless converter.
| Param | Type | Description |
|---|---|---|
value |
number \| string |
The number to convert. Pass a string for very large integers or exact decimals (see Precision). |
locale |
LocaleCode \| Locale |
A built-in code ('en', 'fa', …) or a custom locale object. |
options |
ConvertOptions (optional) |
See Options. |
Returns the number in words as a string.
import { numberToWords } from 'number-to-locale-text';
numberToWords(138, 'en'); // "one hundred thirty-eight"
numberToWords(25.34, 'en'); // "twenty-five point three four"
numberToWords(25.34, 'en', { decimals: 'fraction' }); // "twenty-five and thirty-four hundredths"
createConverter(locale, defaults?)Returns a reusable converter bound to a locale (and optional default options). Ideal when you convert many values for the same locale.
import { createConverter } from 'number-to-locale-text';
const fa = createConverter('fa');
fa.convert(25); // "بیست و پنج"
fa.convert(1205); // "یک هزار و دویست و پنج"
// Bind default options too:
const money = createConverter('en', { decimals: 'fraction' });
money.convert(25.34); // "twenty-five and thirty-four hundredths"
money.convert(25.34, { decimals: 'digits' }); // per-call override → "twenty-five point three four"
money.locale; // the resolved Locale object
interface ConvertOptions {
/** How the fractional part is read. Default: 'digits'. */
decimals?: 'digits' | 'fraction';
}
decimals |
25.34 → |
|---|---|
'digits' |
twenty-five point three four |
'fraction' |
twenty-five and thirty-four hundredths |
| Code | Language | Quality |
|---|---|---|
en |
English | ✅ Fully idiomatic |
fa |
Persian | ✅ Fully idiomatic |
tr |
Turkish | ✅ Idiomatic (except 1000 reads as bir bin) |
ar |
Arabic | ⚠️ Approximate — no gender/dual agreement |
fr |
French | ⚠️ Approximate — no vigesimal 70/80/90 rules |
de |
German | ⚠️ Approximate — not compounded (zwanzig fünf, not fünfundzwanzig) |
es |
Spanish | ⚠️ Approximate — no veinti- contractions |
The built-in engine is positional: it composes ones/tens/hundreds/scales with per-locale connectors. This is fully correct for English and Persian and intelligible everywhere, but it does not model language-specific morphology (German compounding, French vigesimal counting, Spanish/Arabic agreement). Contributions implementing per-language rules are very welcome.
Codes are also available as a typed constant (back-compatible with the v1 enum):
import { Locales, locales } from 'number-to-locale-text';
Locales.EN; // 'en'
locales.en; // the English Locale object
numberToWords(25, 'en'); // "twenty-five"
numberToWords(25, 'fa'); // "بیست و پنج"
numberToWords(25, 'tr'); // "yirmi beş"
numberToWords(25, 'ar'); // "عشرون و خمسة" (approximate)
numberToWords(25, 'fr'); // "vingt-cinq"
numberToWords(25, 'de'); // "zwanzig fünf" (approximate)
numberToWords(25, 'es'); // "veinte y cinco" (approximate)
Pass a Locale object anywhere a code is accepted. Use defineLocale for type-checking and autocompletion:
import { defineLocale, numberToWords } from 'number-to-locale-text';
const pirate = defineLocale({
zero: 'naught',
negative: 'minus',
ones: ['', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine'],
teens: ['ten', 'eleven', 'twelve', 'thirteen', 'fourteen', 'fifteen', 'sixteen', 'seventeen', 'eighteen', 'nineteen'],
tens: ['', '', 'twenty', 'thirty', 'forty', 'fifty', 'sixty', 'seventy', 'eighty', 'ninety'],
hundreds: ['', 'one hundred', 'two hundred', 'three hundred', 'four hundred', 'five hundred', 'six hundred', 'seven hundred', 'eight hundred', 'nine hundred'],
scales: ['', 'thousand', 'million', 'billion'],
decimalSuffixes: ['tenth', 'hundredth', 'thousandth'],
connectors: {
tensAndOnes: '-',
hundredAndRest: ' ',
scale: ' ',
group: ' ',
negative: ' ',
decimalPoint: 'point',
fraction: ' and ',
},
});
numberToWords(1234, pirate); // "one thousand two hundred thirty-four"
Locale schemainterface Locale {
zero: string; // "zero"
negative: string; // "minus" (no trailing space)
ones: string[]; // index 0 = ""; 1..9 = one..nine
teens: string[]; // 0..9 = ten..nineteen
tens: string[]; // index 0,1 = ""; 2..9 = twenty..ninety
hundreds: string[]; // index 0 = ""; 1..9 = one hundred..nine hundred
scales: string[]; // index 0 = ""; then thousand, million, …
decimalSuffixes: string[]; // for 'fraction' mode: tenths, hundredths, …
decimalSuffixesPlural?: string[]; // optional plurals (used when numerator ≠ 1)
connectors: LocaleConnectors;
}
interface LocaleConnectors {
tensAndOnes: string; // tens ↔ ones — EN "-" FA " و "
hundredAndRest: string; // hundreds ↔ rest — EN " " BrE " and "
scale: string; // group ↔ scale — EN " " ("one thousand")
group: string; // chunk ↔ chunk — EN " " FA " و "
negative: string; // negative ↔ body — usually " "
decimalPoint: string; // spoken "point" — EN "point" FR "virgule"
fraction: string; // integer ↔ fraction in 'fraction' mode — EN " and "
}
Each extra entry in scales extends the supported magnitude by three orders of ten.
JavaScript’s number type loses integer precision above 2⁵³ and represents big values in exponential notation. To convert beyond that range exactly, pass a string:
numberToWords('1000000000000000', 'en');
// "one quadrillion"
numberToWords(1e21, 'en');
// ❌ RangeError: exponential notation — pass it as a string instead
Built-in locales support magnitudes up to their largest scales entry (English reaches quintillion, i.e. up to 10²¹ − 1). Beyond that a RangeError is thrown — extend the locale’s scales array to go further.
The library fails loudly instead of returning a wrong answer:
numberToWords('abc', 'en'); // TypeError: Invalid numeric input: "abc".
numberToWords(Infinity, 'en'); // RangeError: Cannot convert a non-finite number.
numberToWords(5, 'xx'); // RangeError: Unknown locale "xx".
The original stateful API still works for backwards compatibility, but is deprecated — it relies on shared mutable state, which is unsafe under concurrency. Prefer numberToWords / createConverter.
import { setDefaultLocale, numberToString } from 'number-to-locale-text';
setDefaultLocale('en'); // now synchronous (awaiting it is harmless)
numberToString(1205); // "one thousand two hundred five"
Migrating from v1:
setDefaultLocaleis now synchronous (drop theawait), and output strings are corrected — v1 produced malformed results such as"twenty and five"and crashed on 4+ digit decimals and very large integers. ReplacesetDefaultLocale(x)+numberToString(n)withnumberToWords(n, x).
Contributions are welcome — especially:
src/locales/, register it in src/locales/index.ts).npm install
npm run build # tsup → dist (ESM + CJS + .d.ts)
npm run typecheck # tsc --noEmit
MIT © dev.zarghami