Skip to content
AFM Studio
  • Pages

    AFM Studio
  • Work
  • Lab
  • About
  • CV
  • Contact
  • Projects

    AksaraA searchable library for Otorita IKN's knowledge, where the server checks every reader's access across three levels
  • Daily TaqwaTamper-resistant prayer attendance system for ~2,000 OIKN government employees at Masjid Negara IKN
  • Lantara v2A permit engine for Otorita IKN where administrators add new permit types from a screen, without a developer
  • eKiosk IKNTwo control rooms for Nusantara: kiosk content management and live bus tracking with arrival times that grade themselves
  • E-Monev IKNA monitoring platform for Nusantara's 23-year build, tracking every unit's quarterly progress against 24 national KPIs
  • JDIH OIKNOIKN's public legal document portal, with a controlled publishing workflow and AI summaries that never leave the server
  • Jejak PenanamanA check-in app for IKN tree-planting events that proves each participant was on site with a geofenced selfie
  • Dashboard SAKTIA nightly pipeline and dashboard that replaces OIKN's manual budget downloads for all six of its budget units
  • DoseRxA bedside dose calculator for 127 drugs that rounds each dose to tablets and syrups Indonesian pharmacies sell
  • CubiqA speedcubing trainer with competition-grade timing and a solver I wrote for each of the 8 major puzzles
  • ScimotionAn interactive science library of 86 articles, each explained next to animations the reader can control
  • WA Daily Scrum BotA WhatsApp bot that chases missing standup reports for a 14-person team and flags tasks stuck for days

↑ ↓ to move · Enter to open · Esc to close

Terbilang

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

Screenshots
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, Date or module-level state.
  • API: format(bigint | string) → words and parse(words) → bigint, exact inverses under identical options. The public surface is 2 functions, 8 types and 4 constants.
  • No parseInt, Number() or number arithmetic 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.ts and sideEffects: false. A separate tsconfig.lib.json keeps DOM and Next types out of the declarations. Version 0.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.json currently 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

Beranda
Pengubah
Kata ke Angka
Aturan
Survei

Hiring, or building something similar?

I'm open to full-time roles and select freelance work.