riskihajar/terbilang
A Laravel/PHP package for converting numbers into Indonesian words (terbilang). Useful for invoices, receipts, and documents that need amount-in-words output, with helpers and facade support for quick integration.
composer require riskihajar/terbilang. Laravel's package auto-discovery registers the service provider and the Terbilang facade alias, so no manual wiring is needed on 5.5+.php artisan vendor:publish --provider="Riskihajar\Terbilang\TerbilangServiceProvider" (or --tag=terbilang). The generated config/terbilang.php is where locale, distance.show.*, and output formatting live.en, id, and pt; the default is Indonesian, which is rarely what an English-language app wants. Put it in the config file rather than relying on app()->getLocale().use Riskihajar\Terbilang\Facades\Terbilang;
Terbilang::make(1250000); // "satu juta dua ratus lima puluh ribu" (id)
Terbilang::make(1250000)->toRoman();
number_to_words(42); // if the helper is available in your version
php artisan tinker) is Terbilang::make(0), Terbilang::make(-5), and Terbilang::make(1.5). As of 2.1.1 these no longer crash, but the exact output shape is worth pinning before you write assertions against it.Currency amounts in documents. The dominant use case is "say the amount" on invoices, receipts, and contracts. Wrap it in a small presenter so you control the suffix and case:
class AmountInWords
{
public static function id(int $amount, string $currency = 'Rupiah'): string
{
return ucfirst(Terbilang::make($amount)) . ' ' . $currency;
}
}
Model accessors and API resources. Expose the spelled-out form alongside the numeric field instead of calling the facade in Blade. This keeps the locale decision in one place and makes it testable:
protected $appends = ['total_in_words'];
public function getTotalInWordsAttribute(): string
{
return Terbilang::make($this->total);
}
Per-request locale switching. The package reads its own config value, not the app locale, so switching languages for a single request means overriding config at runtime — ideally in middleware, and ideally restored afterwards:
config(['terbilang.locale' => $request->user()->locale]);
Wrap this in a middleware rather than scattering config([...]) calls through controllers; the global config mutation is easy to leak between queued jobs and Octane workers.
Roman numerals for numbering. toRoman() is useful for chapter/section numbering in generated PDFs and appendices. Since 2.1.1 it throws Riskihajar\Terbilang\Exceptions\InvalidNumber outside 1–3999, so guard at the call site:
if ($n >= 1 && $n <= 3999) {
$label = Terbilang::make($n)->toRoman();
}
Distance / relative time. The distance feature renders a human-readable gap (e.g. "2 years, 3 months ago") from a date or interval. Use it for "member since" and "last updated" labels, but treat it as display-only — it is not a substitute for Carbon's diff API when you need exact values.
Testing. With 271 tests across all three locales in 2.1.1, the upstream behaviour is now well pinned — but your own snapshot tests will break when translation typos are fixed (see below). Assert on exact strings for the languages you ship, and use assertMatchesRegularExpression on a looser pattern for the rest.
toRoman() now throws instead of returning ''. This is the only behavioural break in 2.1.1. Any if ($roman === '') fallback you wrote for out-of-range values is now dead code, and unguarded calls for values <= 0 or >= 4000 will throw. Audit for toRoman() before upgrading.terbilang.period.show.* → terbilang.distance.show.*. If you published config/terbilang.php on 2.1.0, your copy still has the old key and your show toggles are silently doing nothing. Republish and diff, or rename the block by hand. After any config edit, run php artisan config:clear — config caching is the usual reason a key change "doesn't work".fourty → forty, Indonesian Nopember → November, and the Portuguese abbreviation key short → large-number. Any test asserting on those exact strings, or an override file you copied from the package, needs updating. If you have published translation files under lang/vendor/terbilang, they will still contain the old spellings until you re-copy them.type() no longer emits a leading separator. Both are bug fixes but both change output — expect diff churn in PDF fixtures.LargeNumber no longer crashes on zero or negative input. If you were previously catching exceptions or trimming whitespace as a workaround, remove those shims.Terbilang::make(1234.567) is unlikely to produce what you want; do Terbilang::make((int) round($amount)) for money, and decide explicitly how you handle the cents remainder.App::getLocale(). Setting app()->setLocale('en') will not change the package's output. This is the single most common source of "the translation isn't working" reports.str_replace, override the translation source (publish and edit the vendor language files) so every call site benefits. String surgery on the output breaks silently the next time a package translation is fixed.bootstrap/cache/config.php and composer dump-autoload before assuming the release is broken — several of the 2.1.1 fixes are config-driven and invisible under a cached config.How can I help you explore Laravel packages today?