@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 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,
@@ -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
- * @param currency - The currency to get the override for.
14
- * @param geoLocation - The geo location of the user.
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 currencies_ts_1.defaultCurrencyOverrides[currency];
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 - The currency to get the valid currency for.
27
- * @param geoLocation - The geo location of the user.
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
- * Smallest-unit exponent overrides for currencies where browser ICU's
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
- * Falls back to the browser-derived display precision for any currency not in
107
- * the override map — i.e. existing behavior is preserved for everything except
108
- * the explicitly listed currencies.
109
- * @param currency - The currency code (ISO 4217)
110
- * @param fallback - The browser-derived precision to use when no override applies
111
- * @return number - The smallest unit exponent
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
- * @param currency - The currency to get the override for.
10
- * @param geoLocation - The geo location of the user.
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 defaultCurrencyOverrides[currency];
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 - The currency to get the valid currency for.
23
- * @param geoLocation - The geo location of the user.
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
- * Smallest-unit exponent overrides for currencies where browser ICU's
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
- * Falls back to the browser-derived display precision for any currency not in
103
- * the override map — i.e. existing behavior is preserved for everything except
104
- * the explicitly listed currencies.
105
- * @param currency - The currency code (ISO 4217)
106
- * @param fallback - The browser-derived precision to use when no override applies
107
- * @return number - The smallest unit exponent
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.
@@ -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 };
@@ -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.1.9",
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.3.0",
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.3.0",
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 { CurrencyObject, FormatCurrency, FormatNumber, GetCurrencyObject } from './types.ts';
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
@@ -5,6 +5,7 @@ const defaultFormatter = createNumberFormatters();
5
5
  export const {
6
6
  setLocale,
7
7
  setGeoLocation,
8
+ setCurrencyOverrides,
8
9
  formatNumber,
9
10
  formatNumberCompact,
10
11
  formatCurrency,
@@ -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
- * @param currency - The currency to get the override for.
13
- * @param geoLocation - The geo location of the user.
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
- return defaultCurrencyOverrides[ currency ];
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 - The currency to get the valid currency for.
30
- * @param geoLocation - The geo location of the user.
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( currency: string, geoLocation?: string ): string {
34
- if ( ! getCurrencyOverride( currency, geoLocation ) ) {
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
- * Smallest-unit exponent overrides for currencies where browser ICU's
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
- * Falls back to the browser-derived display precision for any currency not in
122
- * the override map — i.e. existing behavior is preserved for everything except
123
- * the explicitly listed currencies.
124
- * @param currency - The currency code (ISO 4217)
125
- * @param fallback - The browser-derived precision to use when no override applies
126
- * @return number - The smallest unit exponent
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( currency: string, fallback: number ): number {
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 = 10 ** getSmallestUnitExponent( currency, currencyPrecision );
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;