Skip to content

feat(core): let the currency option take a vocab object, not just a code #437

Description

@forzagreen

Follow-up to #428, non-breaking, for after v6.0.0.

The problem

src/utils/currency-vocab.js exports one object literal per language, and a language file
reads it by string key:

const { major, minor } = CURRENCY_VOCAB[currency]
export const currencyValues = { currency: Object.keys(CURRENCY_VOCAB) }

Both pin the whole object. Terser drops unused bindings but never unused properties, so
a page that only ever spells dollars still ships all 24 English currencies:

raw gzip
all 24 en currencies 1,418 B 347 B
USD alone 63 B 70 B

≈ 277 B gzipped of vocabulary nobody asked for. Credit to @TylerVigario, who measured
this first on proto/form-split and pointed out that it's the same rule
docs/currency-vocab.md already applies one level up (keying by language rather than by
ISO code), just not carried one level further down.

Why #428 doesn't fix it

Per-currency named exports and string-keyed selection are mutually exclusive. A switch,
or a code → binding map, still references every binding — nothing is dropped. Only making
the option value be the vocab object tree-shakes, and that would break
{ currency: row.currency } (an invoicing app picks from data, not from identifiers) along
with the currencyValues enum and currency-vocab-contract.test.js.

That's a real cost, so #428 keeps the string API and pays the 277 B.

The proposal

Accept both shapes for the same option.

// today, and still the default — validated, works from data
toCurrency(42.50, { currency: 'GBP' })

// new — tree-shakes to one currency
import { GBP } from 'n2words/en/currencies'
toCurrency(42.50, { currency: GBP })

A string keeps its RangeError-on-typo contract against currencyValues. An object is
opaque to the enum but statically resolved, so a typo is a link-time error instead — the
stronger guarantee of the two, just enforced somewhere else.

Open questions

  • Where do per-currency bindings live? A new src/currencies/{lang}.js per language, or
    named exports beside the existing map in currency-vocab.js?
  • How does resolveOptions validate a non-enum value for an option that also has an enum?
    The <form>Values contract currently assumes one allowed set.
  • Does each vocab object carry its own code, so assertCurrencyExponent and
    minorUnitDigits still work without a separate lookup?
  • What does currency-vocab-contract.test.js assert about the object path?

Acceptance

@TylerVigario — offered to you if you'd like it, since you measured it and this is the part
of proto/form-split that survives keeping the string API. Happy to take it otherwise.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions