@automattic/number-formatters 1.1.9 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/dist/cjs/create-number-formatters.cjs +7 -0
- package/dist/cjs/index.cjs +2 -2
- package/dist/cjs/number-format-currency/index.cjs +53 -27
- package/dist/esm/create-number-formatters.js +7 -0
- package/dist/esm/index.js +1 -1
- package/dist/esm/number-format-currency/index.js +53 -27
- package/dist/types/create-number-formatters.d.ts +18 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/number-format-currency/index.d.ts +4 -2
- package/dist/types/types.d.ts +25 -2
- package/package.json +3 -3
- package/src/create-number-formatters.ts +33 -1
- package/src/index.ts +1 -0
- package/src/number-format-currency/index.ts +66 -25
- package/src/types.ts +26 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,10 +4,22 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|
6
6
|
|
|
7
|
+
## [1.2.0] - 2026-05-25
|
|
8
|
+
### Added
|
|
9
|
+
- Currency formatting: Add `setCurrencyOverrides` for installing a dynamic per-currency override map (e.g. from the WordPress.com currencies endpoint). Falls back to the hard-coded smallest-unit exponent overrides when not called. [#49016]
|
|
10
|
+
|
|
11
|
+
## [1.1.10] - 2026-05-21
|
|
12
|
+
### Changed
|
|
13
|
+
- Update package dependencies. [#49012]
|
|
14
|
+
|
|
7
15
|
## [1.1.9] - 2026-05-19
|
|
8
16
|
### Fixed
|
|
9
17
|
- Currency formatting: use ISO 4217 minor-unit exponent for smallest-unit conversion to correctly format IDR and other currencies where browser ICU disagrees with ISO 4217. [#48967]
|
|
10
18
|
|
|
19
|
+
## [1.1.8] - 2026-05-19
|
|
20
|
+
### Changed
|
|
21
|
+
- Internal updates.
|
|
22
|
+
|
|
11
23
|
## [1.1.7] - 2026-05-04
|
|
12
24
|
### Changed
|
|
13
25
|
- Internal: No longer require automattic/jetpack-changelogger as a per-project dev dependency. [#48225]
|
|
@@ -144,6 +156,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|
|
144
156
|
- Initial release
|
|
145
157
|
- Basic number formatting functionality
|
|
146
158
|
|
|
159
|
+
[1.2.0]: https://github.com/Automattic/number-formatters/compare/1.1.10...1.2.0
|
|
160
|
+
[1.1.10]: https://github.com/Automattic/number-formatters/compare/1.1.9...1.1.10
|
|
161
|
+
[1.1.9]: https://github.com/Automattic/number-formatters/compare/1.1.8...1.1.9
|
|
147
162
|
[1.1.8]: https://github.com/Automattic/number-formatters/compare/1.1.7...1.1.8
|
|
148
163
|
[1.1.7]: https://github.com/Automattic/number-formatters/compare/1.1.6...1.1.7
|
|
149
164
|
[1.1.6]: https://github.com/Automattic/number-formatters/compare/1.1.5...1.1.6
|
|
@@ -10,6 +10,7 @@ const number_format_ts_1 = require("./number-format.cjs");
|
|
|
10
10
|
function createNumberFormatters() {
|
|
11
11
|
let localeState;
|
|
12
12
|
let geoLocationState;
|
|
13
|
+
let currencyOverridesState;
|
|
13
14
|
const setLocale = (locale) => {
|
|
14
15
|
/**
|
|
15
16
|
* The `Intl.NumberFormat` constructor fails only when there is a variant, divided by `_`.
|
|
@@ -18,6 +19,9 @@ function createNumberFormatters() {
|
|
|
18
19
|
*/
|
|
19
20
|
localeState = locale;
|
|
20
21
|
};
|
|
22
|
+
const setCurrencyOverrides = (overrides) => {
|
|
23
|
+
currencyOverridesState = overrides;
|
|
24
|
+
};
|
|
21
25
|
/**
|
|
22
26
|
* Returns the locale defined on the module instance (through `setLocale`)
|
|
23
27
|
* or the "fallback locale" if no locale has been set.
|
|
@@ -80,6 +84,7 @@ function createNumberFormatters() {
|
|
|
80
84
|
signForPositive,
|
|
81
85
|
geoLocation: geoLocationState,
|
|
82
86
|
forceLatin,
|
|
87
|
+
currencyOverrides: currencyOverridesState,
|
|
83
88
|
});
|
|
84
89
|
};
|
|
85
90
|
const getCurrencyObject = (number, currency, { stripZeros = false, isSmallestUnit = false, signForPositive = false, forceLatin = true } = {}) => {
|
|
@@ -92,11 +97,13 @@ function createNumberFormatters() {
|
|
|
92
97
|
signForPositive,
|
|
93
98
|
geoLocation: geoLocationState,
|
|
94
99
|
forceLatin,
|
|
100
|
+
currencyOverrides: currencyOverridesState,
|
|
95
101
|
});
|
|
96
102
|
};
|
|
97
103
|
return {
|
|
98
104
|
setLocale,
|
|
99
105
|
setGeoLocation,
|
|
106
|
+
setCurrencyOverrides,
|
|
100
107
|
formatNumber,
|
|
101
108
|
formatNumberCompact,
|
|
102
109
|
formatCurrency,
|
package/dist/cjs/index.cjs
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.createNumberFormatters = exports.getCurrencyObject = exports.formatCurrency = exports.formatNumberCompact = exports.formatNumber = exports.setGeoLocation = exports.setLocale = void 0;
|
|
3
|
+
exports.createNumberFormatters = exports.getCurrencyObject = exports.formatCurrency = exports.formatNumberCompact = exports.formatNumber = exports.setCurrencyOverrides = exports.setGeoLocation = exports.setLocale = void 0;
|
|
4
4
|
const tslib_1 = require("tslib");
|
|
5
5
|
const create_number_formatters_ts_1 = tslib_1.__importDefault(require("./create-number-formatters.cjs"));
|
|
6
6
|
exports.createNumberFormatters = create_number_formatters_ts_1.default;
|
|
7
7
|
const defaultFormatter = (0, create_number_formatters_ts_1.default)();
|
|
8
|
-
exports.setLocale = defaultFormatter.setLocale, exports.setGeoLocation = defaultFormatter.setGeoLocation, exports.formatNumber = defaultFormatter.formatNumber, exports.formatNumberCompact = defaultFormatter.formatNumberCompact, exports.formatCurrency = defaultFormatter.formatCurrency, exports.getCurrencyObject = defaultFormatter.getCurrencyObject;
|
|
8
|
+
exports.setLocale = defaultFormatter.setLocale, exports.setGeoLocation = defaultFormatter.setGeoLocation, exports.setCurrencyOverrides = defaultFormatter.setCurrencyOverrides, exports.formatNumber = defaultFormatter.formatNumber, exports.formatNumberCompact = defaultFormatter.formatNumberCompact, exports.formatCurrency = defaultFormatter.formatCurrency, exports.getCurrencyObject = defaultFormatter.getCurrencyObject;
|
|
9
9
|
// We can optionally export the formatters individually if we want to use them in a more granular way.
|
|
10
10
|
// export { numberFormat, numberFormatCompact, numberFormatCurrency, getCurrencyObject };
|
|
@@ -9,26 +9,38 @@ const currencies_ts_1 = require("./currencies.cjs");
|
|
|
9
9
|
const debug = (0, debug_1.default)('number-formatters:number-format-currency');
|
|
10
10
|
/**
|
|
11
11
|
* Retrieves the currency override for a given currency.
|
|
12
|
+
*
|
|
12
13
|
* If the currency is USD and the user is not in the US, it will return `US$`.
|
|
13
|
-
*
|
|
14
|
-
*
|
|
14
|
+
*
|
|
15
|
+
* Per-field merge order is: dynamic overrides (from `currencyOverrides`) → hard-coded defaults.
|
|
16
|
+
* This means a caller can supply a partial map (eg: only `decimal`) without losing the
|
|
17
|
+
* default `symbol`.
|
|
18
|
+
* @param currency - The currency to get the override for.
|
|
19
|
+
* @param geoLocation - The geo location of the user.
|
|
20
|
+
* @param currencyOverrides - Dynamic per-currency overrides supplied by the host application.
|
|
15
21
|
* @return {CurrencyOverride | undefined} The currency override.
|
|
16
22
|
*/
|
|
17
|
-
function getCurrencyOverride(currency, geoLocation) {
|
|
23
|
+
function getCurrencyOverride(currency, geoLocation, currencyOverrides) {
|
|
18
24
|
if (currency === 'USD' && geoLocation && geoLocation !== '' && geoLocation !== 'US') {
|
|
19
|
-
return { symbol: 'US$' };
|
|
25
|
+
return { symbol: 'US$', ...currencyOverrides?.USD };
|
|
26
|
+
}
|
|
27
|
+
const defaultOverride = currencies_ts_1.defaultCurrencyOverrides[currency];
|
|
28
|
+
const dynamicOverride = currencyOverrides?.[currency];
|
|
29
|
+
if (!defaultOverride && !dynamicOverride) {
|
|
30
|
+
return undefined;
|
|
20
31
|
}
|
|
21
|
-
return
|
|
32
|
+
return { ...defaultOverride, ...dynamicOverride };
|
|
22
33
|
}
|
|
23
34
|
/**
|
|
24
35
|
* Returns a valid currency code based on a shortlist of currency codes.
|
|
25
36
|
* Only currencies from the shortlist are allowed. Everything else will fall back to `FALLBACK_CURRENCY`.
|
|
26
|
-
* @param currency
|
|
27
|
-
* @param geoLocation
|
|
37
|
+
* @param currency - The currency to get the valid currency for.
|
|
38
|
+
* @param geoLocation - The geo location of the user.
|
|
39
|
+
* @param currencyOverrides - Dynamic per-currency overrides supplied by the host application.
|
|
28
40
|
* @return {string} The valid currency.
|
|
29
41
|
*/
|
|
30
|
-
function getValidCurrency(currency, geoLocation) {
|
|
31
|
-
if (!getCurrencyOverride(currency, geoLocation)) {
|
|
42
|
+
function getValidCurrency(currency, geoLocation, currencyOverrides) {
|
|
43
|
+
if (!getCurrencyOverride(currency, geoLocation, currencyOverrides)) {
|
|
32
44
|
debug(`getValidCurrency was called with a non-existent currency "${currency}"; falling back to ${constants_ts_1.FALLBACK_CURRENCY}`);
|
|
33
45
|
return constants_ts_1.FALLBACK_CURRENCY;
|
|
34
46
|
}
|
|
@@ -85,9 +97,14 @@ function getCurrencyFormatter({ number, currency, browserSafeLocale, forceLatin
|
|
|
85
97
|
});
|
|
86
98
|
}
|
|
87
99
|
/**
|
|
88
|
-
*
|
|
100
|
+
* Hard-coded smallest-unit exponent overrides for currencies where browser ICU's
|
|
89
101
|
* `maximumFractionDigits` disagrees with the API's smallest-unit encoding.
|
|
90
102
|
*
|
|
103
|
+
* This list exists as a safety net for callers that have not yet wired up the
|
|
104
|
+
* dynamic `currencyOverrides` path (eg: the WPCOM currencies endpoint). Once a
|
|
105
|
+
* host application provides overrides via `setCurrencyOverrides`, those take
|
|
106
|
+
* precedence on a per-currency basis.
|
|
107
|
+
*
|
|
91
108
|
* Keep this list minimal — the backend is the source of truth for the API's
|
|
92
109
|
* smallest-unit encoding, so adding speculative entries here risks silent
|
|
93
110
|
* drift. Only add a currency once we've verified that browsers report a
|
|
@@ -103,14 +120,20 @@ const SMALLEST_UNIT_EXPONENT_OVERRIDES = {
|
|
|
103
120
|
/**
|
|
104
121
|
* Returns the smallest unit exponent for a currency.
|
|
105
122
|
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
* @param
|
|
111
|
-
* @
|
|
123
|
+
* Lookup order:
|
|
124
|
+
* 1. The dynamic `currencyOverrides[currency].decimal` if a host application has supplied one (typically via `setCurrencyOverrides`).
|
|
125
|
+
* 2. The hard-coded `SMALLEST_UNIT_EXPONENT_OVERRIDES` map.
|
|
126
|
+
* 3. The browser-derived display precision (`fallback`).
|
|
127
|
+
* @param currency - The currency code (ISO 4217)
|
|
128
|
+
* @param fallback - The browser-derived precision to use when no override applies
|
|
129
|
+
* @param currencyOverrides - Dynamic per-currency overrides supplied by the host application
|
|
130
|
+
* @return number - The smallest unit exponent
|
|
112
131
|
*/
|
|
113
|
-
function getSmallestUnitExponent(currency, fallback) {
|
|
132
|
+
function getSmallestUnitExponent(currency, fallback, currencyOverrides) {
|
|
133
|
+
const dynamicDecimal = currencyOverrides?.[currency]?.decimal;
|
|
134
|
+
if (typeof dynamicDecimal === 'number') {
|
|
135
|
+
return dynamicDecimal;
|
|
136
|
+
}
|
|
114
137
|
return SMALLEST_UNIT_EXPONENT_OVERRIDES[currency] ?? fallback;
|
|
115
138
|
}
|
|
116
139
|
/**
|
|
@@ -151,9 +174,10 @@ function scaleNumberForPrecision(number, currencyPrecision) {
|
|
|
151
174
|
* @param currencyPrecision - The display precision (from the browser) to round the result to.
|
|
152
175
|
* @param currency - The currency code, used to look up any smallest-unit exponent override.
|
|
153
176
|
* @param isSmallestUnit - Whether the number is the smallest unit of a currency.
|
|
177
|
+
* @param currencyOverrides - Dynamic per-currency overrides supplied by the host application.
|
|
154
178
|
* @return {number} The prepared number.
|
|
155
179
|
*/
|
|
156
|
-
function prepareNumberForFormatting(number, currencyPrecision, currency, isSmallestUnit) {
|
|
180
|
+
function prepareNumberForFormatting(number, currencyPrecision, currency, isSmallestUnit, currencyOverrides) {
|
|
157
181
|
if (isNaN(number)) {
|
|
158
182
|
debug('formatCurrency was called with NaN');
|
|
159
183
|
return 0;
|
|
@@ -162,7 +186,7 @@ function prepareNumberForFormatting(number, currencyPrecision, currency, isSmall
|
|
|
162
186
|
if (!Number.isInteger(number)) {
|
|
163
187
|
debug('formatCurrency was called with isSmallestUnit and a float which will be rounded', number);
|
|
164
188
|
}
|
|
165
|
-
const smallestUnitDivisor = 10 ** getSmallestUnitExponent(currency, currencyPrecision);
|
|
189
|
+
const smallestUnitDivisor = 10 ** getSmallestUnitExponent(currency, currencyPrecision, currencyOverrides);
|
|
166
190
|
return scaleNumberForPrecision(Math.round(number) / smallestUnitDivisor, currencyPrecision);
|
|
167
191
|
}
|
|
168
192
|
return scaleNumberForPrecision(number, currencyPrecision);
|
|
@@ -204,16 +228,17 @@ function prepareNumberForFormatting(number, currencyPrecision, currency, isSmall
|
|
|
204
228
|
* @param params.signForPositive - Whether to show the sign for positive numbers.
|
|
205
229
|
* @param params.geoLocation - The geo location of the user.
|
|
206
230
|
* @param params.forceLatin - Whether to force the latin locale.
|
|
231
|
+
* @param params.currencyOverrides - Dynamic per-currency overrides supplied by the host application.
|
|
207
232
|
* @return {string} A formatted string.
|
|
208
233
|
*/
|
|
209
|
-
const numberFormatCurrency = ({ number, browserSafeLocale, currency, stripZeros, isSmallestUnit, signForPositive, geoLocation, forceLatin, }) => {
|
|
210
|
-
const validCurrency = getValidCurrency(currency, geoLocation);
|
|
211
|
-
const currencyOverride = getCurrencyOverride(validCurrency, geoLocation);
|
|
234
|
+
const numberFormatCurrency = ({ number, browserSafeLocale, currency, stripZeros, isSmallestUnit, signForPositive, geoLocation, forceLatin, currencyOverrides, }) => {
|
|
235
|
+
const validCurrency = getValidCurrency(currency, geoLocation, currencyOverrides);
|
|
236
|
+
const currencyOverride = getCurrencyOverride(validCurrency, geoLocation, currencyOverrides);
|
|
212
237
|
const currencyPrecision = getPrecisionForLocaleAndCurrency(browserSafeLocale, validCurrency, forceLatin);
|
|
213
238
|
if (isSmallestUnit && typeof currencyPrecision === 'undefined') {
|
|
214
239
|
throw new Error(`Could not determine currency precision for ${validCurrency} in ${browserSafeLocale}`);
|
|
215
240
|
}
|
|
216
|
-
const numberAsFloat = prepareNumberForFormatting(number, currencyPrecision ?? 0, validCurrency, isSmallestUnit);
|
|
241
|
+
const numberAsFloat = prepareNumberForFormatting(number, currencyPrecision ?? 0, validCurrency, isSmallestUnit, currencyOverrides);
|
|
217
242
|
const formatter = getCurrencyFormatter({
|
|
218
243
|
number: numberAsFloat,
|
|
219
244
|
currency: validCurrency,
|
|
@@ -280,13 +305,14 @@ exports.numberFormatCurrency = numberFormatCurrency;
|
|
|
280
305
|
* @param params.signForPositive - Whether to show the sign for positive numbers.
|
|
281
306
|
* @param params.geoLocation - The geo location of the user.
|
|
282
307
|
* @param params.forceLatin - Whether to force the latin locale.
|
|
308
|
+
* @param params.currencyOverrides - Dynamic per-currency overrides supplied by the host application.
|
|
283
309
|
* @return {CurrencyObject} A formatted string e.g. { symbol:'$', integer: '$99', fraction: '.99', sign: '-' }
|
|
284
310
|
*/
|
|
285
|
-
const getCurrencyObject = ({ number, browserSafeLocale, currency, stripZeros, isSmallestUnit, signForPositive, geoLocation, forceLatin, }) => {
|
|
286
|
-
const validCurrency = getValidCurrency(currency, geoLocation);
|
|
287
|
-
const currencyOverride = getCurrencyOverride(validCurrency, geoLocation);
|
|
311
|
+
const getCurrencyObject = ({ number, browserSafeLocale, currency, stripZeros, isSmallestUnit, signForPositive, geoLocation, forceLatin, currencyOverrides, }) => {
|
|
312
|
+
const validCurrency = getValidCurrency(currency, geoLocation, currencyOverrides);
|
|
313
|
+
const currencyOverride = getCurrencyOverride(validCurrency, geoLocation, currencyOverrides);
|
|
288
314
|
const currencyPrecision = getPrecisionForLocaleAndCurrency(browserSafeLocale, validCurrency, forceLatin);
|
|
289
|
-
const numberAsFloat = prepareNumberForFormatting(number, currencyPrecision ?? 0, validCurrency, isSmallestUnit);
|
|
315
|
+
const numberAsFloat = prepareNumberForFormatting(number, currencyPrecision ?? 0, validCurrency, isSmallestUnit, currencyOverrides);
|
|
290
316
|
const formatter = getCurrencyFormatter({
|
|
291
317
|
number: numberAsFloat,
|
|
292
318
|
currency: validCurrency,
|
|
@@ -8,6 +8,7 @@ import { numberFormat, numberFormatCompact } from "./number-format.js";
|
|
|
8
8
|
function createNumberFormatters() {
|
|
9
9
|
let localeState;
|
|
10
10
|
let geoLocationState;
|
|
11
|
+
let currencyOverridesState;
|
|
11
12
|
const setLocale = (locale) => {
|
|
12
13
|
/**
|
|
13
14
|
* The `Intl.NumberFormat` constructor fails only when there is a variant, divided by `_`.
|
|
@@ -16,6 +17,9 @@ function createNumberFormatters() {
|
|
|
16
17
|
*/
|
|
17
18
|
localeState = locale;
|
|
18
19
|
};
|
|
20
|
+
const setCurrencyOverrides = (overrides) => {
|
|
21
|
+
currencyOverridesState = overrides;
|
|
22
|
+
};
|
|
19
23
|
/**
|
|
20
24
|
* Returns the locale defined on the module instance (through `setLocale`)
|
|
21
25
|
* or the "fallback locale" if no locale has been set.
|
|
@@ -78,6 +82,7 @@ function createNumberFormatters() {
|
|
|
78
82
|
signForPositive,
|
|
79
83
|
geoLocation: geoLocationState,
|
|
80
84
|
forceLatin,
|
|
85
|
+
currencyOverrides: currencyOverridesState,
|
|
81
86
|
});
|
|
82
87
|
};
|
|
83
88
|
const getCurrencyObject = (number, currency, { stripZeros = false, isSmallestUnit = false, signForPositive = false, forceLatin = true } = {}) => {
|
|
@@ -90,11 +95,13 @@ function createNumberFormatters() {
|
|
|
90
95
|
signForPositive,
|
|
91
96
|
geoLocation: geoLocationState,
|
|
92
97
|
forceLatin,
|
|
98
|
+
currencyOverrides: currencyOverridesState,
|
|
93
99
|
});
|
|
94
100
|
};
|
|
95
101
|
return {
|
|
96
102
|
setLocale,
|
|
97
103
|
setGeoLocation,
|
|
104
|
+
setCurrencyOverrides,
|
|
98
105
|
formatNumber,
|
|
99
106
|
formatNumberCompact,
|
|
100
107
|
formatCurrency,
|
package/dist/esm/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import createNumberFormatters from "./create-number-formatters.js";
|
|
2
2
|
const defaultFormatter = createNumberFormatters();
|
|
3
|
-
export const { setLocale, setGeoLocation, formatNumber, formatNumberCompact, formatCurrency, getCurrencyObject, } = defaultFormatter;
|
|
3
|
+
export const { setLocale, setGeoLocation, setCurrencyOverrides, formatNumber, formatNumberCompact, formatCurrency, getCurrencyObject, } = defaultFormatter;
|
|
4
4
|
export { createNumberFormatters };
|
|
5
5
|
// We can optionally export the formatters individually if we want to use them in a more granular way.
|
|
6
6
|
// export { numberFormat, numberFormatCompact, numberFormatCurrency, getCurrencyObject };
|
|
@@ -5,26 +5,38 @@ import { defaultCurrencyOverrides } from "./currencies.js";
|
|
|
5
5
|
const debug = debugFactory('number-formatters:number-format-currency');
|
|
6
6
|
/**
|
|
7
7
|
* Retrieves the currency override for a given currency.
|
|
8
|
+
*
|
|
8
9
|
* If the currency is USD and the user is not in the US, it will return `US$`.
|
|
9
|
-
*
|
|
10
|
-
*
|
|
10
|
+
*
|
|
11
|
+
* Per-field merge order is: dynamic overrides (from `currencyOverrides`) → hard-coded defaults.
|
|
12
|
+
* This means a caller can supply a partial map (eg: only `decimal`) without losing the
|
|
13
|
+
* default `symbol`.
|
|
14
|
+
* @param currency - The currency to get the override for.
|
|
15
|
+
* @param geoLocation - The geo location of the user.
|
|
16
|
+
* @param currencyOverrides - Dynamic per-currency overrides supplied by the host application.
|
|
11
17
|
* @return {CurrencyOverride | undefined} The currency override.
|
|
12
18
|
*/
|
|
13
|
-
function getCurrencyOverride(currency, geoLocation) {
|
|
19
|
+
function getCurrencyOverride(currency, geoLocation, currencyOverrides) {
|
|
14
20
|
if (currency === 'USD' && geoLocation && geoLocation !== '' && geoLocation !== 'US') {
|
|
15
|
-
return { symbol: 'US$' };
|
|
21
|
+
return { symbol: 'US$', ...currencyOverrides?.USD };
|
|
22
|
+
}
|
|
23
|
+
const defaultOverride = defaultCurrencyOverrides[currency];
|
|
24
|
+
const dynamicOverride = currencyOverrides?.[currency];
|
|
25
|
+
if (!defaultOverride && !dynamicOverride) {
|
|
26
|
+
return undefined;
|
|
16
27
|
}
|
|
17
|
-
return
|
|
28
|
+
return { ...defaultOverride, ...dynamicOverride };
|
|
18
29
|
}
|
|
19
30
|
/**
|
|
20
31
|
* Returns a valid currency code based on a shortlist of currency codes.
|
|
21
32
|
* Only currencies from the shortlist are allowed. Everything else will fall back to `FALLBACK_CURRENCY`.
|
|
22
|
-
* @param currency
|
|
23
|
-
* @param geoLocation
|
|
33
|
+
* @param currency - The currency to get the valid currency for.
|
|
34
|
+
* @param geoLocation - The geo location of the user.
|
|
35
|
+
* @param currencyOverrides - Dynamic per-currency overrides supplied by the host application.
|
|
24
36
|
* @return {string} The valid currency.
|
|
25
37
|
*/
|
|
26
|
-
function getValidCurrency(currency, geoLocation) {
|
|
27
|
-
if (!getCurrencyOverride(currency, geoLocation)) {
|
|
38
|
+
function getValidCurrency(currency, geoLocation, currencyOverrides) {
|
|
39
|
+
if (!getCurrencyOverride(currency, geoLocation, currencyOverrides)) {
|
|
28
40
|
debug(`getValidCurrency was called with a non-existent currency "${currency}"; falling back to ${FALLBACK_CURRENCY}`);
|
|
29
41
|
return FALLBACK_CURRENCY;
|
|
30
42
|
}
|
|
@@ -81,9 +93,14 @@ function getCurrencyFormatter({ number, currency, browserSafeLocale, forceLatin
|
|
|
81
93
|
});
|
|
82
94
|
}
|
|
83
95
|
/**
|
|
84
|
-
*
|
|
96
|
+
* Hard-coded smallest-unit exponent overrides for currencies where browser ICU's
|
|
85
97
|
* `maximumFractionDigits` disagrees with the API's smallest-unit encoding.
|
|
86
98
|
*
|
|
99
|
+
* This list exists as a safety net for callers that have not yet wired up the
|
|
100
|
+
* dynamic `currencyOverrides` path (eg: the WPCOM currencies endpoint). Once a
|
|
101
|
+
* host application provides overrides via `setCurrencyOverrides`, those take
|
|
102
|
+
* precedence on a per-currency basis.
|
|
103
|
+
*
|
|
87
104
|
* Keep this list minimal — the backend is the source of truth for the API's
|
|
88
105
|
* smallest-unit encoding, so adding speculative entries here risks silent
|
|
89
106
|
* drift. Only add a currency once we've verified that browsers report a
|
|
@@ -99,14 +116,20 @@ const SMALLEST_UNIT_EXPONENT_OVERRIDES = {
|
|
|
99
116
|
/**
|
|
100
117
|
* Returns the smallest unit exponent for a currency.
|
|
101
118
|
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
* @param
|
|
107
|
-
* @
|
|
119
|
+
* Lookup order:
|
|
120
|
+
* 1. The dynamic `currencyOverrides[currency].decimal` if a host application has supplied one (typically via `setCurrencyOverrides`).
|
|
121
|
+
* 2. The hard-coded `SMALLEST_UNIT_EXPONENT_OVERRIDES` map.
|
|
122
|
+
* 3. The browser-derived display precision (`fallback`).
|
|
123
|
+
* @param currency - The currency code (ISO 4217)
|
|
124
|
+
* @param fallback - The browser-derived precision to use when no override applies
|
|
125
|
+
* @param currencyOverrides - Dynamic per-currency overrides supplied by the host application
|
|
126
|
+
* @return number - The smallest unit exponent
|
|
108
127
|
*/
|
|
109
|
-
function getSmallestUnitExponent(currency, fallback) {
|
|
128
|
+
function getSmallestUnitExponent(currency, fallback, currencyOverrides) {
|
|
129
|
+
const dynamicDecimal = currencyOverrides?.[currency]?.decimal;
|
|
130
|
+
if (typeof dynamicDecimal === 'number') {
|
|
131
|
+
return dynamicDecimal;
|
|
132
|
+
}
|
|
110
133
|
return SMALLEST_UNIT_EXPONENT_OVERRIDES[currency] ?? fallback;
|
|
111
134
|
}
|
|
112
135
|
/**
|
|
@@ -147,9 +170,10 @@ function scaleNumberForPrecision(number, currencyPrecision) {
|
|
|
147
170
|
* @param currencyPrecision - The display precision (from the browser) to round the result to.
|
|
148
171
|
* @param currency - The currency code, used to look up any smallest-unit exponent override.
|
|
149
172
|
* @param isSmallestUnit - Whether the number is the smallest unit of a currency.
|
|
173
|
+
* @param currencyOverrides - Dynamic per-currency overrides supplied by the host application.
|
|
150
174
|
* @return {number} The prepared number.
|
|
151
175
|
*/
|
|
152
|
-
function prepareNumberForFormatting(number, currencyPrecision, currency, isSmallestUnit) {
|
|
176
|
+
function prepareNumberForFormatting(number, currencyPrecision, currency, isSmallestUnit, currencyOverrides) {
|
|
153
177
|
if (isNaN(number)) {
|
|
154
178
|
debug('formatCurrency was called with NaN');
|
|
155
179
|
return 0;
|
|
@@ -158,7 +182,7 @@ function prepareNumberForFormatting(number, currencyPrecision, currency, isSmall
|
|
|
158
182
|
if (!Number.isInteger(number)) {
|
|
159
183
|
debug('formatCurrency was called with isSmallestUnit and a float which will be rounded', number);
|
|
160
184
|
}
|
|
161
|
-
const smallestUnitDivisor = 10 ** getSmallestUnitExponent(currency, currencyPrecision);
|
|
185
|
+
const smallestUnitDivisor = 10 ** getSmallestUnitExponent(currency, currencyPrecision, currencyOverrides);
|
|
162
186
|
return scaleNumberForPrecision(Math.round(number) / smallestUnitDivisor, currencyPrecision);
|
|
163
187
|
}
|
|
164
188
|
return scaleNumberForPrecision(number, currencyPrecision);
|
|
@@ -200,16 +224,17 @@ function prepareNumberForFormatting(number, currencyPrecision, currency, isSmall
|
|
|
200
224
|
* @param params.signForPositive - Whether to show the sign for positive numbers.
|
|
201
225
|
* @param params.geoLocation - The geo location of the user.
|
|
202
226
|
* @param params.forceLatin - Whether to force the latin locale.
|
|
227
|
+
* @param params.currencyOverrides - Dynamic per-currency overrides supplied by the host application.
|
|
203
228
|
* @return {string} A formatted string.
|
|
204
229
|
*/
|
|
205
|
-
const numberFormatCurrency = ({ number, browserSafeLocale, currency, stripZeros, isSmallestUnit, signForPositive, geoLocation, forceLatin, }) => {
|
|
206
|
-
const validCurrency = getValidCurrency(currency, geoLocation);
|
|
207
|
-
const currencyOverride = getCurrencyOverride(validCurrency, geoLocation);
|
|
230
|
+
const numberFormatCurrency = ({ number, browserSafeLocale, currency, stripZeros, isSmallestUnit, signForPositive, geoLocation, forceLatin, currencyOverrides, }) => {
|
|
231
|
+
const validCurrency = getValidCurrency(currency, geoLocation, currencyOverrides);
|
|
232
|
+
const currencyOverride = getCurrencyOverride(validCurrency, geoLocation, currencyOverrides);
|
|
208
233
|
const currencyPrecision = getPrecisionForLocaleAndCurrency(browserSafeLocale, validCurrency, forceLatin);
|
|
209
234
|
if (isSmallestUnit && typeof currencyPrecision === 'undefined') {
|
|
210
235
|
throw new Error(`Could not determine currency precision for ${validCurrency} in ${browserSafeLocale}`);
|
|
211
236
|
}
|
|
212
|
-
const numberAsFloat = prepareNumberForFormatting(number, currencyPrecision ?? 0, validCurrency, isSmallestUnit);
|
|
237
|
+
const numberAsFloat = prepareNumberForFormatting(number, currencyPrecision ?? 0, validCurrency, isSmallestUnit, currencyOverrides);
|
|
213
238
|
const formatter = getCurrencyFormatter({
|
|
214
239
|
number: numberAsFloat,
|
|
215
240
|
currency: validCurrency,
|
|
@@ -275,13 +300,14 @@ const numberFormatCurrency = ({ number, browserSafeLocale, currency, stripZeros,
|
|
|
275
300
|
* @param params.signForPositive - Whether to show the sign for positive numbers.
|
|
276
301
|
* @param params.geoLocation - The geo location of the user.
|
|
277
302
|
* @param params.forceLatin - Whether to force the latin locale.
|
|
303
|
+
* @param params.currencyOverrides - Dynamic per-currency overrides supplied by the host application.
|
|
278
304
|
* @return {CurrencyObject} A formatted string e.g. { symbol:'$', integer: '$99', fraction: '.99', sign: '-' }
|
|
279
305
|
*/
|
|
280
|
-
const getCurrencyObject = ({ number, browserSafeLocale, currency, stripZeros, isSmallestUnit, signForPositive, geoLocation, forceLatin, }) => {
|
|
281
|
-
const validCurrency = getValidCurrency(currency, geoLocation);
|
|
282
|
-
const currencyOverride = getCurrencyOverride(validCurrency, geoLocation);
|
|
306
|
+
const getCurrencyObject = ({ number, browserSafeLocale, currency, stripZeros, isSmallestUnit, signForPositive, geoLocation, forceLatin, currencyOverrides, }) => {
|
|
307
|
+
const validCurrency = getValidCurrency(currency, geoLocation, currencyOverrides);
|
|
308
|
+
const currencyOverride = getCurrencyOverride(validCurrency, geoLocation, currencyOverrides);
|
|
283
309
|
const currencyPrecision = getPrecisionForLocaleAndCurrency(browserSafeLocale, validCurrency, forceLatin);
|
|
284
|
-
const numberAsFloat = prepareNumberForFormatting(number, currencyPrecision ?? 0, validCurrency, isSmallestUnit);
|
|
310
|
+
const numberAsFloat = prepareNumberForFormatting(number, currencyPrecision ?? 0, validCurrency, isSmallestUnit, currencyOverrides);
|
|
285
311
|
const formatter = getCurrencyFormatter({
|
|
286
312
|
number: numberAsFloat,
|
|
287
313
|
currency: validCurrency,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { FormatCurrency, FormatNumber, GetCurrencyObject } from './types.ts';
|
|
1
|
+
import type { CurrencyOverride, FormatCurrency, FormatNumber, GetCurrencyObject } from './types.ts';
|
|
2
2
|
declare global {
|
|
3
3
|
interface Window {
|
|
4
4
|
wp?: {
|
|
@@ -23,6 +23,23 @@ export interface NumberFormatters {
|
|
|
23
23
|
* @param geoLocation - The geo location to use for formatting
|
|
24
24
|
*/
|
|
25
25
|
setGeoLocation(geoLocation: string): void;
|
|
26
|
+
/**
|
|
27
|
+
* Sets a dynamic map of per-currency overrides used by currency formatting.
|
|
28
|
+
*
|
|
29
|
+
* Typical use: load `{ "IDR": { "decimal": 0 } }` from the WPCOM currencies
|
|
30
|
+
* endpoint at app boot and pass the parsed object here. Each entry can carry
|
|
31
|
+
* a `symbol` and/or `decimal` (the smallest-unit exponent), and additional
|
|
32
|
+
* fields may be added to `CurrencyOverride` in the future.
|
|
33
|
+
*
|
|
34
|
+
* If this setter is never called, the package falls back to the hard-coded
|
|
35
|
+
* defaults shipped with the package, preserving previous behavior.
|
|
36
|
+
*
|
|
37
|
+
* When called with a partial map, missing currencies or fields fall back to
|
|
38
|
+
* the hard-coded defaults — passing `{ IDR: { decimal: 0 } }` does not clear
|
|
39
|
+
* the default IDR symbol, for example.
|
|
40
|
+
* @param overrides - Map of currency code to override settings
|
|
41
|
+
*/
|
|
42
|
+
setCurrencyOverrides(overrides: Record<string, CurrencyOverride>): void;
|
|
26
43
|
/**
|
|
27
44
|
* Formats numbers using locale settings and/or passed options.
|
|
28
45
|
* @param number - The number to format.
|
package/dist/types/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import createNumberFormatters from './create-number-formatters.ts';
|
|
2
|
-
export declare const setLocale: (locale: string) => void, setGeoLocation: (geoLocation: string) => void, formatNumber: import("./types.ts").FormatNumber, formatNumberCompact: import("./types.ts").FormatNumber, formatCurrency: import("./types.ts").FormatCurrency, getCurrencyObject: import("./types.ts").GetCurrencyObject;
|
|
2
|
+
export declare const setLocale: (locale: string) => void, setGeoLocation: (geoLocation: string) => void, setCurrencyOverrides: (overrides: Record<string, import("./types.ts").CurrencyOverride>) => void, formatNumber: import("./types.ts").FormatNumber, formatNumberCompact: import("./types.ts").FormatNumber, formatCurrency: import("./types.ts").FormatCurrency, getCurrencyObject: import("./types.ts").GetCurrencyObject;
|
|
3
3
|
export { createNumberFormatters };
|
|
4
4
|
export type * from './types.ts';
|
|
@@ -36,9 +36,10 @@ import type { CurrencyObject, NumberFormatCurrencyParams } from '../types.ts';
|
|
|
36
36
|
* @param params.signForPositive - Whether to show the sign for positive numbers.
|
|
37
37
|
* @param params.geoLocation - The geo location of the user.
|
|
38
38
|
* @param params.forceLatin - Whether to force the latin locale.
|
|
39
|
+
* @param params.currencyOverrides - Dynamic per-currency overrides supplied by the host application.
|
|
39
40
|
* @return {string} A formatted string.
|
|
40
41
|
*/
|
|
41
|
-
declare const numberFormatCurrency: ({ number, browserSafeLocale, currency, stripZeros, isSmallestUnit, signForPositive, geoLocation, forceLatin, }: NumberFormatCurrencyParams) => string;
|
|
42
|
+
declare const numberFormatCurrency: ({ number, browserSafeLocale, currency, stripZeros, isSmallestUnit, signForPositive, geoLocation, forceLatin, currencyOverrides, }: NumberFormatCurrencyParams) => string;
|
|
42
43
|
/**
|
|
43
44
|
* Returns a formatted price object which can be used to manually render a
|
|
44
45
|
* formatted currency (eg: if you wanted to render the currency symbol in a
|
|
@@ -83,7 +84,8 @@ declare const numberFormatCurrency: ({ number, browserSafeLocale, currency, stri
|
|
|
83
84
|
* @param params.signForPositive - Whether to show the sign for positive numbers.
|
|
84
85
|
* @param params.geoLocation - The geo location of the user.
|
|
85
86
|
* @param params.forceLatin - Whether to force the latin locale.
|
|
87
|
+
* @param params.currencyOverrides - Dynamic per-currency overrides supplied by the host application.
|
|
86
88
|
* @return {CurrencyObject} A formatted string e.g. { symbol:'$', integer: '$99', fraction: '.99', sign: '-' }
|
|
87
89
|
*/
|
|
88
|
-
declare const getCurrencyObject: ({ number, browserSafeLocale, currency, stripZeros, isSmallestUnit, signForPositive, geoLocation, forceLatin, }: NumberFormatCurrencyParams) => CurrencyObject;
|
|
90
|
+
declare const getCurrencyObject: ({ number, browserSafeLocale, currency, stripZeros, isSmallestUnit, signForPositive, geoLocation, forceLatin, currencyOverrides, }: NumberFormatCurrencyParams) => CurrencyObject;
|
|
89
91
|
export { numberFormatCurrency, getCurrencyObject };
|
package/dist/types/types.d.ts
CHANGED
|
@@ -21,6 +21,19 @@ export interface NumberFormatParams {
|
|
|
21
21
|
}
|
|
22
22
|
export interface CurrencyOverride {
|
|
23
23
|
symbol?: string;
|
|
24
|
+
/**
|
|
25
|
+
* Smallest-unit exponent for this currency, used when the browser's ICU
|
|
26
|
+
* `maximumFractionDigits` disagrees with the API's smallest-unit encoding,
|
|
27
|
+
* or when this package's hard-coded fallback exponent (see
|
|
28
|
+
* `SMALLEST_UNIT_EXPONENT_OVERRIDES` in `number-format-currency/index.ts`)
|
|
29
|
+
* disagrees with the host application's source of truth.
|
|
30
|
+
*
|
|
31
|
+
* For example, modern browser ICU (Chrome / Node 24+) reports IDR as
|
|
32
|
+
* 0-decimal, but this package's hard-coded fallback applies an exponent of
|
|
33
|
+
* 2 for legacy compatibility. The WPCOM currencies endpoint can send
|
|
34
|
+
* `{ "IDR": { "decimal": 0 } }` to override that hard-coded 2 back to 0.
|
|
35
|
+
*/
|
|
36
|
+
decimal?: number;
|
|
24
37
|
}
|
|
25
38
|
export interface NumberFormatCurrencyParams {
|
|
26
39
|
/**
|
|
@@ -67,6 +80,16 @@ export interface NumberFormatCurrencyParams {
|
|
|
67
80
|
* sign (eg: `+$35.00`). Has no effect on negative numbers or 0.
|
|
68
81
|
*/
|
|
69
82
|
signForPositive?: boolean;
|
|
83
|
+
/**
|
|
84
|
+
* Dynamic currency overrides, typically supplied by the host application
|
|
85
|
+
* (eg: from a remote endpoint) via `setCurrencyOverrides`.
|
|
86
|
+
*
|
|
87
|
+
* When provided, entries here take precedence over the hard-coded defaults
|
|
88
|
+
* baked into the package on a per-field basis. Anything not specified in
|
|
89
|
+
* this map falls back to the hard-coded defaults, so passing a partial map
|
|
90
|
+
* is safe.
|
|
91
|
+
*/
|
|
92
|
+
currencyOverrides?: Record<string, CurrencyOverride>;
|
|
70
93
|
}
|
|
71
94
|
export interface CurrencyObject {
|
|
72
95
|
/**
|
|
@@ -115,5 +138,5 @@ export interface CurrencyObject {
|
|
|
115
138
|
floatValue: number;
|
|
116
139
|
}
|
|
117
140
|
export type FormatNumber = (number: number, options?: Omit<NumberFormatParams, 'browserSafeLocale'>) => string;
|
|
118
|
-
export type FormatCurrency = (number: number, currency: string, options?: Omit<NumberFormatCurrencyParams, 'number' | 'currency' | 'browserSafeLocale' | 'geoLocation'>) => string;
|
|
119
|
-
export type GetCurrencyObject = (number: number, currency: string, options?: Omit<NumberFormatCurrencyParams, 'number' | 'currency' | 'browserSafeLocale' | 'geoLocation'>) => CurrencyObject;
|
|
141
|
+
export type FormatCurrency = (number: number, currency: string, options?: Omit<NumberFormatCurrencyParams, 'number' | 'currency' | 'browserSafeLocale' | 'geoLocation' | 'currencyOverrides'>) => string;
|
|
142
|
+
export type GetCurrencyObject = (number: number, currency: string, options?: Omit<NumberFormatCurrencyParams, 'number' | 'currency' | 'browserSafeLocale' | 'geoLocation' | 'currencyOverrides'>) => CurrencyObject;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@automattic/number-formatters",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Number formatting utilities",
|
|
5
5
|
"homepage": "https://github.com/Automattic/jetpack/tree/HEAD/projects/js-packages/number-formatters/#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -39,11 +39,11 @@
|
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@babel/core": "7.29.0",
|
|
41
41
|
"@babel/preset-react": "7.28.5",
|
|
42
|
-
"@jest/globals": "30.
|
|
42
|
+
"@jest/globals": "30.4.1",
|
|
43
43
|
"@knighted/duel": "4.0.2",
|
|
44
44
|
"@types/jest": "30.0.0",
|
|
45
45
|
"@typescript/native-preview": "7.0.0-dev.20260225.1",
|
|
46
|
-
"jest": "30.
|
|
46
|
+
"jest": "30.4.2",
|
|
47
47
|
"jetpack-js-tools": "workspace:*",
|
|
48
48
|
"typescript": "5.9.3"
|
|
49
49
|
}
|
|
@@ -4,7 +4,13 @@ import {
|
|
|
4
4
|
getCurrencyObject as getCurrencyObjectFromCurrencyFormatter,
|
|
5
5
|
} from './number-format-currency/index.ts';
|
|
6
6
|
import { numberFormat, numberFormatCompact } from './number-format.ts';
|
|
7
|
-
import type {
|
|
7
|
+
import type {
|
|
8
|
+
CurrencyObject,
|
|
9
|
+
CurrencyOverride,
|
|
10
|
+
FormatCurrency,
|
|
11
|
+
FormatNumber,
|
|
12
|
+
GetCurrencyObject,
|
|
13
|
+
} from './types.ts';
|
|
8
14
|
|
|
9
15
|
declare global {
|
|
10
16
|
interface Window {
|
|
@@ -33,6 +39,24 @@ export interface NumberFormatters {
|
|
|
33
39
|
*/
|
|
34
40
|
setGeoLocation( geoLocation: string ): void;
|
|
35
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Sets a dynamic map of per-currency overrides used by currency formatting.
|
|
44
|
+
*
|
|
45
|
+
* Typical use: load `{ "IDR": { "decimal": 0 } }` from the WPCOM currencies
|
|
46
|
+
* endpoint at app boot and pass the parsed object here. Each entry can carry
|
|
47
|
+
* a `symbol` and/or `decimal` (the smallest-unit exponent), and additional
|
|
48
|
+
* fields may be added to `CurrencyOverride` in the future.
|
|
49
|
+
*
|
|
50
|
+
* If this setter is never called, the package falls back to the hard-coded
|
|
51
|
+
* defaults shipped with the package, preserving previous behavior.
|
|
52
|
+
*
|
|
53
|
+
* When called with a partial map, missing currencies or fields fall back to
|
|
54
|
+
* the hard-coded defaults — passing `{ IDR: { decimal: 0 } }` does not clear
|
|
55
|
+
* the default IDR symbol, for example.
|
|
56
|
+
* @param overrides - Map of currency code to override settings
|
|
57
|
+
*/
|
|
58
|
+
setCurrencyOverrides( overrides: Record< string, CurrencyOverride > ): void;
|
|
59
|
+
|
|
36
60
|
/**
|
|
37
61
|
* Formats numbers using locale settings and/or passed options.
|
|
38
62
|
* @param number - The number to format.
|
|
@@ -150,6 +174,7 @@ export interface NumberFormatters {
|
|
|
150
174
|
function createNumberFormatters(): NumberFormatters {
|
|
151
175
|
let localeState: string | undefined;
|
|
152
176
|
let geoLocationState: string | undefined;
|
|
177
|
+
let currencyOverridesState: Record< string, CurrencyOverride > | undefined;
|
|
153
178
|
|
|
154
179
|
const setLocale = ( locale: string ): void => {
|
|
155
180
|
/**
|
|
@@ -160,6 +185,10 @@ function createNumberFormatters(): NumberFormatters {
|
|
|
160
185
|
localeState = locale;
|
|
161
186
|
};
|
|
162
187
|
|
|
188
|
+
const setCurrencyOverrides = ( overrides: Record< string, CurrencyOverride > ): void => {
|
|
189
|
+
currencyOverridesState = overrides;
|
|
190
|
+
};
|
|
191
|
+
|
|
163
192
|
/**
|
|
164
193
|
* Returns the locale defined on the module instance (through `setLocale`)
|
|
165
194
|
* or the "fallback locale" if no locale has been set.
|
|
@@ -242,6 +271,7 @@ function createNumberFormatters(): NumberFormatters {
|
|
|
242
271
|
signForPositive,
|
|
243
272
|
geoLocation: geoLocationState,
|
|
244
273
|
forceLatin,
|
|
274
|
+
currencyOverrides: currencyOverridesState,
|
|
245
275
|
} );
|
|
246
276
|
};
|
|
247
277
|
|
|
@@ -259,12 +289,14 @@ function createNumberFormatters(): NumberFormatters {
|
|
|
259
289
|
signForPositive,
|
|
260
290
|
geoLocation: geoLocationState,
|
|
261
291
|
forceLatin,
|
|
292
|
+
currencyOverrides: currencyOverridesState,
|
|
262
293
|
} );
|
|
263
294
|
};
|
|
264
295
|
|
|
265
296
|
return {
|
|
266
297
|
setLocale,
|
|
267
298
|
setGeoLocation,
|
|
299
|
+
setCurrencyOverrides,
|
|
268
300
|
formatNumber,
|
|
269
301
|
formatNumberCompact,
|
|
270
302
|
formatCurrency,
|
package/src/index.ts
CHANGED
|
@@ -8,30 +8,47 @@ const debug = debugFactory( 'number-formatters:number-format-currency' );
|
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* Retrieves the currency override for a given currency.
|
|
11
|
+
*
|
|
11
12
|
* If the currency is USD and the user is not in the US, it will return `US$`.
|
|
12
|
-
*
|
|
13
|
-
*
|
|
13
|
+
*
|
|
14
|
+
* Per-field merge order is: dynamic overrides (from `currencyOverrides`) → hard-coded defaults.
|
|
15
|
+
* This means a caller can supply a partial map (eg: only `decimal`) without losing the
|
|
16
|
+
* default `symbol`.
|
|
17
|
+
* @param currency - The currency to get the override for.
|
|
18
|
+
* @param geoLocation - The geo location of the user.
|
|
19
|
+
* @param currencyOverrides - Dynamic per-currency overrides supplied by the host application.
|
|
14
20
|
* @return {CurrencyOverride | undefined} The currency override.
|
|
15
21
|
*/
|
|
16
22
|
function getCurrencyOverride(
|
|
17
23
|
currency: string,
|
|
18
|
-
geoLocation?: string
|
|
24
|
+
geoLocation?: string,
|
|
25
|
+
currencyOverrides?: Record< string, CurrencyOverride >
|
|
19
26
|
): CurrencyOverride | undefined {
|
|
20
27
|
if ( currency === 'USD' && geoLocation && geoLocation !== '' && geoLocation !== 'US' ) {
|
|
21
|
-
return { symbol: 'US$' };
|
|
28
|
+
return { symbol: 'US$', ...currencyOverrides?.USD };
|
|
22
29
|
}
|
|
23
|
-
|
|
30
|
+
const defaultOverride = defaultCurrencyOverrides[ currency ];
|
|
31
|
+
const dynamicOverride = currencyOverrides?.[ currency ];
|
|
32
|
+
if ( ! defaultOverride && ! dynamicOverride ) {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
return { ...defaultOverride, ...dynamicOverride };
|
|
24
36
|
}
|
|
25
37
|
|
|
26
38
|
/**
|
|
27
39
|
* Returns a valid currency code based on a shortlist of currency codes.
|
|
28
40
|
* Only currencies from the shortlist are allowed. Everything else will fall back to `FALLBACK_CURRENCY`.
|
|
29
|
-
* @param currency
|
|
30
|
-
* @param geoLocation
|
|
41
|
+
* @param currency - The currency to get the valid currency for.
|
|
42
|
+
* @param geoLocation - The geo location of the user.
|
|
43
|
+
* @param currencyOverrides - Dynamic per-currency overrides supplied by the host application.
|
|
31
44
|
* @return {string} The valid currency.
|
|
32
45
|
*/
|
|
33
|
-
function getValidCurrency(
|
|
34
|
-
|
|
46
|
+
function getValidCurrency(
|
|
47
|
+
currency: string,
|
|
48
|
+
geoLocation?: string,
|
|
49
|
+
currencyOverrides?: Record< string, CurrencyOverride >
|
|
50
|
+
): string {
|
|
51
|
+
if ( ! getCurrencyOverride( currency, geoLocation, currencyOverrides ) ) {
|
|
35
52
|
debug(
|
|
36
53
|
`getValidCurrency was called with a non-existent currency "${ currency }"; falling back to ${ FALLBACK_CURRENCY }`
|
|
37
54
|
);
|
|
@@ -99,9 +116,14 @@ function getCurrencyFormatter( {
|
|
|
99
116
|
}
|
|
100
117
|
|
|
101
118
|
/**
|
|
102
|
-
*
|
|
119
|
+
* Hard-coded smallest-unit exponent overrides for currencies where browser ICU's
|
|
103
120
|
* `maximumFractionDigits` disagrees with the API's smallest-unit encoding.
|
|
104
121
|
*
|
|
122
|
+
* This list exists as a safety net for callers that have not yet wired up the
|
|
123
|
+
* dynamic `currencyOverrides` path (eg: the WPCOM currencies endpoint). Once a
|
|
124
|
+
* host application provides overrides via `setCurrencyOverrides`, those take
|
|
125
|
+
* precedence on a per-currency basis.
|
|
126
|
+
*
|
|
105
127
|
* Keep this list minimal — the backend is the source of truth for the API's
|
|
106
128
|
* smallest-unit encoding, so adding speculative entries here risks silent
|
|
107
129
|
* drift. Only add a currency once we've verified that browsers report a
|
|
@@ -118,14 +140,24 @@ const SMALLEST_UNIT_EXPONENT_OVERRIDES: Record< string, number > = {
|
|
|
118
140
|
/**
|
|
119
141
|
* Returns the smallest unit exponent for a currency.
|
|
120
142
|
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
* @param
|
|
126
|
-
* @
|
|
143
|
+
* Lookup order:
|
|
144
|
+
* 1. The dynamic `currencyOverrides[currency].decimal` if a host application has supplied one (typically via `setCurrencyOverrides`).
|
|
145
|
+
* 2. The hard-coded `SMALLEST_UNIT_EXPONENT_OVERRIDES` map.
|
|
146
|
+
* 3. The browser-derived display precision (`fallback`).
|
|
147
|
+
* @param currency - The currency code (ISO 4217)
|
|
148
|
+
* @param fallback - The browser-derived precision to use when no override applies
|
|
149
|
+
* @param currencyOverrides - Dynamic per-currency overrides supplied by the host application
|
|
150
|
+
* @return number - The smallest unit exponent
|
|
127
151
|
*/
|
|
128
|
-
function getSmallestUnitExponent(
|
|
152
|
+
function getSmallestUnitExponent(
|
|
153
|
+
currency: string,
|
|
154
|
+
fallback: number,
|
|
155
|
+
currencyOverrides?: Record< string, CurrencyOverride >
|
|
156
|
+
): number {
|
|
157
|
+
const dynamicDecimal = currencyOverrides?.[ currency ]?.decimal;
|
|
158
|
+
if ( typeof dynamicDecimal === 'number' ) {
|
|
159
|
+
return dynamicDecimal;
|
|
160
|
+
}
|
|
129
161
|
return SMALLEST_UNIT_EXPONENT_OVERRIDES[ currency ] ?? fallback;
|
|
130
162
|
}
|
|
131
163
|
|
|
@@ -173,13 +205,15 @@ function scaleNumberForPrecision( number: number, currencyPrecision: number ): n
|
|
|
173
205
|
* @param currencyPrecision - The display precision (from the browser) to round the result to.
|
|
174
206
|
* @param currency - The currency code, used to look up any smallest-unit exponent override.
|
|
175
207
|
* @param isSmallestUnit - Whether the number is the smallest unit of a currency.
|
|
208
|
+
* @param currencyOverrides - Dynamic per-currency overrides supplied by the host application.
|
|
176
209
|
* @return {number} The prepared number.
|
|
177
210
|
*/
|
|
178
211
|
function prepareNumberForFormatting(
|
|
179
212
|
number: number,
|
|
180
213
|
currencyPrecision: number,
|
|
181
214
|
currency: string,
|
|
182
|
-
isSmallestUnit?: boolean
|
|
215
|
+
isSmallestUnit?: boolean,
|
|
216
|
+
currencyOverrides?: Record< string, CurrencyOverride >
|
|
183
217
|
): number {
|
|
184
218
|
if ( isNaN( number ) ) {
|
|
185
219
|
debug( 'formatCurrency was called with NaN' );
|
|
@@ -193,7 +227,8 @@ function prepareNumberForFormatting(
|
|
|
193
227
|
number
|
|
194
228
|
);
|
|
195
229
|
}
|
|
196
|
-
const smallestUnitDivisor =
|
|
230
|
+
const smallestUnitDivisor =
|
|
231
|
+
10 ** getSmallestUnitExponent( currency, currencyPrecision, currencyOverrides );
|
|
197
232
|
return scaleNumberForPrecision( Math.round( number ) / smallestUnitDivisor, currencyPrecision );
|
|
198
233
|
}
|
|
199
234
|
|
|
@@ -237,6 +272,7 @@ function prepareNumberForFormatting(
|
|
|
237
272
|
* @param params.signForPositive - Whether to show the sign for positive numbers.
|
|
238
273
|
* @param params.geoLocation - The geo location of the user.
|
|
239
274
|
* @param params.forceLatin - Whether to force the latin locale.
|
|
275
|
+
* @param params.currencyOverrides - Dynamic per-currency overrides supplied by the host application.
|
|
240
276
|
* @return {string} A formatted string.
|
|
241
277
|
*/
|
|
242
278
|
const numberFormatCurrency = ( {
|
|
@@ -248,9 +284,10 @@ const numberFormatCurrency = ( {
|
|
|
248
284
|
signForPositive,
|
|
249
285
|
geoLocation,
|
|
250
286
|
forceLatin,
|
|
287
|
+
currencyOverrides,
|
|
251
288
|
}: NumberFormatCurrencyParams ) => {
|
|
252
|
-
const validCurrency = getValidCurrency( currency, geoLocation );
|
|
253
|
-
const currencyOverride = getCurrencyOverride( validCurrency, geoLocation );
|
|
289
|
+
const validCurrency = getValidCurrency( currency, geoLocation, currencyOverrides );
|
|
290
|
+
const currencyOverride = getCurrencyOverride( validCurrency, geoLocation, currencyOverrides );
|
|
254
291
|
const currencyPrecision = getPrecisionForLocaleAndCurrency(
|
|
255
292
|
browserSafeLocale,
|
|
256
293
|
validCurrency,
|
|
@@ -267,7 +304,8 @@ const numberFormatCurrency = ( {
|
|
|
267
304
|
number,
|
|
268
305
|
currencyPrecision ?? 0,
|
|
269
306
|
validCurrency,
|
|
270
|
-
isSmallestUnit
|
|
307
|
+
isSmallestUnit,
|
|
308
|
+
currencyOverrides
|
|
271
309
|
);
|
|
272
310
|
const formatter = getCurrencyFormatter( {
|
|
273
311
|
number: numberAsFloat,
|
|
@@ -336,6 +374,7 @@ const numberFormatCurrency = ( {
|
|
|
336
374
|
* @param params.signForPositive - Whether to show the sign for positive numbers.
|
|
337
375
|
* @param params.geoLocation - The geo location of the user.
|
|
338
376
|
* @param params.forceLatin - Whether to force the latin locale.
|
|
377
|
+
* @param params.currencyOverrides - Dynamic per-currency overrides supplied by the host application.
|
|
339
378
|
* @return {CurrencyObject} A formatted string e.g. { symbol:'$', integer: '$99', fraction: '.99', sign: '-' }
|
|
340
379
|
*/
|
|
341
380
|
const getCurrencyObject = ( {
|
|
@@ -347,9 +386,10 @@ const getCurrencyObject = ( {
|
|
|
347
386
|
signForPositive,
|
|
348
387
|
geoLocation,
|
|
349
388
|
forceLatin,
|
|
389
|
+
currencyOverrides,
|
|
350
390
|
}: NumberFormatCurrencyParams ): CurrencyObject => {
|
|
351
|
-
const validCurrency = getValidCurrency( currency, geoLocation );
|
|
352
|
-
const currencyOverride = getCurrencyOverride( validCurrency, geoLocation );
|
|
391
|
+
const validCurrency = getValidCurrency( currency, geoLocation, currencyOverrides );
|
|
392
|
+
const currencyOverride = getCurrencyOverride( validCurrency, geoLocation, currencyOverrides );
|
|
353
393
|
const currencyPrecision = getPrecisionForLocaleAndCurrency(
|
|
354
394
|
browserSafeLocale,
|
|
355
395
|
validCurrency,
|
|
@@ -359,7 +399,8 @@ const getCurrencyObject = ( {
|
|
|
359
399
|
number,
|
|
360
400
|
currencyPrecision ?? 0,
|
|
361
401
|
validCurrency,
|
|
362
|
-
isSmallestUnit
|
|
402
|
+
isSmallestUnit,
|
|
403
|
+
currencyOverrides
|
|
363
404
|
);
|
|
364
405
|
const formatter = getCurrencyFormatter( {
|
|
365
406
|
number: numberAsFloat,
|
package/src/types.ts
CHANGED
|
@@ -22,6 +22,19 @@ export interface NumberFormatParams {
|
|
|
22
22
|
|
|
23
23
|
export interface CurrencyOverride {
|
|
24
24
|
symbol?: string;
|
|
25
|
+
/**
|
|
26
|
+
* Smallest-unit exponent for this currency, used when the browser's ICU
|
|
27
|
+
* `maximumFractionDigits` disagrees with the API's smallest-unit encoding,
|
|
28
|
+
* or when this package's hard-coded fallback exponent (see
|
|
29
|
+
* `SMALLEST_UNIT_EXPONENT_OVERRIDES` in `number-format-currency/index.ts`)
|
|
30
|
+
* disagrees with the host application's source of truth.
|
|
31
|
+
*
|
|
32
|
+
* For example, modern browser ICU (Chrome / Node 24+) reports IDR as
|
|
33
|
+
* 0-decimal, but this package's hard-coded fallback applies an exponent of
|
|
34
|
+
* 2 for legacy compatibility. The WPCOM currencies endpoint can send
|
|
35
|
+
* `{ "IDR": { "decimal": 0 } }` to override that hard-coded 2 back to 0.
|
|
36
|
+
*/
|
|
37
|
+
decimal?: number;
|
|
25
38
|
}
|
|
26
39
|
|
|
27
40
|
export interface NumberFormatCurrencyParams {
|
|
@@ -76,6 +89,17 @@ export interface NumberFormatCurrencyParams {
|
|
|
76
89
|
* sign (eg: `+$35.00`). Has no effect on negative numbers or 0.
|
|
77
90
|
*/
|
|
78
91
|
signForPositive?: boolean;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Dynamic currency overrides, typically supplied by the host application
|
|
95
|
+
* (eg: from a remote endpoint) via `setCurrencyOverrides`.
|
|
96
|
+
*
|
|
97
|
+
* When provided, entries here take precedence over the hard-coded defaults
|
|
98
|
+
* baked into the package on a per-field basis. Anything not specified in
|
|
99
|
+
* this map falls back to the hard-coded defaults, so passing a partial map
|
|
100
|
+
* is safe.
|
|
101
|
+
*/
|
|
102
|
+
currencyOverrides?: Record< string, CurrencyOverride >;
|
|
79
103
|
}
|
|
80
104
|
|
|
81
105
|
export interface CurrencyObject {
|
|
@@ -141,7 +165,7 @@ export type FormatCurrency = (
|
|
|
141
165
|
currency: string,
|
|
142
166
|
options?: Omit<
|
|
143
167
|
NumberFormatCurrencyParams,
|
|
144
|
-
'number' | 'currency' | 'browserSafeLocale' | 'geoLocation'
|
|
168
|
+
'number' | 'currency' | 'browserSafeLocale' | 'geoLocation' | 'currencyOverrides'
|
|
145
169
|
>
|
|
146
170
|
) => string;
|
|
147
171
|
|
|
@@ -150,6 +174,6 @@ export type GetCurrencyObject = (
|
|
|
150
174
|
currency: string,
|
|
151
175
|
options?: Omit<
|
|
152
176
|
NumberFormatCurrencyParams,
|
|
153
|
-
'number' | 'currency' | 'browserSafeLocale' | 'geoLocation'
|
|
177
|
+
'number' | 'currency' | 'browserSafeLocale' | 'geoLocation' | 'currencyOverrides'
|
|
154
178
|
>
|
|
155
179
|
) => CurrencyObject;
|