Docs

Enough to be productive in five minutes. The full reference lives in the README, LANGUAGES.md and the docs/ directory.

Install

npm install n2words

Zero dependencies. Node.js 22+, or any browser with BigInt support.

Command line

The package ships an n2words command, so a number can be spelled without writing any code. npx runs it without installing anything first.

npx n2words 42 --lang en                   # forty-two
npx n2words 42 -l fr-FR --ordinal          # quarante-deuxième
npx n2words 42.50 -l en-US --currency EUR  # forty-two euro and fifty cents

--lang takes any entry point this site lists — a bare tag (en, de) or a region/script-qualified code (en-GB, zh-Hans-CN), in any casing. --cardinal (the default), --ordinal and --currency pick the form; --currency <CODE> names the currency and selects the form in one flag, while --form currency keeps the locale's own default.

The options are the language's own. Every flag past the built-ins is derived at runtime from the module you selected — from the same <form>Defaults and <form>Values declarations the demo builds its options panel from — so the CLI offers exactly what that language and form accept, and nothing else:

npx n2words --list                # every entry point and the forms it exports
npx n2words --help -l es-ES       # es-ES's forms, their ceilings, and its options
npx n2words 101 -l es-ES --gender feminine
npx n2words 1500 -l en --hundred-pairing

With no values it reads stdin, one per line, streaming — so it composes in a pipeline. Values are read as text and passed through untouched, so precision is never lost.

printf '1\n2\n3\n' | npx n2words -l de
seq 1 100 | npx n2words -l fr --json > numbers.jsonl

--json emits one record per line ({input, output, lang, form, options}), with failures reported as {input, error} rather than on stderr. Exit codes are part of the contract: 0 success, 1 a usage error, 2 at least one value the library refused. A bad line in a pipe doesn't stop the rest.

The three forms

Each language exports one, two or all three of these as plain functions — no classes, no configuration, no shared state.

import { toCardinal, toOrdinal, toCurrency } from 'n2words/en-US'

toCardinal(1234)     // 'one thousand two hundred thirty-four'
toOrdinal(1234)      // 'one thousand two hundred thirty-fourth'
toCurrency(42.50)    // 'forty-two dollars and fifty cents'

Cardinal and currency accept negatives and decimals; ordinal is positive integers only. Values can be a number, a numeric string, or a bigint — pass a string or a bigint when the value is larger than Number.MAX_SAFE_INTEGER and it keeps full precision.

Entry points

Import one language, not the library: there is no barrel file, so you only ever ship the languages you name. Most languages resolve from a bare BCP 47 primary subtag.

import { toCardinal } from 'n2words/de'          // bare tag — the primary way
import { toCardinal } from 'n2words/en-GB'       // a specific variant
import { toCardinal } from 'n2words/zh-Hans-CN'  // zh has no bare tag
import { toCardinal as fr } from 'n2words/fr'    // rename when mixing

A bare tag is a documented alias for one specific variant. Chinese, Portuguese, Serbian and Amharic have none — their variants diverge in script or core numbering grammar, not just vocabulary — so they're always imported by full code.

When does the region matter?

Less often than 72 variants suggests. A region changes two independent things, and usually only the second: which words the numbers get, and which currency the amount defaults to. English has 16 regions but only four spellings —

import { toCardinal as us } from 'n2words/en-US'
import { toCardinal as gb } from 'n2words/en-GB'
import { toCardinal as ind } from 'n2words/en-IN'
import { toCardinal as au } from 'n2words/en-AU'

us(101)   // 'one hundred one'
gb(101)   // 'one hundred and one'
ind(12345678)  // 'one crore twenty-three lakh forty-five thousand six hundred and seventy-eight'
au(101)   // 'one hundred and one' — identical to en-GB; only its default currency differs (AUD)

The languages table groups every language's regions by the words they actually produce, so you can see at a glance whether a region is a real spelling or only a currency default.

Options

A form that takes options also exports its defaults, and any option with a fixed set of values exports that set — so the accepted values are readable at runtime and narrowed to a literal union in TypeScript, instead of being something you discover by catching an error.

import { toCardinal, cardinalDefaults } from 'n2words/en-US'

cardinalDefaults  // { hundredPairing: false, and: false }

toCardinal(1500)                           // 'one thousand five hundred'
toCardinal(1500, { hundredPairing: true })  // 'fifteen hundred'
toCardinal(101, { and: true })              // 'one hundred and one'

An unknown key or a wrong-typed value throws a TypeError; a value outside a declared set throws a RangeError. The demo renders each language's real options panel, and LANGUAGES.md lists every option, type and default.

Currency

A bare tag names a language, and a default currency belongs to a country — so toCurrency is the one form a bare tag won't guess at.

import { toCurrency } from 'n2words/en'
import { toCurrency as usd } from 'n2words/en-US'

// `en` is a language, so it has no default currency to fall back on.
toCurrency(42.50)                       // TypeError
toCurrency(42.50, { currency: 'GBP' })  // 'forty-two pounds and fifty pence'
usd(42.50)                              // 'forty-two dollars and fifty cents'

Any language can name a currency it has words for, and the set it knows is validated rather than guessed at — read it from currencyValues:

import { currencyDefaults, currencyValues } from 'n2words/pt-BR'

currencyDefaults.currency  // 'BRL'
currencyValues.currency    // ['BRL', 'USD', 'EUR', 'GBP', 'JPY']

Upgrading from v5? All three breaking changes are in toCurrency — see the v6 migration guide.

Range and errors

Each form spells values up to the largest scale word its language knows, then throws a RangeError rather than inventing vocabulary. The ceiling varies by language and by form — es-ES cardinals reach 1030 − 1, its ordinals only 109 − 1. For any input, a form returns a well-formed string or throws; it never returns something malformed.

import { toCardinal } from 'n2words/en-US'

toCardinal(10n ** 65n)  // 'one hundred vigintillion …'
toCardinal(10n ** 66n)  // RangeError: the largest supported value is 10^66 - 1

The languages table lists each variant's range.

Browser and CDN

<!-- ESM (recommended) -->
<script type="module">
  import { toCardinal } from 'https://cdn.jsdelivr.net/npm/n2words/dist/en.js'
  console.log(toCardinal(42))  // 'forty-two'
</script>

<!-- UMD (legacy script tags) -->
<script src="https://cdn.jsdelivr.net/npm/n2words/dist/en.umd.js"></script>
<script>
  n2words.en(42)                                   // 'forty-two'
  n2words.ordinal.en(42)                           // 'forty-second'
  n2words.currency.en(42.50, { currency: 'USD' })  // 'forty-two dollars and fifty cents'
</script>

dist/{code}.js carries all three forms. A page that needs only one can fetch just that form from dist/{code}/{form}.js — under half the bytes for cardinal or ordinal. That's exactly what this site does: the demo imports dist/{code}/{form}.js for whichever form you selected.

<script type="module">
  import { toCurrency } from 'https://cdn.jsdelivr.net/npm/n2words/dist/en-US/currency.js'
  console.log(toCurrency(42.50))  // 'forty-two dollars and fifty cents'
</script>

Installing from npm with a bundler? There's nothing to opt into — the package is sideEffects-free and each form is an independent export, so importing one form drops the others.

TypeScript

Declarations ship with the package; no @types install. Option values are literal unions, so a typo in a currency code or a gender fails at compile time as well as at runtime.

import { toCurrency, type CurrencyOptions } from 'n2words/en-US'

toCurrency(42.50, { currency: 'GBP' })  // ok
toCurrency(42.50, { currency: 'XYZ' })  // compile error

Compatibility

BigInt is required and cannot be polyfilled.

Going further