Terbilang
A library that writes Indonesian amounts in words for invoices and reads them back, exact at any size

- values round-tripped on every change
- 1,000,001
- largest unit supported, kuadriliun
- 10¹⁵
- typed refusals instead of a guess
- 6
- runtime dependencies in the library
- 0
On this page
The problem
Indonesian invoices, receipts and cheques carry the amount twice, once in digits and once in words, called terbilang. Every accounting, payroll and invoicing system in the country needs this, so almost every one writes its own. Many get it wrong in ways that end up on printed paper.
The spelling has an irregular rule. A leading 1 becomes se- in sepuluh, seratus and seribu, but standard usage says satu juta, not sejuta. Empty groups leak: 1.000.001 is satu juta satu, never satu juta nol ribu satu. And JavaScript numbers lose precision above about 9 quadrillion. Code built on them works through the trillions, then quietly writes the wrong amount on an actual invoice.
What I built
A library first, with a site to show it
I built Terbilang as a library that other invoicing software can depend on unchanged. The first intended user is Rinci, my payslip project, which needs it for amount lines. The library knows nothing about browsers and has no runtime dependencies. The website imports it and computes nothing of its own. I rebuilt the whole site after the library was finished, and the library didn't change at all.
Large amounts stay exact
The library accepts only whole numbers stored exactly, as big integers or digit strings. Passing an ordinary JavaScript number is refused, never converted, because that conversion is where large amounts go wrong. A permanent set of tests checks values above the precision limit. The largest supported unit is kuadriliun (10¹⁵), because the national dictionary, KBBI, lists the word. That ceiling also puts some supported values above the precision limit, which keeps those tests meaningful.
It refuses rather than guesses
Six kinds of error each get a typed answer that names exactly what failed: an amount too large, a number with decimals, words it can't parse, or options that contradict each other. It never returns a best guess or a partial result. A wrong but believable amount on an invoice is worse than no amount. Past kuadriliun, Indonesian usage is unsettled, so the library stops instead of extending the pattern.
It reads words back into numbers
Most tools only go one way. Terbilang also parses words back into numbers, which is the harder direction. Dua ratus lima puluh ribu means (2×100 + 5×10) × 1,000, not 200 + 50,000. Having both directions lets me test them against each other.
Every number up to a million is checked
The problem is bounded, so I test it completely. Every value from 0 to 1,000,000 is written out, checked, read back and compared with the original. That's 1,000,001 round trips on every change, in about five seconds, and the build fails if any one breaks.
Result
The library is built and ready to publish, with support for both module formats. The public site is in Indonesian and English. As you type, the converter draws brackets over each group of three digits, labelled juta or ribu, and empty groups visibly disappear. A document mode prints the amount the way it appears on a receipt: # seribu lima ratus rupiah #. Five options, such as sejuta or milyar versus miliar, each cite the usage behind both choices. A page for comparing other packages ships empty, because I haven't measured any yet. I built it in under seven hours on 5 August 2026.
Under the hoodTechnical detail for engineers
lib/is about 980 lines across 8 files: lexicon, grouping, formatter, parser, options and structured errors. No React, DOM,Intl,Dateor module-level state.- API:
format(bigint | string) → wordsandparse(words) → bigint, exact inverses under identical options. The public surface is 2 functions, 8 types and 4 constants. - No
parseInt,Number()ornumberarithmetic in the value path. JavaScript's safe-integer limit is 2⁵³ ≈ 9×10¹⁵. - Error variants are a discriminated union, e.g.
di-luar-jangkauan,tipe-tidak-didukung,opsi-bertentangan. - Options:
sejuta,ejaanMiliar,mataUang,sen,kapitalisasi. Every combination is round-trip tested under itself, and keys are stable because they appear in the URL hash. - tsup builds dual ESM and CJS with
.d.tsandsideEffects: false. A separatetsconfig.lib.jsonkeeps DOM and Next types out of the declarations. Version0.0.0,files: ["dist"], MIT licence. - The survey script (
pnpm survey:record) records package versions and dates. It doubles as a differential test: any disagreement must be classified as our bug, their bug or a valid variant.data/survey/hasil.jsoncurrently holds empty arrays. - The site is a Next.js 14 static export, 4 routes × 2 locales. The night theme swaps CSS variables via
data-tema="malam", set before first paint. Converter state lives in the URL hash. - About 4,370 lines of TypeScript, 26 commits.
Screenshots
Hiring, or building something similar?
I'm open to full-time roles and select freelance work.