UNPKG

@automattic/format-currency

Version:

JavaScript library for formatting currency.

386 lines 17 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.createFormatter = createFormatter; exports.geolocateCurrencySymbol = geolocateCurrencySymbol; exports.formatCurrency = formatCurrency; exports.getCurrencyObject = getCurrencyObject; exports.setDefaultLocale = setDefaultLocale; const tslib_1 = require("tslib"); const currencies_1 = require("./currencies"); const get_cached_formatter_1 = require("./get-cached-formatter"); tslib_1.__exportStar(require("./types"), exports); const fallbackLocale = 'en'; const fallbackCurrency = 'USD'; const geolocationEndpointUrl = 'https://public-api.wordpress.com/geo/'; // TODO clk numberFormatCurrency exported only for tests function createFormatter() { let defaultLocale = undefined; let geoLocation = ''; // If the user is inside the US using USD, they should only see `$` and not `US$`. async function geolocateCurrencySymbol() { const geoData = await globalThis .fetch?.(geolocationEndpointUrl) .then((response) => response.json()) .catch((error) => { // Do nothing if the fetch fails. // eslint-disable-next-line no-console console.warn('Fetching geolocation for format-currency failed.', error); }); if (!containsGeolocationCountry(geoData)) { return; } if (!geoData.country_short) { return; } geoLocation = geoData.country_short; } function getLocaleToUse(options) { return options.locale ?? defaultLocale ?? getLocaleFromBrowser(); } function getFormatter(number, code, options) { const numberFormatOptions = { style: 'currency', currency: code, ...(options.stripZeros && Number.isInteger(number) && { /** * There's an option called `trailingZeroDisplay` but it does not yet work * in FF so we have to strip zeros manually. */ maximumFractionDigits: 0, minimumFractionDigits: 0, }), ...(options.signForPositive && { signDisplay: 'exceptZero' }), }; /** * `numberingSystem` is an option to `Intl.NumberFormat` and is available * in all major browsers according to * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat#options * but is not part of the TypeScript types in `es2020`: * * https://github.com/microsoft/TypeScript/blob/cfd472f7aa5a2010a3115263bf457b30c5b489f3/src/lib/es2020.intl.d.ts#L272 * * However, it is part of the TypeScript types in `es5`: * * https://github.com/microsoft/TypeScript/blob/cfd472f7aa5a2010a3115263bf457b30c5b489f3/src/lib/es5.d.ts#L4310 * * Apparently calypso uses `es2020` so we cannot use that option here right * now. Instead, we will use the unicode extension to the locale, documented * here: * * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/numberingSystem#adding_a_numbering_system_via_the_locale_string */ return (0, get_cached_formatter_1.getCachedFormatter)({ locale: `${getLocaleToUse(options)}-u-nu-latn`, options: numberFormatOptions, }); } /** * Formats money with a given currency code. * * The currency will define the properties to use for this formatting, but * those properties can be overridden using the options. Be careful when doing * this. * * For currencies that include decimals, this will always return the amount * with decimals included, even if those decimals are zeros. To exclude the * zeros, use the `stripZeros` option. For example, the function will normally * format `10.00` in `USD` as `$10.00` but when this option is true, it will * return `$10` instead. * * Since rounding errors are common in floating point math, sometimes a price * is provided as an integer in the smallest unit of a currency (eg: cents in * USD or yen in JPY). Set the `isSmallestUnit` to change the function to * operate on integer numbers instead. If this option is not set or false, the * function will format the amount `1025` in `USD` as `$1,025.00`, but when the * option is true, it will return `$10.25` instead. * * If the number is NaN, it will be treated as 0. * * If the currency code is not known, this will assume a default currency * similar to USD. * * If `isSmallestUnit` is set and the number is not an integer, it will be * rounded to an integer. * @param {number} number number to format; assumed to be a float unless isSmallestUnit is set. * @param {string} code currency code e.g. 'USD' * @param {CurrencyObjectOptions} options options object * @returns {string} A formatted string. */ function formatCurrency(number, code, options = {}) { const locale = getLocaleToUse(options); const validCurrency = getValidCurrency(code); const currencyOverride = getCurrencyOverride(validCurrency); const currencyPrecision = getPrecisionForLocaleAndCurrency(locale, validCurrency); const numberAsFloat = prepareNumberForFormatting(number, currencyPrecision ?? 0, options); const formatter = getFormatter(numberAsFloat, validCurrency, options); const parts = formatter.formatToParts(numberAsFloat); return parts.reduce((formatted, part) => { switch (part.type) { case 'currency': if (currencyOverride?.symbol) { return formatted + currencyOverride.symbol; } return formatted + part.value; default: return formatted + part.value; } }, ''); } /** * Returns a formatted price object which can be used to manually render a * formatted currency (eg: if you wanted to render the currency symbol in a * different font size). * * The currency will define the properties to use for this formatting, but * those properties can be overridden using the options. Be careful when doing * this. * * For currencies that include decimals, this will always return the amount * with decimals included, even if those decimals are zeros. To exclude the * zeros, use the `stripZeros` option. For example, the function will normally * format `10.00` in `USD` as `$10.00` but when this option is true, it will * return `$10` instead. * * Since rounding errors are common in floating point math, sometimes a price * is provided as an integer in the smallest unit of a currency (eg: cents in * USD or yen in JPY). Set the `isSmallestUnit` to change the function to * operate on integer numbers instead. If this option is not set or false, the * function will format the amount `1025` in `USD` as `$1,025.00`, but when the * option is true, it will return `$10.25` instead. * * Note that the `integer` return value of this function is not a number, but a * locale-formatted string which may include symbols like spaces, commas, or * periods as group separators. Similarly, the `fraction` property is a string * that contains the decimal separator. * * If the number is NaN, it will be treated as 0. * * If the currency code is not known, this will assume a default currency * similar to USD. * * If `isSmallestUnit` is set and the number is not an integer, it will be * rounded to an integer. * @param {number} number number to format; assumed to be a float unless isSmallestUnit is set. * @param {string} code currency code e.g. 'USD' * @param {CurrencyObjectOptions} options options object * @returns {CurrencyObject} A formatted string e.g. { symbol:'$', integer: '$99', fraction: '.99', sign: '-' } */ function getCurrencyObject(number, code, options = {}) { const locale = getLocaleToUse(options); const validCurrency = getValidCurrency(code); const currencyOverride = getCurrencyOverride(validCurrency); const currencyPrecision = getPrecisionForLocaleAndCurrency(locale, validCurrency); const numberAsFloat = prepareNumberForFormatting(number, currencyPrecision ?? 0, options); const formatter = getFormatter(numberAsFloat, validCurrency, options); const parts = formatter.formatToParts(numberAsFloat); let sign = ''; let symbol = '$'; let symbolPosition = 'before'; let hasAmountBeenSet = false; let hasDecimalBeenSet = false; let integer = ''; let fraction = ''; parts.forEach((part) => { switch (part.type) { case 'currency': symbol = currencyOverride?.symbol ?? part.value; if (hasAmountBeenSet) { symbolPosition = 'after'; } return; case 'group': integer += part.value; hasAmountBeenSet = true; return; case 'decimal': fraction += part.value; hasAmountBeenSet = true; hasDecimalBeenSet = true; return; case 'integer': integer += part.value; hasAmountBeenSet = true; return; case 'fraction': fraction += part.value; hasAmountBeenSet = true; hasDecimalBeenSet = true; return; case 'minusSign': sign = '-'; return; case 'plusSign': sign = '+'; return; } }); const hasNonZeroFraction = !Number.isInteger(numberAsFloat) && hasDecimalBeenSet; return { sign, symbol, symbolPosition, integer, fraction, hasNonZeroFraction, }; } function getValidCurrency(code) { if (!doesCurrencyExist(code)) { // eslint-disable-next-line no-console console.warn(`getCurrencyObject was called with a non-existent currency "${code}"; falling back to ${fallbackCurrency}`); return fallbackCurrency; } return code; } function getCurrencyOverride(code) { if (code === 'USD' && geoLocation !== '' && geoLocation !== 'US') { return { symbol: 'US$' }; } return currencies_1.defaultCurrencyOverrides[code]; } function doesCurrencyExist(code) { return Boolean(getCurrencyOverride(code)); } /** * Set a default locale for use by `formatCurrency` and `getCurrencyObject`. * * Note that this is global and will override any browser locale that is set! * Use it with care. */ function setDefaultLocale(locale) { defaultLocale = locale; } function getPrecisionForLocaleAndCurrency(locale, currency) { const formatter = getFormatter(0, currency, { locale }); return formatter.resolvedOptions().maximumFractionDigits ?? 3; // 3 is the default for Intl.NumberFormat if minimumFractionDigits is not set } return { formatCurrency, getCurrencyObject, setDefaultLocale, geolocateCurrencySymbol, }; } function getLocaleFromBrowser() { if (typeof window === 'undefined') { return fallbackLocale; } if (window.navigator?.languages?.length > 0) { return window.navigator.languages[0]; } return window.navigator?.language ?? fallbackLocale; } function prepareNumberForFormatting(number, // currencyPrecision here must be the precision of the currency, regardless // of what precision is requested for display! currencyPrecision, options) { if (isNaN(number)) { // eslint-disable-next-line no-console console.warn('formatCurrency was called with NaN'); number = 0; } if (options.isSmallestUnit) { if (!Number.isInteger(number)) { // eslint-disable-next-line no-console console.warn('formatCurrency was called with isSmallestUnit and a float which will be rounded', number); number = Math.round(number); } number = convertPriceForSmallestUnit(number, currencyPrecision); } const scale = Math.pow(10, currencyPrecision); return Math.round(number * scale) / scale; } function convertPriceForSmallestUnit(price, precision) { return price / getSmallestUnitDivisor(precision); } function getSmallestUnitDivisor(precision) { return 10 ** precision; } function containsGeolocationCountry(response) { return typeof response?.country_short === 'string'; } const defaultFormatter = createFormatter(); async function geolocateCurrencySymbol() { return defaultFormatter.geolocateCurrencySymbol(); } /** * Formats money with a given currency code. * * The currency will define the properties to use for this formatting, but * those properties can be overridden using the options. Be careful when doing * this. * * For currencies that include decimals, this will always return the amount * with decimals included, even if those decimals are zeros. To exclude the * zeros, use the `stripZeros` option. For example, the function will normally * format `10.00` in `USD` as `$10.00` but when this option is true, it will * return `$10` instead. * * Since rounding errors are common in floating point math, sometimes a price * is provided as an integer in the smallest unit of a currency (eg: cents in * USD or yen in JPY). Set the `isSmallestUnit` to change the function to * operate on integer numbers instead. If this option is not set or false, the * function will format the amount `1025` in `USD` as `$1,025.00`, but when the * option is true, it will return `$10.25` instead. * * If the number is NaN, it will be treated as 0. * * If the currency code is not known, this will assume a default currency * similar to USD. * * If `isSmallestUnit` is set and the number is not an integer, it will be * rounded to an integer. */ function formatCurrency(...args) { return defaultFormatter.formatCurrency(...args); } /** * Returns a formatted price object which can be used to manually render a * formatted currency (eg: if you wanted to render the currency symbol in a * different font size). * * The currency will define the properties to use for this formatting, but * those properties can be overridden using the options. Be careful when doing * this. * * For currencies that include decimals, this will always return the amount * with decimals included, even if those decimals are zeros. To exclude the * zeros, use the `stripZeros` option. For example, the function will normally * format `10.00` in `USD` as `$10.00` but when this option is true, it will * return `$10` instead. * * Since rounding errors are common in floating point math, sometimes a price * is provided as an integer in the smallest unit of a currency (eg: cents in * USD or yen in JPY). Set the `isSmallestUnit` to change the function to * operate on integer numbers instead. If this option is not set or false, the * function will format the amount `1025` in `USD` as `$1,025.00`, but when the * option is true, it will return `$10.25` instead. * * Note that the `integer` return value of this function is not a number, but a * locale-formatted string which may include symbols like spaces, commas, or * periods as group separators. Similarly, the `fraction` property is a string * that contains the decimal separator. * * If the number is NaN, it will be treated as 0. * * If the currency code is not known, this will assume a default currency * similar to USD. * * If `isSmallestUnit` is set and the number is not an integer, it will be * rounded to an integer. */ function getCurrencyObject(...args) { return defaultFormatter.getCurrencyObject(...args); } /** * Set a default locale for use by `formatCurrency` and `getCurrencyObject`. * * Note that this is global and will override any browser locale that is set! * Use it with care. */ function setDefaultLocale(...args) { return defaultFormatter.setDefaultLocale(...args); } exports.default = defaultFormatter.formatCurrency; //# sourceMappingURL=index.js.map