Skip to content

Proposal: native Intl currency and percent formatting via a format prop #3

Description

@Amanfromearth

Opening this first as CONTRIBUTING.md asks ("For pull requests that change the API or
implementation, discuss with maintainers first by opening an issue"). Feature requests are routed
to Discussions in .github/ISSUE_TEMPLATE/config.yml, but Discussions is not enabled on the repo,
so an issue seemed the only channel available. Happy to move this anywhere you prefer.

The gap

NumericText formats through locale, useGrouping, minimumFractionDigits and
maximumFractionDigits. There is no way to render money, which is the case the README's own
opening line names first: "counters, balances, prices, scores, dashboards".

The proposal

One format prop shaped like Intl.NumberFormatOptions, resolved natively on each side:
NumberFormatter on iOS, android.icu on Android, Intl on web.

<NumericText value={total} currency="USD" />                    // $1,234.50
<NumericText value={total} locale="de-DE" currency="EUR" />     // 1.234,50 €
<NumericText value={rate} format={{ style: 'percent' }} />      // 42%

The existing four props stay as shorthands, so it is additive.

The shape is borrowed from number-flow, which answers
the same question on the web with one options object rather than a growing row of flat props.

Two things it forces, which are the parts worth your opinion

1. The affix has to be keyed structurally. TransitionLogic keys a non-digit token by its
offset in the string (O$i), so $999$1,000 would kill the $ at offset 0 and create a new
one at offset 0: a born/died cycle where it should slide. The fix is to count outward from the
nearest end of the number, P0 before the first digit and X0 after the last. Unreachable today,
since a plain grouped decimal only produces digits, group and decimal marks, and a sign.

2. The bundled font has no currency glyphs. The subset carries digits, separators and signs
only: no $, no , no letters. A currency format would drive canRender into falling the whole
line back to the platform font, silently losing the rounded face. Regenerating with
.agent/tools/subset_font.sh costs ~97 KB → ~300 KB across the nine weights. Digit and separator
advances come out unchanged to four decimals, so the measureBox constants still hold, but the
size increase is a real trade and yours to accept or reject.

There is also a rounding question: Intl rounds halves away from zero, NumberFormatter and ICU
both round half-to-even, so 2.5 at zero decimals currently reads 3 on web, 2 on iOS and 2
on Android. I think that should be pinned to Intl's rule on all three rather than exposed as an
option, but it is a behaviour change to existing output.

Status

I have this implemented and verified (39 JS tests, 25 Kotlin tests, compileDebugKotlin, a full
iOS xcodebuild, and both renderers driven by hand on a simulator through every format). Happy to
open the PR, or to drop it entirely if the direction is wrong for the library. It is not
verified against a ground-truth recording, so the affix's motion during a carry is reasoned-about
rather than measured, which I know is not the bar .agent/AGENTS.md sets.

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