Skip to content

feat: add CurrencyTransformer formatter#4

Merged
janicduplessis merged 1 commit intomainfrom
@janic/currency-transformer
Apr 25, 2026
Merged

feat: add CurrencyTransformer formatter#4
janicduplessis merged 1 commit intomainfrom
@janic/currency-transformer

Conversation

@janicduplessis
Copy link
Copy Markdown
Contributor

Description

Adds a built-in currency formatter to the library, alongside the existing PatternTransformer and PhoneNumberTransformer. Configured per-instance with currency (ISO 4217 code) and an optional locale (BCP 47 tag). The transformer worklet uses Intl.NumberFormat (Hermes ships with Intl by default on iOS and Android) to drive locale-aware formatting — separators, symbol position, and fraction-digit count are all derived from the currency/locale combination rather than hardcoded.

The input model is cents-focused: the user types digits only, and the last N (where N = Intl.NumberFormat.resolvedOptions().maximumFractionDigits) are treated as the fractional part. So with USD a user types `1234567` and sees `$12,345.67`; with JPY (zero decimals) they see `¥1,234,567`; with EUR (`de-DE`) they see `12.345,67 €` with the suffix symbol.

Solution

CurrencyTransformer extends Transformer. The constructor pre-resolves maximumFractionDigits once via Intl, and the worklet captures currency/locale strings via closure. Intl.NumberFormat instances are cached per (locale, currency) on the worklet runtime's `globalThis` to avoid reconstructing on every keystroke.

The interesting work is cursor placement, which has a few cases that all collapse into one rule. The cents model means leading zeros get stripped by `parseInt` and the formatter pads or shifts digits as the magnitude crosses boundaries — so the digit-position-from-start in raw doesn't match the digit-position-from-start in formatted. The cursor placement formula compensates:

```
targetDigit = cursorDigitIndex + (formattedDigitsCount − rawDigitsCount)
```

Then the cursor goes right after the `targetDigit`-th digit in the formatted output, with two snap rules: past the last digit → snap to right after the last digit (preserves the cents-accumulator UX and excludes any trailing symbol like ` €` from the typing area); before the first digit → snap to the first digit (skips prefix symbols). Backspace next to a thousands or decimal separator drops the digit before the cursor instead of the separator (the formatter would just re-add the separator otherwise — silent no-op without this branch).

The example app's Currency card now uses this built-in instead of the inline transformer it had previously, and switches between USD / EUR / JPY by changing the `transformer` prop. Three module-scope instances, no shared mutable state.

Test plan

The library's jest suite covers the new code (`yarn test` — 33 tests in `CurrencyTransformer.test.ts`, all green; 73 pre-existing tests unchanged). The interesting cases reviewers might want to spot-check:

  • Run the example app, open the Currency card.
  • USD: type `1234567`, expect `$12,345.67` with cursor at the end. Switch to EUR — value reformats to `12.345,67 €` with cursor right after the `7` (before the trailing space + €). Switch to JPY — `¥12,345.67` becomes `¥1,234,567` (no decimals, all whole units).
  • Type `1` from empty: expect `$0.01` cursor at end.
  • Type `1` between `0` and `.` of `$0.23`: expect `$1.23` cursor between `1` and `.` (right after the typed digit).
  • Backspace next to the decimal point or thousands separator: expect the digit before the cursor to disappear (visible deletion, since deleting just the separator would silently re-format the same value).

Adds a built-in currency formatter (cents-focused input formatted via
Intl.NumberFormat) alongside the existing pattern and phone-number
formatters. Configured per-instance with `currency` (ISO 4217) and
optional `locale` (BCP 47); the worklet caches Intl.NumberFormat
instances on the worklet runtime.

The example app's Currency card now uses this built-in instead of an
inline transformer, and demonstrates switching between USD/EUR/JPY by
changing the `transformer` prop.
@janicduplessis janicduplessis merged commit 17aec15 into main Apr 25, 2026
5 checks passed
@janicduplessis janicduplessis deleted the @janic/currency-transformer branch April 25, 2026 20:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant