The field refused an amount a tester had copied out of a finance spreadsheet into EUR. On screen the value looked perfect.
The minus was U+2212, the real typographic minus, and not the ASCII hyphen our parser expected. A spreadsheet writes it. So does a Japanese keyboard.
We fixed that one character and then got a second report, this time from a right-to-left locale: a value copied straight back out of our own formatted output failed too. Intl.NumberFormat writes bidi marks around a number in RTL text. They are invisible. They were in the clipboard.
Two reports, same shape. The paste boundary was doing nothing, so the parser kept getting handed text no human could see was dirty.
What we do now, in our open-source amountfield: there is exactly one place where a raw string becomes a candidate number, and it removes exactly four things.
Bidi marks go, because our own renderer put them there.
Unicode minus and dash characters are folded into an ASCII hyphen, including the case where the locale's own minus is not a hyphen either.
The field's own currency marks go. We ask Intl for symbol, narrowSymbol and code, keep every part the formatter labels as currency, and sort them longest first, so stripping CFA cannot leave a stray F standing in the number. That list is memoized per locale and currency code, because it is the same answer on every keystroke.
Leading whitespace goes, since a space in front of digits reads as a group separator with nothing before it.
Everything else survives. A space inside the number survives, because that is grouping and not noise. The locale's own digits survive, because parsing already reads them.
What took us longest to agree on was the part we refuse. We do not read the currency out of the text. Paste a dollar amount into a EUR field and the dollar sign stays in the string and comes back as a not-a-number failure. The other option is a field that quietly decides 1,200 dollars were 1,200 euros, and nobody finds out until reconciliation.
The caret has to survive all of this too, so cleaning also returns a mapping of the old caret position onto the cleaned text. Otherwise a paste that fixes your number moves your cursor. And a parse failure mid-typing is marked incomplete rather than invalid, because a trailing separator is unfinished and not wrong.
Clean the characters you authored. Refuse the ambiguity you did not. That same line sits somewhere in your own input handling, either repairing the pasted value or handing it back, and we think it pays to know which one you picked.