Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Number To Words Laravel Package

kwn/number-to-words

A PHP library that converts numbers and currency amounts into readable words across many languages. Use number or currency transformers via RFC 3066 language identifiers, call toWords(), or a static helper. Install with Composer.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and pin the major version

composer require kwn/number-to-words:^3.0

v3 requires PHP 8.2+, so it drops into Laravel 10/11/12 projects without fuss. It is not a Laravel package: there is no service provider, no config file, no facade. You wire it yourself.

Where to look first

  1. The locale table in the README — note the two columns (Number / Currency). Several languages have numbers but no currency transformer (sv, it, nl, cs, et, fr_BE, mk, fa, ku). That single fact drives most of your design decisions.
  2. src/NumberToWords.php — the "manager" and the only entry point you need 95% of the time: getNumberTransformer($locale), getCurrencyTransformer($locale), plus the static transformNumber() / transformCurrency() shortcuts.
  3. src/Locale/<Lang>/… — the actual transformers. These are your reference implementation when output looks wrong.
  4. tests/Locale/… — golden strings for every language. When a stakeholder says "that's not how we say 1,234 in Polish", the repo's own test data is the fastest way to confirm whether you found a bug or a misunderstanding.

First use case: "total in words" on an invoice

use NumberToWords\NumberToWords;

$manager = new NumberToWords();

$manager->getNumberTransformer('en')->toWords(5120);
// "five thousand one hundred twenty"

// Amounts are MINOR UNITS (cents) and integers only.
$manager->getCurrencyTransformer('en')->toWords(5099, 'USD');
// "fifty dollars ninety nine cents"

That two-liner is the whole library. Everything below is about making it safe inside a Laravel app.

Implementation Patterns

Wrap it in one injectable service (do this on day one)

Creating transformers per call is cheap but pointless churn, and it scatters locale handling across your codebase. Bind a singleton and memoize per locale:

namespace App\Support;

use NumberToWords\NumberToWords;
use NumberToWords\Transformer\NumberTransformer;
use NumberToWords\Transformer\CurrencyTransformer;

class MoneyWords
{
    private NumberToWords $manager;
    /** @var array<string, NumberTransformer> */
    private array $numbers = [];
    /** @var array<string, CurrencyTransformer> */
    private array $currencies = [];

    public function __construct()
    {
        $this->manager = new NumberToWords();
    }

    public function number(int $value, ?string $locale = null): string
    {
        $locale = $this->locale($locale);

        return ($this->numbers[$locale] ??= $this->manager->getNumberTransformer($locale))
            ->toWords($value);
    }

    /** @param int $minorUnits amount in cents */
    public function currency(int $minorUnits, string $currency, ?string $locale = null): string
    {
        $locale = $this->locale($locale);

        return ($this->currencies[$locale] ??= $this->manager->getCurrencyTransformer($locale))
            ->toWords($minorUnits, $currency);
    }

    private function locale(?string $locale): string
    {
        $locale ??= app()->getLocale();

        return config('number-to-words.locales')[$locale]
            ?? config('number-to-words.fallback', 'en');
    }
}
// AppServiceProvider::register()
$this->app->singleton(\App\Support\MoneyWords::class);

Map Laravel locales to package locales in config

The package uses RFC 3066-ish identifiers that do not always match Laravel's:

// config/number-to-words.php
return [
    'fallback' => 'en',
    'locales' => [
        'en'    => 'en',
        'pl'    => 'pl',
        'de'    => 'de',
        'fr'    => 'fr',
        'fr_FR' => 'fr_FR',   // alias for fr
        'uk'    => 'ua',      // Ukrainian — NOT "uk"
        'da'    => 'dk',      // Danish — NOT "da"
        'pt_BR' => 'pt_BR',
        'pt_PT' => 'pt_PT',
    ],
];

Model accessor for documents

protected function totalInWords(): Attribute
{
    return Attribute::get(fn () => app(MoneyWords::class)->currency(
        (int) round($this->total * 100),
        $this->currency,
        $this->locale,
    ));
}

Append it in the API resource rather than the model so PDFs and JSON stay in sync. For generated invoices/PDFs, persist the string at issue time in a total_in_words column: the wording then never changes retroactively when you bump the library version.

Blade directive

Blade::directive('words', fn ($expr) =>
    "<?php echo e(app(\App\Support\MoneyWords::class)->number((int) ($expr))); ?>");
<p>@words($invoice->total_cents / 100)</p>

Prefer passing an already-integer value; the cast above is a guard rail, not a substitute for rounding.

Validation rule for supported combos

Since currency support is per-language, validate up front instead of throwing at render time:

class CurrencyInWordsSupported implements ValidationRule
{
    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        $currency = strtoupper((string) $value);

        if (! in_array($currency, config('number-to-words.supported.'.$this->locale, []), true)) {
            $fail("Currency {$currency} has no words transformer for {$this->locale}.");
        }
    }
}

Keep supported as a hand-maintained list derived from the README table, and add a feature test per currency you actually ship.

Custom or missing language

Implement the package interfaces — they are typed int in v2+:

use NumberToWords\Transformer\NumberTransformer;

class KlingonNumberTransformer implements NumberTransformer
{
    public function toWords(int $number): string { /* ... */ }
}

Then inject it directly, or subclass NumberToWords and extend its locale→class mapping (see how the manager resolves locales in src/NumberToWords.php). For a language that has numbers but no currency transformer, extend the existing currency transformer and add your currency names — don't fork the package.

Gotchas and Tips

  • Currency input is minor units, always an integer. 5099 means $50.99. Multiply, but round: 45.67 * 100 is 4566.999… in floating point, so (int) ($x * 100) silently undercharges by a cent in words. Use (int) round($x * 100).
  • Floats are rejected. The v2+ interfaces typehint int; passing 45.67 throws a TypeError (or triggers a deprecation on older paths). Cast at the boundary, never inside a loop.
  • Never feed formatted numbers. Strip thousands separators, spaces and locale decimal commas before conversion — the transformers expect a raw integer.
  • Locale code mismatches are the #1 production bug. uk/ua, da/dk, and fr_FR (the only alias in the library) all need mapping. An unmapped locale either throws or silently falls back, depending on how you wrap it — so wrap it explicitly, as in the service above.
  • Currency coverage varies by language. 51 currencies are supported across currency transformers as of v3, but the language still gates it: fr_BE, cs, nl, et, it, ku, mk, fa, sv have no currency transformer at all. Falling back to en for a Swedish invoice produces mixed-language output — decide deliberately (fallback vs. numeric-only display) and test it.
  • Negative and zero amounts are historically the buggy paths. German negatives were fixed in 2.9.1, Romanian currency negatives in 3.0.0, Yoruba zero returned '' before 3.0.0. For refunds/credit notes, prefer abs() plus your own localized "minus"/"minusem" prefix, and never treat an empty string as "zero is fine".
  • Pin the major version and re-test on upgrade. 3.0.0 renamed MalaysianCurrencyTransformer → MalayCurrencyTransformer and changed the Romanian code ROL → RON. If you referenced transformer classes directly (or stored ROL), that breaks.
  • Static helpers build a fresh manager per call. NumberToWords::transformNumber('en', 5120) is fine in a seeder or a one-off, wasteful inside a PDF loop over 500 line items. Use the singleton service.
  • Huge numbers aren't guaranteed. Transformers work up to a limited magnitude (typically billions). Validate the upper bound for your domain — e.g. a max: rule on cents — rather than discovering a wrong output on a six-figure invoice.
  • Debugging workflow: log the raw integer + locale + currency before conversion; reproduce against the package's own test data for that locale; if the repo's tests disagree with your expectation, it's either a real bug (open an issue — the maintainer explicitly asks for PRs since he doesn't speak every language) or regional phrasing you should encode in your own test suite.
  • Golden-file tests are worth it. Put the exact expected strings for the 3–5 locales you actually ship into #[DataProvider] tests, mirroring the repo's style. Wording is business-visible; a silently changed connector word is a support ticket.
  • Don't localize what you don't need. If you only ever emit English invoices, skip the locale mapping layer entirely and call getCurrencyTransformer('en') directly — the abstraction earns its keep only on multi-language documents.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
abbasudo/artisan-route
mirvan/ippanel
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package