Convert numbers and currency amounts into words across 137 locales — production-ready BigInt, ordinal, and TypeScript support.
import { toWords } from 'to-words';
toWords(12345, { localeCode: 'en-US' });
// "Twelve Thousand Three Hundred Forty Five"
toWords(452.36, { localeCode: 'en-IN', currency: true });
// "Four Hundred Fifty Two Rupees And Thirty Six Paise Only"Try the interactive demo → — currency, ordinals, gender, and large numbers, live in the browser. The documentation site has a guide for every feature and a page for every language.
Need the reverse conversion too? Use the companion package to-numbers to parse spelled-out numbers back into numeric values across the same locales.
- Features
- Installation
- Quick Start
- Usage — currency, ordinals, gender, decimals, letter case, large numbers, locale detection, custom locales
- CLI
- Framework Integration
- Numbering Systems
- API Reference
- Error Handling
- Supported Locales
- Performance & Compatibility
- Migrating From Other Libraries
- FAQ
- Contributing
- 137 Locales — 72 languages with their regional currencies, from English, Hindi and Arabic to Georgian, Yoruba and Khmer
- Verified Output — Every locale's cardinals, ordinals, currency and decimals are checked against independent references: CLDR number rules, dictionaries, language-authority guidance and real-world usage
- Currency — Locale-specific currency and fractional units, custom currencies, and 3-decimal currencies (KWD, OMR, BHD)
- Ordinals — First, Second, Third, and their equivalents in every locale
- Grammar-Aware — Grammatical gender, plural forms of scale and currency words, and language-specific connectors
- Numbering Systems — Short scale, long scale, Indian (lakh, crore) and East Asian (万, 億)
- Large Numbers — Exact BigInt and string input beyond
Number.MAX_SAFE_INTEGER, with a verified range per locale - Legal Decimals — "Forty Five Hundredths" style via
decimalStyle: 'fraction'in 101 locales - Formal Chinese Numerals — Financial characters (大写/大寫) via
formal: true - Small Per-Locale Bundles — Import only the locale you need; ESM, CommonJS and UMD builds, TypeScript types, zero dependencies
- Locale Detection —
detectLocale()andresolveLocale()map browser, runtime or request locales to a supported locale - CLI —
npx to-words 12345 --locale en-US
Common uses: amounts in words on invoices, cheques, receipts, payslips and contracts; order totals in e-commerce; accessible, screen-reader friendly numbers; text-to-speech; and language-learning apps.
npm install to-words
# or
yarn add to-words
# or
pnpm add to-wordsRequires Node.js 20 or newer, or any modern browser. For a <script> tag, use the UMD bundles from a CDN:
<!-- Single locale (recommended) -->
<script src="https://cdn.jsdelivr.net/npm/to-words/dist/umd/en-US.min.js"></script>
<script>
const toWords = new ToWords(); // pre-configured for en-US
toWords.convert(12345); // "Twelve Thousand Three Hundred Forty Five"
</script>
<!-- All locales -->
<script src="https://cdn.jsdelivr.net/npm/to-words/dist/umd/to-words.min.js"></script>
<script>
const toWords = new ToWords({ localeCode: 'fr-FR' });
toWords.convert(12345); // "Douze Mille Trois Cent Quarante-Cinq"
</script>There are three ways to use to-words:
1. Class-based — reuse one instance for many conversions:
import { ToWords } from 'to-words';
const tw = new ToWords({ localeCode: 'en-US' });
tw.convert(12345); // "Twelve Thousand Three Hundred Forty Five"
tw.convert(100, { currency: true }); // "One Hundred Dollars Only"
tw.toOrdinal(3); // "Third"2. Functional, all locales — one-liners with a localeCode option:
import { toWords, toOrdinal, toCurrency } from 'to-words';
toWords(12345, { localeCode: 'en-US' }); // "Twelve Thousand Three Hundred Forty Five"
toCurrency(100, { localeCode: 'en-US' }); // "One Hundred Dollars Only"
toOrdinal(3, { localeCode: 'en-US' }); // "Third"3. Functional, one locale — the locale is built in, and the bundle is about a tenth of the size:
import { toWords, toOrdinal, toCurrency } from 'to-words/en-US';
toWords(12345); // "Twelve Thousand Three Hundred Forty Five"
toCurrency(100); // "One Hundred Dollars Only"
toOrdinal(3); // "Third"Every locale entry point (to-words/<locale>) also exports a pre-configured ToWords class (new ToWords()), and CommonJS works the same way (const { ToWords } = require('to-words')).
| Situation | Import | Gzip size |
|---|---|---|
| Browser app with a fixed locale | to-words/<locale>, or its UMD bundle |
~8.8 KiB |
| Locale chosen at runtime (per user, per request) | to-words, passing localeCode |
~89.7 KiB |
| Server code (Node.js, SSR, APIs) | to-words — bundle size rarely matters |
— |
Sizes are for the minified UMD builds of 7.2.0 (en-US for a single locale); the build enforces size budgets, and npm run size:check prints the current figures.
Default locale: when no
localeCodeis given, the runtime locale is auto-detected (see Locale Detection), falling back toen-INif it cannot be matched.
The examples use the class API; every option works the same with toWords(), toCurrency() and toOrdinal().
const tw = new ToWords({ localeCode: 'en-US' });
tw.convert(123); // "One Hundred Twenty Three"
tw.convert(-5); // "Minus Five"
tw.convert(123.45); // "One Hundred Twenty Three Point Forty Five"
tw.convert('1000'); // "One Thousand" (numeric strings are accepted)const tw = new ToWords({ localeCode: 'en-IN' });
tw.convert(452.36, { currency: true });
// "Four Hundred Fifty Two Rupees And Thirty Six Paise Only"
// Without the "Only" suffix
tw.convert(452, { currency: true, doNotAddOnly: true });
// "Four Hundred Fifty Two Rupees"
// Ignore the fractional part
tw.convert(452.36, { currency: true, ignoreDecimal: true });
// "Four Hundred Fifty Two Rupees Only"
// Skip a zero main amount
tw.convert(0.36, { currency: true, ignoreZeroCurrency: true });
// "Thirty Six Paise Only"
// Show a zero fractional part (string input keeps the .00)
tw.convert('452.00', { currency: true, includeZeroFractional: true });
// "Four Hundred Fifty Two Rupees And Zero Paise Only"toCurrency(n, options) is shorthand for toWords(n, { currency: true, ...options }).
Custom currency — keep the locale's language and replace its currency:
const tw = new ToWords({ localeCode: 'en-US' });
tw.convert(100.5, {
currency: true,
currencyOptions: {
name: 'Euro',
plural: 'Euros',
singular: 'Euro',
symbol: '€',
fractionalUnit: { name: 'Cent', plural: 'Cents', singular: 'Cent', symbol: '' },
},
});
// "One Hundred Euros And Fifty Cents Only"singular is the form used for an amount of exactly one; without it, the plural form is used for every amount ("One Euros").
3-decimal currencies — OMR, IQD, KWD and BHD have 1000 minor units. en-OM, en-IQ and ar-IQ have this built in; for a custom currency, add precision: 3:
const om = new ToWords({ localeCode: 'en-OM' });
om.convert('500.500', { currency: true });
// "Five Hundred Omani Rials And Five Hundred Baisa Only"
const tw = new ToWords({ localeCode: 'en-US' });
tw.convert('1.500', {
currency: true,
currencyOptions: {
name: 'Kuwaiti Dinar',
plural: 'Kuwaiti Dinars',
singular: 'Kuwaiti Dinar',
symbol: 'KWD',
precision: 3,
fractionalUnit: { name: 'Fils', plural: 'Fils', symbol: '' },
},
});
// "One Kuwaiti Dinar And Five Hundred Fils Only"Pass amounts as strings when trailing zeros matter: in JavaScript
500.500 === 500.5.
const tw = new ToWords({ localeCode: 'en-US' });
tw.toOrdinal(1); // "First"
tw.toOrdinal(21); // "Twenty First"
tw.toOrdinal(100); // "One Hundredth"Ordinals are available in all 137 locales. They accept non-negative integers, plus the gender and formal options where the locale supports them.
Languages with grammatical gender take a gender option, per call or as a constructor default:
const es = new ToWords({ localeCode: 'es-ES' });
es.convert(1); // "Uno"
es.convert(1, { gender: 'feminine' }); // "Una"
es.convert(21, { gender: 'feminine' }); // "Veintiuna"
es.convert(200, { gender: 'feminine' }); // "Doscientas"
const ar = new ToWords({ localeCode: 'ar-AE' });
ar.convert(3, { gender: 'feminine' }); // "ثلاث"
const fem = new ToWords({ localeCode: 'pt-BR', converterOptions: { gender: 'feminine' } });
fem.convert(2); // "Duas"Supported in 41 locales: Spanish (9), Arabic (9), Portuguese (4), Hebrew (2), Russian, Ukrainian, Belarusian, Polish, Czech, Slovak, Slovenian, Croatian, Serbian, Bulgarian, Greek, Catalan, Romanian, Latvian, Lithuanian, Icelandic, and Swedish. Currency amounts agree with the currency's own gender automatically (ru "Один Рубль", uk "Одна Гривня"). The capability matrix lists support per locale.
useAnd inserts the locale's "and" word before the last part of a number:
const tw = new ToWords({ localeCode: 'en-US' });
tw.convert(123, { useAnd: true }); // "One Hundred And Twenty Three"
tw.convert(1023, { useAnd: true }); // "One Thousand And Twenty Three"- Danish and Norwegian (
da-DK,nb-NO) include "Og" by default, as written Danish and Norwegian do ("Hundrede Og Tyve"). PassuseAnd: falseto leave it out. - It has no effect where the locale already joins numbers with a connector (Portuguese "E", Spanish "Y") or has none (ja-JP, zh-CN, zh-TW, yue-HK).
By default, the digits after the point word are read as a number, with any leading zeros read one by one. decimalStyle: 'digit-by-digit' reads every digit on its own, and for legal and financial writing decimalStyle: 'fraction' reads them as a fraction with a place-value denominator:
const tw = new ToWords({ localeCode: 'en-US' });
tw.convert(123.45); // "One Hundred Twenty Three Point Forty Five"
tw.convert(0.05); // "Zero Point Zero Five"
tw.convert(123.45, { decimalStyle: 'digit-by-digit' }); // "One Hundred Twenty Three Point Four Five"
tw.convert(123.45, { decimalStyle: 'fraction' }); // "One Hundred Twenty Three And Forty Five Hundredths"
tw.convert(1.1, { decimalStyle: 'fraction' }); // "One And One Tenth"
tw.convert(0.001, { decimalStyle: 'fraction' }); // "Zero And One Thousandth"
const ru = new ToWords({ localeCode: 'ru-RU' });
ru.convert(1.21, { decimalStyle: 'fraction' }); // "Одна Целая Двадцать Одна Сотая"- Fraction style covers 1 to 6 decimal places (tenths to millionths) in 101 locales, and up to 15 in Russian, Ukrainian, Belarusian, Polish, Czech and Slovak, each with its own grammar: Russian, for example, uses the singular denominator after 21, 31 and so on.
- For more decimal places, or a locale without fraction words (Arabic, East Asian and Turkic locales, among others), it falls back to the default style without an error. The fraction-style guide lists the locales.
- Currency amounts always use the currency's fractional unit ("Fifty Six Cents"), whichever style is set.
Output is in Title Case by default. casing changes it in every locale whose script has capital letters; other scripts are returned unchanged. hyphenate: true writes English compounds with a hyphen.
const tw = new ToWords({ localeCode: 'en-US' });
tw.convert(21.5, { currency: true, casing: 'sentence', hyphenate: true });
// "Twenty-one dollars and fifty cents only"
tw.convert(21, { casing: 'lower', hyphenate: true }); // "twenty-one"
tw.convert(21, { casing: 'upper' }); // "TWENTY ONE"
const de = new ToWords({ localeCode: 'de-DE' });
de.convert(2500000, { currency: true, casing: 'natural' }); // "zwei Millionen fünfhunderttausend Euro"casing |
Result |
|---|---|
'title' |
Every word capitalised (the default) |
'lower' |
Everything lowercase, with the language's own rules (Turkish İki → iki) |
'upper' |
Everything uppercase |
'natural' |
Lowercase except words the language capitalises: German nouns, "Australian", "CFA", and your own currency names |
'sentence' |
'natural' with the first letter capitalised, as on a cheque |
JavaScript numbers lose precision above Number.MAX_SAFE_INTEGER (and print in scientific notation past 1e21), so pass large values as a bigint or a string:
const tw = new ToWords({ localeCode: 'en-US' });
tw.convert(1000000000000000000n); // "One Quintillion"
tw.convert('9007199254740993');
// "Nine Quadrillion Seven Trillion One Hundred Ninety Nine Billion Two Hundred Fifty Four Million Seven Hundred Forty Thousand Nine Hundred Ninety Three"
tw.convert(123456789012345678901); // ❌ precision already lost: "...Six Hundred Eighty Three Thousand Nine Hundred Sixty Eight"
tw.convert(123456789012345678901n); // ✅ exactEach locale has a verified range: en-US goes up to vigintillions (10^63), for example. A number above it throws a NumberOutOfRangeError (see Error Handling), unless you pass { rangeMode: 'compose' } to compose the words from smaller scale words ('1e70' → "Ten Million Vigintillion"). The manifest reports each locale's exact range.
const cn = new ToWords({ localeCode: 'zh-CN' });
cn.convert(123); // "一 百 二十 三"
cn.convert(123, { formal: true }); // "壹 佰 贰拾 叁"
const tw = new ToWords({ localeCode: 'zh-TW' });
tw.toOrdinal(5, { formal: true }); // "第伍"When no localeCode is given, new ToWords(), toWords(), toOrdinal() and toCurrency() call detectLocale(): it reads navigator.language in browsers and Intl in Node.js and other runtimes, and falls back to en-IN. resolveLocale() applies the same matching to any string, such as a user setting or an Accept-Language header, without reading the environment:
import { detectLocale, resolveLocale, ToWords } from 'to-words';
const tw = new ToWords({ localeCode: detectLocale('en-US') }); // custom fallback
resolveLocale('EN_us'); // "en-US"
resolveLocale('zh-Hant-TW'); // "zh-TW"
resolveLocale('pt'); // "pt-BR" (language-only values use a fixed default)
resolveLocale('xx'); // undefinedIn SSR and APIs, pass the request's locale explicitly. setLocaleDetector() replaces the detector for the whole process, so keep it to tests and app-wide setup.
to-words/manifest describes every locale without loading its word tables, for example to build a language picker or check a feature before using it:
import { getLocaleCapabilities, getLocaleMetadata, isSupportedLocale, SUPPORTED_LOCALES } from 'to-words/manifest';
isSupportedLocale('en-US'); // true
getLocaleCapabilities('zh-CN')?.formal; // true
getLocaleMetadata('hi-IN')?.numbering.grouping; // [3, 2]
getLocaleMetadata('en-US')?.range.maximumSupported.cardinal; // exact inclusive ceiling
SUPPORTED_LOCALES.length; // 137The same data is published as the capability matrix.
For units or wording no built-in locale has, define your own locale class and pass it to ToWordsCore; no fork or PR needed:
import { ToWordsCore } from 'to-words/core';
import type { LocaleInterface, LocaleConfig } from 'to-words/types';
class BitcoinLocale implements LocaleInterface {
config: LocaleConfig = {
currency: {
name: 'Bitcoin',
plural: 'Bitcoins',
singular: 'Bitcoin',
symbol: '₿',
fractionalUnit: { name: 'Satoshi', plural: 'Satoshis', singular: 'Satoshi', symbol: 'sat' },
},
texts: { and: 'And', minus: 'Minus', only: 'Only', point: 'Point' },
numberWordsMapping: [
// Largest number first, same structure as any built-in locale
// ...
{ number: 2, value: 'Two' },
{ number: 1, value: 'One' },
{ number: 0, value: 'Zero' },
],
};
}
const tw = new ToWordsCore();
tw.setLocale(BitcoinLocale);
tw.convert(2.1, { currency: true }); // "Two Bitcoins And Ten Satoshis Only"- The easiest start is to copy the nearest built-in locale from
src/locales/and change only what differs. - Beyond the required fields,
LocaleConfighas optional keys for grammar such as plural rules, gender agreement, connector words and ordinal formation. Each is documented insrc/types.ts, and the built-in locales show them in use. - A custom locale is validated on first use;
assertLocaleConfig()fromto-words/locale-contractruns the same check in CI. Its configuration is frozen once initialized, so create a new class to change it.
npx to-words 12345 --locale en-US
# Twelve Thousand Three Hundred Forty Five
npx to-words 1234.56 --locale en-US --currency
# One Thousand Two Hundred Thirty Four Dollars And Fifty Six Cents Only
npx to-words 3 --locale en-US --ordinal
# Third
npx to-words --locale en-US -- -5
# Minus Five
npx to-words --detect-locale
# en-US (or whatever your system locale is)Without --locale, the system locale is used. After npm i -g to-words, the to-words command is available directly.
to-words has no framework dependencies. Create an instance once, outside render, and call it like any function:
// Change the locale import to match your users' region (e.g. 'to-words/en-GB')
import { ToWords } from 'to-words/en-US';
const toWords = new ToWords();
function PriceInWords({ amount }: { amount: number }) {
return <span>{toWords.convert(amount, { currency: true })}</span>;
}
// <PriceInWords amount={1234.56} /> renders
// "One Thousand Two Hundred Thirty Four Dollars And Fifty Six Cents Only"The framework guide has the same for Vue 3, Angular (a pipe), Svelte, Next.js (server and client components) and Express.
| System | Used by | Large numbers |
|---|---|---|
| Short scale | English and most locales | Million (10^6), Billion (10^9), Trillion (10^12) … up to Vigintillion (10^63) |
| Long scale | German, French, Spanish, Portuguese (pt-PT) and others | 10^9 is a thousand millions (Milliarde, Milliard, Mil Millones), 10^12 a Billion (Billón) |
| Indian | India, Bangladesh, Nepal, Pakistan (ur-PK) |
Lakh (10^5), Crore (10^7) |
| East Asian | Japanese, Chinese, Korean | 万 (10^4), 億 (10^8), 兆 (10^12), 京 (10^16) |
new ToWords({ localeCode: 'de-DE' }).convert(1000000000); // "Eine Milliarde"
new ToWords({ localeCode: 'en-IN' }).convert(1000000000000n); // "One Lakh Crore"
new ToWords({ localeCode: 'hi-IN' }).convert(1000000000000n); // "एक लाख करोड़"
new ToWords({ localeCode: 'ja-JP' }).convert(100000000); // "一 億"Above a crore, the Indian and Bangladeshi locales compose crore-based amounts, as official Indian usage does (Union Budget 2026-27: "₹53.5 lakh crore"): 10^9 is "One Hundred Crore" and 10^12 "One Lakh Crore". Nepal (ne-NP, en-NP) keeps Arba and Kharba, as its government budget does; ur-PK counts in ارب (ایک ہزار ارب = 10^12); and en-PK and en-MU use Million and Billion, as the Pakistani and Mauritian budget documents do.
| Import | Exports |
|---|---|
to-words |
ToWords class, toWords, toOrdinal, toCurrency, detectLocale, resolveLocale, setLocaleDetector, LOCALES, NumberOutOfRangeError |
to-words/<locale> (e.g. to-words/en-US) |
ToWords class (pre-configured), toWords, toOrdinal, toCurrency — no localeCode option |
to-words/manifest |
SUPPORTED_LOCALES, isSupportedLocale, getLocaleCapabilities, getLocaleMetadata |
to-words/core |
ToWordsCore, for custom locales |
to-words/locale-contract |
assertLocaleConfig, validateLocaleConfig |
to-words/errors |
NumberOutOfRangeError |
to-words/types |
TypeScript types (ConverterOptions, LocaleConfig, LocaleCode …) |
| Call | Returns |
|---|---|
new ToWords({ localeCode?, converterOptions? }) |
An instance; converterOptions sets defaults that each call can override |
tw.convert(number, options?) / toWords(number, options?) |
Number in words |
tw.toOrdinal(number, options?) / toOrdinal(number, options?) |
Ordinal in words; non-negative integers only; options gender, formal |
toCurrency(number, options?) |
Same as toWords with currency: true |
detectLocale(fallback?) |
The runtime's supported locale, or fallback (default 'en-IN') |
resolveLocale(input) |
The matching LocaleCode, or undefined |
number can be a number, bigint or numeric string. The functional helpers take localeCode in their options and cache one instance per locale.
| Option | Type | Default | Description |
|---|---|---|---|
currency |
boolean | false |
Convert as a currency amount, with the locale's currency |
currencyOptions |
object | — | Replace the locale's currency (name, plural, singular, symbol, precision, fractionalUnit); see Currency |
ignoreDecimal |
boolean | false |
Drop the fractional part |
ignoreZeroCurrency |
boolean | false |
Skip a zero main amount ("Thirty Six Paise Only") |
doNotAddOnly |
boolean | false |
Omit the "Only" word in currency mode |
includeZeroFractional |
boolean | false |
For string input like "123.00", include "And Zero Paise" |
gender |
string | — | 'masculine' or 'feminine', in locales with gendered number words |
useAnd |
boolean | — | Insert the locale's "and" before the last part ("One Hundred And Twenty Three"); default true for da-DK and nb-NO |
formal |
boolean | — | Formal/financial characters (zh-CN, zh-TW) |
decimalStyle |
string | 'digit' |
'digit' reads the digits after the point as a number; 'digit-by-digit' reads each digit; 'fraction' reads "Forty Five Hundredths"; see Decimals |
casing |
string | 'title' |
'title', 'lower', 'upper', 'natural' or 'sentence'; see Letter Case & Hyphens (also for toOrdinal) |
hyphenate |
boolean | false |
Hyphenate English compounds ("Twenty-One"); no effect in other languages (also for toOrdinal) |
rangeMode |
string | 'strict' |
'strict' throws above the locale's verified range; 'compose' composes larger numbers from smaller scale words |
Option objects, LOCALES and the default option objects are frozen or copied when used, so changing them later has no effect on an instance. The API reference on the docs site also covers setLocaleDetector(), the utility methods and locale inspection.
Invalid input throws an Error with a descriptive message:
const tw = new ToWords({ localeCode: 'en-US' });
tw.convert('abc'); // Error: Invalid Number "abc" (also NaN and Infinity)
tw.toOrdinal(-5); // Error: Ordinal numbers must be non-negative integers, got "-5"
new ToWords({ localeCode: 'xx-XX' }).convert(1); // Error: Unknown Locale "xx-XX".A number above the locale's verified range throws a NumberOutOfRangeError (a RangeError with code: 'NUMBER_OUT_OF_RANGE') that names the locale and its maximum:
import { NumberOutOfRangeError } from 'to-words';
try {
tw.convert(userInput);
} catch (error) {
if (error instanceof NumberOutOfRangeError) {
// too large for this locale; or retry with { rangeMode: 'compose' }
} else {
// invalid input
}
}| Group | Locales |
|---|---|
| English (31) | en-AE, en-AU, en-BD, en-CA, en-GB, en-GH, en-HK, en-IE, en-IN, en-IQ, en-JM, en-KE, en-LK, en-MA, en-MM, en-MU, en-MY, en-NG, en-NP, en-NZ, en-OM, en-PH, en-PK, en-SA, en-SG, en-TT, en-TZ, en-UG, en-US, en-ZA, en-ZW |
| French (10) | fr-BE, fr-CA, fr-CH, fr-CI, fr-CM, fr-DZ, fr-FR, fr-MA, fr-MG, fr-SA |
| Arabic (9) | ar-AE, ar-DZ, ar-EG, ar-IQ, ar-LB, ar-MA, ar-SA, ar-SD, ar-YE |
| Spanish (9) | es-AR, es-CL, es-CO, es-DO, es-ES, es-MX, es-PE, es-US, es-VE |
| Portuguese (4) | pt-AO, pt-BR, pt-MZ, pt-PT |
| Other European (29) | Albanian sq-AL, Belarusian be-BY, Bulgarian bg-BG, Catalan ca-ES, Croatian hr-HR, Czech cs-CZ, Danish da-DK, Dutch nl-NL / nl-SR, Estonian et-EE, Finnish fi-FI, German de-AT / de-CH / de-DE, Greek el-GR, Hungarian hu-HU, Icelandic is-IS, Italian it-IT, Latvian lv-LV, Lithuanian lt-LT, Norwegian nb-NO, Polish pl-PL, Romanian ro-RO, Russian ru-RU, Serbian sr-RS, Slovak sk-SK, Slovenian sl-SI, Swedish sv-SE, Ukrainian uk-UA |
| South Asian (15) | Assamese as-IN, Bengali bn-BD / bn-IN, Gujarati gu-IN, Hindi hi-IN, Kannada kn-IN, Malayalam ml-IN, Marathi mr-IN, Nepali ne-NP, Odia or-IN, Punjabi pa-IN, Sinhala si-LK, Tamil ta-IN, Telugu te-IN, Urdu ur-PK |
| East & Southeast Asian (15) | Burmese my-MM, Cantonese yue-HK, Chinese zh-CN / zh-TW, Filipino fil-PH, Indonesian id-ID, Japanese ja-JP, Javanese jv-ID, Khmer km-KH, Korean ko-KR, Lao lo-LA, Malay ms-MY / ms-SG, Thai th-TH, Vietnamese vi-VN |
| Middle East & Central Asia (7) | Azerbaijani az-AZ, Georgian ka-GE, Hebrew he-IL, Biblical Hebrew hbo-IL, Persian fa-IR, Turkish tr-TR, Uzbek uz-UZ |
| African (8) | Afrikaans af-ZA, Amharic am-ET, Hausa ha-NG, Igbo ig-NG, Swahili sw-KE / sw-TZ, Yoruba yo-NG, Zulu zu-ZA |
Every locale supports cardinals, ordinals and currency. Each language has a docs page with examples, and the capability matrix shows numbering system, range, gender and fraction support per locale.
Full table: country and currency of every locale
| Locale | Language | Country | Currency |
|---|---|---|---|
| af-ZA | Afrikaans | South Africa | Rand |
| am-ET | Amharic | Ethiopia | ብር |
| ar-AE | Arabic | UAE | درهم |
| ar-DZ | Arabic | Algeria | دينار |
| ar-EG | Arabic | Egypt | جنيه |
| ar-IQ | Arabic | Iraq | دينار |
| ar-LB | Arabic | Lebanon | ليرة |
| ar-MA | Arabic | Morocco | درهم |
| ar-SA | Arabic | Saudi Arabia | ريال |
| ar-SD | Arabic | Sudan | جنيه |
| ar-YE | Arabic | Yemen | ريال |
| as-IN | Assamese | India | টকা |
| az-AZ | Azerbaijani | Azerbaijan | Manat |
| be-BY | Belarusian | Belarus | Рубель |
| bg-BG | Bulgarian | Bulgaria | Евро |
| bn-BD | Bengali | Bangladesh | টাকা |
| bn-IN | Bengali | India | টাকা |
| ca-ES | Catalan | Spain | Euro |
| cs-CZ | Czech | Czech Republic | Koruna |
| da-DK | Danish | Denmark | Krone |
| de-AT | German | Austria | Euro |
| de-CH | German | Switzerland | Franken |
| de-DE | German | Germany | Euro |
| el-GR | Greek | Greece | Ευρώ |
| en-AE | English | UAE | Dirham |
| en-AU | English | Australia | Dollar |
| en-BD | English | Bangladesh | Taka |
| en-CA | English | Canada | Dollar |
| en-GB | English | United Kingdom | Pound |
| en-GH | English | Ghana | Cedi |
| en-HK | English | Hong Kong | Dollar |
| en-IE | English | Ireland | Euro |
| en-IN | English | India | Rupee |
| en-IQ | English | Iraq | Dinar |
| en-JM | English | Jamaica | Dollar |
| en-KE | English | Kenya | Shilling |
| en-LK | English | Sri Lanka | Rupee |
| en-MA | English | Morocco | Dirham |
| en-MM | English | Myanmar | Kyat |
| en-MU | English | Mauritius | Rupee |
| en-MY | English | Malaysia | Ringgit |
| en-NG | English | Nigeria | Naira |
| en-NP | English | Nepal | Rupee |
| en-NZ | English | New Zealand | Dollar |
| en-OM | English | Oman | Rial |
| en-PH | English | Philippines | Peso |
| en-PK | English | Pakistan | Rupee |
| en-SA | English | Saudi Arabia | Riyal |
| en-SG | English | Singapore | Dollar |
| en-TT | English | Trinidad and Tobago | Dollar |
| en-TZ | English | Tanzania | Shilling |
| en-UG | English | Uganda | Shilling |
| en-US | English | USA | Dollar |
| en-ZA | English | South Africa | Rand |
| en-ZW | English | Zimbabwe | Zimbabwe Gold |
| es-AR | Spanish | Argentina | Peso |
| es-CL | Spanish | Chile | Peso |
| es-CO | Spanish | Colombia | Peso |
| es-DO | Spanish | Dominican Republic | Peso |
| es-ES | Spanish | Spain | Euro |
| es-MX | Spanish | Mexico | Peso |
| es-PE | Spanish | Peru | Sol |
| es-US | Spanish | USA | Dólar |
| es-VE | Spanish | Venezuela | Bolívar |
| et-EE | Estonian | Estonia | Euro |
| fa-IR | Persian | Iran | تومان |
| fi-FI | Finnish | Finland | Euro |
| fil-PH | Filipino | Philippines | Piso |
| fr-BE | French | Belgium | Euro |
| fr-CA | French | Canada | Dollar |
| fr-CH | French | Switzerland | Franc |
| fr-CI | French | Côte d'Ivoire | Franc CFA |
| fr-CM | French | Cameroon | Franc CFA |
| fr-DZ | French | Algeria | Dinar |
| fr-FR | French | France | Euro |
| fr-MA | French | Morocco | Dirham |
| fr-MG | French | Madagascar | Ariary |
| fr-SA | French | Saudi Arabia | Riyal |
| gu-IN | Gujarati | India | રૂપિયો |
| ha-NG | Hausa | Nigeria | Naira |
| hbo-IL | Biblical Hebrew | Israel | שקל |
| he-IL | Hebrew | Israel | שקל |
| hi-IN | Hindi | India | रुपया |
| hr-HR | Croatian | Croatia | Euro |
| hu-HU | Hungarian | Hungary | Forint |
| id-ID | Indonesian | Indonesia | Rupiah |
| ig-NG | Igbo | Nigeria | Naira |
| is-IS | Icelandic | Iceland | Króna |
| it-IT | Italian | Italy | Euro |
| ja-JP | Japanese | Japan | 円 |
| jv-ID | Javanese | Indonesia | Rupiah |
| ka-GE | Georgian | Georgia | ლარი |
| km-KH | Khmer | Cambodia | រៀល |
| kn-IN | Kannada | India | ರೂಪಾಯಿ |
| ko-KR | Korean | South Korea | 원 |
| lo-LA | Lao | Laos | ກີບ |
| lt-LT | Lithuanian | Lithuania | Euras |
| lv-LV | Latvian | Latvia | Eiro |
| ml-IN | Malayalam | India | രൂപ |
| mr-IN | Marathi | India | रुपया |
| ms-MY | Malay | Malaysia | Ringgit |
| ms-SG | Malay | Singapore | Dolar |
| my-MM | Burmese | Myanmar | ကျပ် |
| nb-NO | Norwegian | Norway | Krone |
| nl-NL | Dutch | Netherlands | Euro |
| nl-SR | Dutch | Suriname | Dollar |
| ne-NP | Nepali | Nepal | रुपैयाँ |
| or-IN | Odia | India | ଟଙ୍କା |
| pa-IN | Punjabi | India | ਰੁਪਇਆ |
| pl-PL | Polish | Poland | Złoty |
| pt-AO | Portuguese | Angola | Kwanza |
| pt-BR | Portuguese | Brazil | Real |
| pt-MZ | Portuguese | Mozambique | Metical |
| pt-PT | Portuguese | Portugal | Euro |
| ro-RO | Romanian | Romania | Leu |
| ru-RU | Russian | Russia | Рубль |
| si-LK | Sinhala | Sri Lanka | රුපියල |
| sk-SK | Slovak | Slovakia | Euro |
| sl-SI | Slovenian | Slovenia | Evro |
| sq-AL | Albanian | Albania | Lek |
| sr-RS | Serbian | Serbia | Dinar |
| sv-SE | Swedish | Sweden | Krona |
| sw-KE | Swahili | Kenya | Shilingi |
| sw-TZ | Swahili | Tanzania | Shilingi |
| ta-IN | Tamil | India | ரூபாய் |
| te-IN | Telugu | India | రూపాయి |
| th-TH | Thai | Thailand | บาท |
| tr-TR | Turkish | Turkey | Lira |
| uk-UA | Ukrainian | Ukraine | Гривня |
| ur-PK | Urdu | Pakistan | روپیہ |
| uz-UZ | Uzbek | Uzbekistan | So'm |
| vi-VN | Vietnamese | Vietnam | Đồng |
| yo-NG | Yoruba | Nigeria | Naira |
| yue-HK | Cantonese | Hong Kong | 元 |
| zh-CN | Chinese | China | 元 |
| zh-TW | Chinese | Taiwan | 元 |
| zu-ZA | Zulu | South Africa | Rand |
Conversions run at hundreds of thousands to millions per second. Benchmarked for en-US on an Apple M5 with Node.js 24:
| Operation | Throughput |
|---|---|
| Small integers (42) | ~6.7M ops/sec |
| Medium integers (12,345) | ~2.4M ops/sec |
| Large integers (15 digits) | ~740K ops/sec |
| Currency conversion | ~1.3M ops/sec |
| BigInt (30+ digits) | ~230K ops/sec |
Run npm run bench to measure locally. Creating an instance costs far more than a conversion, so reuse one instance (or the functional helpers, which cache one per locale).
Runtimes: Node.js 20+ (tested in CI), and Deno, Bun and Cloudflare Workers, which work because the library uses only standard ECMAScript (BigInt, Intl, Map, globalThis) and no Node.js APIs.
Browsers: Chrome 67+, Firefox 68+, Safari 14+, Edge 79+, Opera 54+. BigInt is required, so Internet Explorer is not supported.
| Capability | to-words | number-to-words | written-number | num-words | n2words |
|---|---|---|---|---|---|
| Locale / language coverage | 137 locales | English-focused | Multi-language | Indian English | 70+ languages |
| TypeScript declarations | ✅ | ❌ | ❌ | ✅ | ✅ |
| ESM-ready package | ✅ | ❌ | ❌ | ❌ | ✅ |
Package exports map |
✅ | ❌ | ❌ | ❌ | ✅ |
| BigInt support | ✅ | ❌ | ❌ | ❌ | ✅ |
| Currency conversion mode | ✅ | ❌ | ❌ | ❌ | ✅ |
| Ordinal conversion | ✅ | ✅ | ❌ | ❌ | ✅ |
| Subpath locale imports | ✅ | ❌ | ❌ | ❌ | ✅ |
MIGRATION.md maps each library's API to to-words, with behaviour notes and a regression checklist. The docs site has a detailed comparison.
Why does a large number come out wrong, or in scientific notation?
JavaScript can't represent integers above Number.MAX_SAFE_INTEGER exactly, and writes numbers from 1e21 up in scientific notation, so the value is already wrong before to-words sees it. Pass a bigint (123456789012345678901n) or a string ('123456789012345678901'). See BigInt & Large Numbers.
Why are amounts like 1234.56 read as "Point Fifty Six" and not "Fifty Six Hundredths"?
That is the default decimalStyle: 'digit'. Pass decimalStyle: 'fraction' for the legal style, or currency: true for currency amounts. See Decimals.
Which locale do I get without a localeCode?
The runtime locale, detected from navigator.language or Intl, and en-IN if it can't be matched. Pass localeCode (or detectLocale('your-fallback')) to be explicit. See Locale Detection.
How do I add support for a new locale?
CONTRIBUTING.md walks through adding a locale file and its tests. To use your own locale without contributing it, see Custom Locales.
Contributions are welcome! CONTRIBUTING.md covers development setup, coding guidelines, adding a locale, the commit message format, and the PR process.
For questions or ideas, open an issue or start a discussion. This project follows the Contributor Covenant Code of Conduct.
If to-words is useful in your work, consider supporting ongoing maintenance through GitHub Sponsors. After installation, npm fund to-words shows the same link.
See CHANGELOG.md for a detailed history of changes.