Implement formatMoney template function - #88
Open
drubtsov1 wants to merge 14 commits into
Open
Conversation
Format an amount together with its currency code, so a template can show
the price a customer actually paid without the author hard-coding a
currency sign.
${ formatMoney(Order.CustomField.Total, Order.CustomField.Currency) }
Two overloads by arity, the way substring is done: the two-argument form
uses the narrow symbol, the three-argument form takes a display mode of
narrowSymbol, symbol or code. An unsupported constant mode is a static
template error; a mode arriving through the model falls back to
narrowSymbol rather than failing the render.
Symbols, symbol placement, digit grouping and decimal places come from a
table rather than from RenderSettings.CultureInfo. The currency is a
property of the order while the culture is a property of the project, so
deriving the format from the render culture is what makes a shop's
emails show the wrong money today. .NET also exposes no way to map an
ISO 4217 code to a symbol without enumerating cultures, which is neither
stable across ICU versions nor available under invariant globalization.
Grouping follows the currency, since it is the only signal about the
reader: $1,234.56 but 1.234,56 € and 12,34,567.89 for the Indian lakh
grouping. Separators are plain spaces, not U+00A0, because the same
engine renders SMS and a non-breaking space forces UCS-2, halving the
segment length.
An unknown or absent currency code renders the amount followed by the
code as written, so an unmapped currency degrades instead of taking the
whole message down.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review pass over the currency table against CLDR, plus the edge cases a money formatter meets in production. Data: SGD's disambiguated symbol was invented (CLDR has no "S$"), ILS follows the amount in its own locale, and ARS/BRL/COP take a space after the symbol that the letter heuristic cannot express. Currencies outside the table now take their minor units from ISO 4217 instead of assuming two, so PKR no longer grows centimes and TND no longer loses a digit. Negative amounts put the sign before the symbol rather than between it and the digits, and an amount that rounds to zero is no longer rendered as a negative zero. Display mode is matched case-insensitively, like the currency code already was, and a null mode falls back like an unsupported one instead of failing the render. A null amount renders as zero: these fields are legitimately absent on orders without shipping or tax, and taking the message down over one of them is worse than showing zero. The number formats are precomputed per currency and made read-only, so the shared instances cannot be mutated by a later edit and each call stops allocating its format string. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Eleven currencies carried a non-default grouping style that no test touched, so their assignment could be changed without failing anything. Adds them at an amount large enough to show two group separators, which also pins their symbol placement. Display-mode casing is now asserted per mode rather than by inequality, and the round-to-zero rule is checked on a zero-decimal currency too. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The hand-written table held 42 currencies against the 161 Shopify accepts, so a merchant selling in naira or Pakistani rupees got the bare ISO code and, worse, invented decimals. The table is now generated from CLDR for all 152 usable Shopify codes, which also removes the hand-curation that produced an "S$" symbol CLDR has never had. Generating from one locale collapsed most of the model. In CLDR's en-001 every currency puts the symbol first and groups in thousands with a dot decimal, so per-currency grouping styles, symbol placement and the Indian lakh special case all disappear; what actually varies is the symbol, the number of decimals and whether a space follows the symbol. This also settles a question the previous table answered by accident. Formatting now follows the language the email is written in rather than each currency's home locale, so a Vietnamese amount reads as "₫1,731,200" in an English message instead of "1.731.200 ₫". That matches the acceptance examples in the effort card, which were written that way. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The table was written by hand, which is how it ended up claiming an "S$" symbol that exists in no CLDR locale and giving the forint two decimal places it lost in 1999. It is now generated from the Unicode CLDR data packages, pinned by version in tools/currency-table. The generated file stays committed so builds need no network and every data change arrives as a reviewable diff. CI re-runs the generator and fails if the committed file differs, so the table can neither be edited by hand nor drift from CLDR unnoticed; refreshing after a CLDR release is a version bump and a rerun. Taking the data straight from CLDR also fixes what the hand-written version got wrong: fourteen currencies, the forint and the Pakistani rupee among them, no longer show a fractional part they do not have. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The generator removed the hand-maintenance of the table itself, but the pinned CLDR version still had to be bumped by someone remembering to. Dependabot now does it monthly, and the pin stays because it is what makes the CI regeneration check deterministic and what keeps a CLDR release — which moves the money shown in every tenant's emails — from arriving inside an unrelated pull request. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Renovate is the standard here, not Dependabot, and this repository has neither — Renovate was offered in 2020 and the pull request was closed. Turning on a dependency bot is the maintainers' call, not something to slip into a pull request about formatting money. The CLDR bump stays manual for now; the CI check is what keeps the table from drifting. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The generator took the currency table off hand-maintenance, but the pinned CLDR version still needed a human to remember it. Renovate is the standard across the organisation and already has a shared preset here, which sibling packages such as Mindbox.Analyzers and data-linq extend. This repository's onboarding pull request was closed in 2020, but that was not a lasting decision: data-linq closed the same evening's invitation and later opted in through a config file, which is the path Renovate's own bot documented in the closed pull request. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The generator was fed a checked-in list of the currency codes Shopify accepts, which quietly reintroduced the hand-maintenance the generator removed: a currency added by any payment provider would be absent from the table and render without a symbol and with two decimal places it may not have. The list is gone. All 307 currencies CLDR carries are generated, which costs twelve kilobytes of source and covers anything ISO 4217 defines, including currencies that do not exist yet. It also fits the engine better — it renders templates for every integration, and had no business knowing which currencies one of them happens to support. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The workflow pointed at tools/currency-table while git records the directory as Tools/currency-table, next to the existing Tools/ holding the ANTLR jar. A case-insensitive macOS filesystem hid this locally and the step failed on Linux before anything else could run, which is also why the test report found no results. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The lockfile was generated on a machine configured against the internal Artifactory mirror, so its resolved URLs pointed there and CI could not authenticate. The tool now pins the public registry and the lockfile is regenerated against it, which is also the right source for an Apache-licensed package built on a public runner. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Installing the packages pulled forty megabytes of every locale on earth to read one hundred and forty kilobytes, and brought a lockfile, a registry and an authentication failure with it — the lockfile was resolved against the internal Artifactory mirror, which a public runner cannot reach. The two files are now copied verbatim from the CLDR release named in cldr/VERSION, the generator needs nothing but Node, and the build no longer touches the network. Refreshing is two curl commands documented next to the script. The Renovate config goes with it: it was added to bump the CLDR pin, and there is no longer a pin to bump. Whether this repository wants a dependency bot for its other packages is a decision for its maintainers. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The mode arrives as a string because the template language has no other way to spell it, and because it may come from the model rather than a literal. That is fine at the boundary; carrying the string into the formatter was not. Every call compared it up to three times, and the set of valid modes was spelled out twice — once in the validator and once in the formatter — which is two places to disagree. It is now parsed at the boundary into an enum, and the validator asks the same parser the formatter uses. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The hand-rolled table was built from CLDR because I believed no library
carried the disambiguated symbols the card asks for. That was wrong: I
had only read NodaMoney's Symbol property and missed InternationalSymbol
sitting next to it, which yields US$, S$, CA$ and NT$ directly. CLDR
cannot produce S$ at all — it falls back to the SGD code — so the table
failed the card's own acceptance example while the library meets it.
Decimals move from CLDR's display digits to ISO 4217 minor units. That
gives HUF, IDR, PKR and COP two decimals rather than none, which matches
what Shopify shows: its checkout always renders decimal places, and it
does not list those currencies as zero-decimal.
Arabic-script currencies now render their native RTL symbol where the
table substituted the ISO code. Left as is on purpose — a rule guarding
them would have to test for RTL script, and the obvious spelling of it
("symbol is not Latin") would also catch the won, baht and rupee. The
display mode already lets a template ask for the code instead.
Drops the generator, the vendored CLDR payload and the CI drift check.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds
formatMoney(amount, currencyCode)andformatMoney(amount, currencyCode, displayMode)to the standard function library.Currency data comes from NodaMoney. It deliberately does not go through
RenderSettings.CultureInfothe wayformatDecimaldoes: the currency belongs to the order, not to the project, so project culture is the wrong input here.Display modes:
narrowSymbol(default)$1,234.56,Ft 3,075.00symbolUS$1,234.56,S$1,234.56codeUSD 1,234.56Decimal places follow ISO 4217 minor units, so JPY renders without decimals and KWD with three. This matches what Shopify shows a shopper at checkout, verified against live checkouts in nine currencies.
An unrecognised currency code falls back to the amount followed by the uppercased code, so a template never renders empty. Separators are plain spaces, keeping SMS inside the GSM alphabet.