Documentation & live playground →
TypeScript implementation of ICU Rule-Based Number Formatting (RBNF) — converts numbers to words across 89 CLDR locales.
import { RuleBasedNumberFormat } from "@pointnet/cldr-spellout";
const fmt = RuleBasedNumberFormat.fromLocale("en");
fmt.format(123); // "one hundred twenty-three"
fmt.format(1, "%digits-ordinal"); // "1st"
fmt.parse("forty-two"); // 42- 89 CLDR locales — all locales from
cldr-rbnfv48 (Unicode CLDR 48) - Spellout & ordinals — cardinal numbers, ordinals, and more depending on locale
- Parse — convert spelled-out text back to numbers
- Custom rules — construct a formatter from your own RBNF rule string
- Synchronous — no async, no dynamic imports; works in Node.js and browsers
- Dual ESM/CJS — ships both formats; tree-shakeable (
"sideEffects": false)
npm install @pointnet/cldr-spellout
Requires Node.js ≥ 24 (for native ESM JSON imports). Browser bundles are unaffected.
import { RuleBasedNumberFormat } from "@pointnet/cldr-spellout";
const en = RuleBasedNumberFormat.fromLocale("en");
en.format(1000); // "one thousand"
en.format(0.5); // "zero point five"
en.format(-7); // "minus seven"
const fr = RuleBasedNumberFormat.fromLocale("fr");
fr.format(71); // "soixante-et-onze"
fr.format(80); // "quatre-vingts"
const de = RuleBasedNumberFormat.fromLocale("de");
de.format(42); // "zweiundvierzig"Each locale exposes one or more named rule sets. Pass a rule set name as the second argument to format():
const en = RuleBasedNumberFormat.fromLocale("en");
en.format(1, "%digits-ordinal"); // "1st"
en.format(2, "%digits-ordinal"); // "2nd"
en.format(13, "%digits-ordinal"); // "13th"
// List available rule sets
en.getRuleSetNames();
// → ["%spellout-numbering", "%spellout-numbering-year",
// "%spellout-cardinal", "%spellout-ordinal", "%digits-ordinal", ...]const en = RuleBasedNumberFormat.fromLocale("en");
en.parse("one hundred twenty-three"); // 123
en.parse("forty-two"); // 42fromCldrData() accepts any object matching the IRBNFData shape — useful when you supply your own rule data rather than a locale file:
import { RuleBasedNumberFormat } from "@pointnet/cldr-spellout";
import type { IRBNFData } from "@pointnet/cldr-spellout";
const durationData: IRBNFData = {
rbnf: {
identity: { language: "en" },
rbnf: {
DurationRules: {
"%%min-sec": [["0", ":=00="]],
"%duration": [
["0", "=%%seconds="],
["60/60", "<0<>%%min-sec>"],
["3600/3600","<0<:>%%min-sec>"],
],
"%%seconds": [
["0", "0 sec."],
["1", "1 sec."],
["2", "=#,##0= sec."],
],
},
},
},
};
const fmt = RuleBasedNumberFormat.fromCldrData(durationData);
fmt.format(3662); // "1:01:02"You can construct a formatter from any ICU RBNF rule description string:
import { RuleBasedNumberFormat } from "@pointnet/cldr-spellout";
// Custom duration rules (adapted from ICU test suite)
const rules = `
%%min-sec:
0: :=00=;
%duration:
0: =%%seconds=;
60/60: <0<>%%min-sec>;
3600/3600: <0<:>%%min-sec>;
%%seconds:
0: 0 sec.;
1: 1 sec.;
2: =#,##0= sec.;
`;
const fmt = new RuleBasedNumberFormat(rules, "en");
fmt.format(3662); // "1:01:02"| Method | Description |
|---|---|
RuleBasedNumberFormat.fromLocale(locale) |
Create a formatter from a built-in CLDR locale. Throws if the locale is unsupported. |
RuleBasedNumberFormat.fromCldrData(data, locale?) |
Create a formatter from a raw IRBNFData object. locale defaults to the identity in the JSON. |
RuleBasedNumberFormat.getSupportedLocales() |
Returns an array of all 89 supported BCP 47 locale tags. |
new RuleBasedNumberFormat(rules: string, locale?: string)Construct from an ICU RBNF rule description string. locale is a BCP 47 tag used for Intl.NumberFormat / Intl.PluralRules (default: "en").
| Method | Description |
|---|---|
format(number, ruleSetName?) |
Format a number to text. Uses the default rule set unless ruleSetName is given. |
parse(text) |
Parse spelled-out text back to a number. Tries all public rule sets and returns the best match. |
getRuleSetNames() |
Returns all public rule set names (e.g., ["%spellout-numbering", "%digits-ordinal"]). |
getDefaultRuleSetName() |
Returns the name of the active default rule set. |
setDefaultRuleSet(name) |
Change the default rule set used when format() is called without a rule set name. |
getRules() |
Returns the original rule description string. |
import type { IRBNFData } from "@pointnet/cldr-spellout";IRBNFData is the shape of a cldr-rbnf locale JSON file, accepted by fromCldrData().
89 BCP 47 locale tags:
af ak am ar az be bg bs ca ccp chr cs cy da de de-CH ee el en en-IN eo es es-419 et fa fa-AF ff fi fil fo fr fr-BE fr-CH ga gu he hi hr hu hy id is it ja ka kk kl km ko ky lb lo lrc lt lv mk ms mt my ne nl nn no pl pt pt-PT qu ro ru se sk sl sq sr sr-Latn su sv sw ta th tr uk und vec vi yue yue-Hans zh zh-Hant
Use RuleBasedNumberFormat.getSupportedLocales() at runtime to get the full list.
MIT © pointnet