diff --git a/package-lock.json b/package-lock.json index 318f9fcfb..122f420ef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5568,6 +5568,12 @@ "integrity": "sha1-QICG1AlVDCYxFVYZ6fp7ytw7mRs=", "dev": true }, + "currency-symbol-map": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/currency-symbol-map/-/currency-symbol-map-5.0.1.tgz", + "integrity": "sha512-2yHvoVBPIS9Dx7fjritu9l7UHWo4Z3D2oQnQj8AYwUee2j/PAcoHB4issCqwabfbipISZ/iG6vVYsfrAoks2eA==", + "dev": true + }, "dashdash": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", @@ -6652,7 +6658,10 @@ "version": "1.3.1", "resolved": "https://registry.npmjs.org/full-icu/-/full-icu-1.3.1.tgz", "integrity": "sha512-VMtK//85QJomhk3cXOCksNwOYaw1KWnYTS37GYGgyf7A3ajdBoPGhaJuJWAH2S2kq8GZeXkdKn+3Mfmgy11cVw==", - "dev": true + "dev": true, + "requires": { + "icu4c-data": "^0.67.2" + } }, "function-bind": { "version": "1.1.1", @@ -7211,6 +7220,12 @@ "safer-buffer": ">= 2.1.2 < 3" } }, + "icu4c-data": { + "version": "0.67.2", + "resolved": "https://registry.npmjs.org/icu4c-data/-/icu4c-data-0.67.2.tgz", + "integrity": "sha512-OIRiop+k1IVf4TBLEOj910duoO9NKwtJLwp++qWT6KT5gRziHNt+5gwhcGuTqRy++RTK2gLoAIbk8KYCNxW++g==", + "dev": true + }, "ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -8586,6 +8601,12 @@ "json5": "^2.1.2" } }, + "locale-currency": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/locale-currency/-/locale-currency-0.0.2.tgz", + "integrity": "sha1-4skGB1Y85HpZ+VWeRacOJOTbS20=", + "dev": true + }, "locate-path": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", diff --git a/package.json b/package.json index c08a8536b..753631922 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "babel-jest": "^25.5.1", "comment-json": "^4.1.1", "cross-env": "^7.0.2", + "currency-symbol-map": "^5.0.1", "express": "^4.17.1", "file-system": "^2.2.2", "full-icu": "^1.3.1", @@ -37,6 +38,7 @@ "jest": "^25.5.2", "libphonenumber-js": "^1.9.6", "loader-utils": "^2.0.0", + "locale-currency": "0.0.2", "lodash.clonedeep": "^4.5.0", "postcss": "^8.2.1", "puppeteer": "^10.2.0", diff --git a/static/js/formatters-internal.js b/static/js/formatters-internal.js index 85e1d1d88..29fb2ab9b 100644 --- a/static/js/formatters-internal.js +++ b/static/js/formatters-internal.js @@ -7,7 +7,8 @@ import HoursStringsLocalizer from './hours/stringslocalizer.js'; import HoursTableBuilder from './hours/table/builder.js'; import { DayNames } from './hours/constants.js'; import { generateCTAFieldTypeLink } from './formatters/generate-cta-field-type-link'; - +import LocaleCurrency from 'locale-currency' +import getSymbolFromCurrency from 'currency-symbol-map' export function address(profile) { if (!profile.address) { @@ -522,6 +523,28 @@ export function price(fieldValue = {}, locale) { return price.toLocaleString(localeForFormatting, { style: 'currency', currency: currencyCode }); } +/** + * Returns a localized price range string for the given price range ($-$$$$) and country code (ISO format) + * @param {string} defaultPriceRange The price range from LiveAPI entity + * @param {string} countrycode The country code from LiveAPI entity (e.g. profile.address.countryCode) + * @return {string} The price range with correct currency symbol formatting according to country code + */ +export function priceRange(defaultPriceRange, countryCode) { + if (!defaultPriceRange || !countryCode) { + console.warn(`No price range or country code given.`); + return ''; + } + const currencyCode = LocaleCurrency.getCurrency(countryCode); + if (currencyCode) { + const currencySymbol = getSymbolFromCurrency(currencyCode); + if (currencySymbol) { + return defaultPriceRange.replace(/\$/g, currencySymbol); + } + } + console.warn(`Unable to determine currency symbol from ISO country code ${countryCode}.`); + return defaultPriceRange; +} + /** * Highlights snippets of the provided fieldValue according to the matched substrings. * Each match will be wrapped in tags. diff --git a/static/js/formatters.js b/static/js/formatters.js index 1d1084929..c4e054fbd 100644 --- a/static/js/formatters.js +++ b/static/js/formatters.js @@ -28,6 +28,7 @@ import { hoursList, generateCTAFieldTypeLink, price, + priceRange, highlightField, getYoutubeUrl } from './formatters-internal.js'; @@ -61,6 +62,7 @@ let Formatters = { hoursList, generateCTAFieldTypeLink, price, + priceRange, highlightField, getYoutubeUrl }; diff --git a/static/package-lock.json b/static/package-lock.json index 305a2c38f..c16fbc21a 100644 --- a/static/package-lock.json +++ b/static/package-lock.json @@ -667,15 +667,206 @@ } }, "@babel/plugin-transform-modules-commonjs": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.10.4.tgz", - "integrity": "sha512-Xj7Uq5o80HDLlW64rVfDBhao6OX89HKUmb+9vWYaLXBZOma4gA6tw4Ni1O5qVDoZWUV0fxMYA0aYzOawz0l+1w==", + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.15.4.tgz", + "integrity": "sha512-qg4DPhwG8hKp4BbVDvX1s8cohM8a6Bvptu4l6Iingq5rW+yRUAhe/YRup/YcW2zCOlrysEWVhftIcKzrEZv3sA==", "dev": true, "requires": { - "@babel/helper-module-transforms": "^7.10.4", - "@babel/helper-plugin-utils": "^7.10.4", - "@babel/helper-simple-access": "^7.10.4", + "@babel/helper-module-transforms": "^7.15.4", + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/helper-simple-access": "^7.15.4", "babel-plugin-dynamic-import-node": "^2.3.3" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.14.5.tgz", + "integrity": "sha512-9pzDqyc6OLDaqe+zbACgFkb6fKMNG6CObKpnYXChRsvYGyEdc7CA2BaqeOM+vOtCS5ndmJicPJhKAwYRI6UfFw==", + "dev": true, + "requires": { + "@babel/highlight": "^7.14.5" + } + }, + "@babel/generator": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.15.4.tgz", + "integrity": "sha512-d3itta0tu+UayjEORPNz6e1T3FtvWlP5N4V5M+lhp/CxT4oAA7/NcScnpRyspUMLK6tu9MNHmQHxRykuN2R7hw==", + "dev": true, + "requires": { + "@babel/types": "^7.15.4", + "jsesc": "^2.5.1", + "source-map": "^0.5.0" + } + }, + "@babel/helper-function-name": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.15.4.tgz", + "integrity": "sha512-Z91cOMM4DseLIGOnog+Z8OI6YseR9bua+HpvLAQ2XayUGU+neTtX+97caALaLdyu53I/fjhbeCnWnRH1O3jFOw==", + "dev": true, + "requires": { + "@babel/helper-get-function-arity": "^7.15.4", + "@babel/template": "^7.15.4", + "@babel/types": "^7.15.4" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.15.4.tgz", + "integrity": "sha512-1/AlxSF92CmGZzHnC515hm4SirTxtpDnLEJ0UyEMgTMZN+6bxXKg04dKhiRx5Enel+SUA1G1t5Ed/yQia0efrA==", + "dev": true, + "requires": { + "@babel/types": "^7.15.4" + } + }, + "@babel/helper-hoist-variables": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.15.4.tgz", + "integrity": "sha512-VTy085egb3jUGVK9ycIxQiPbquesq0HUQ+tPO0uv5mPEBZipk+5FkRKiWq5apuyTE9FUrjENB0rCf8y+n+UuhA==", + "dev": true, + "requires": { + "@babel/types": "^7.15.4" + } + }, + "@babel/helper-member-expression-to-functions": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.15.4.tgz", + "integrity": "sha512-cokOMkxC/BTyNP1AlY25HuBWM32iCEsLPI4BHDpJCHHm1FU2E7dKWWIXJgQgSFiu4lp8q3bL1BIKwqkSUviqtA==", + "dev": true, + "requires": { + "@babel/types": "^7.15.4" + } + }, + "@babel/helper-module-imports": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.15.4.tgz", + "integrity": "sha512-jeAHZbzUwdW/xHgHQ3QmWR4Jg6j15q4w/gCfwZvtqOxoo5DKtLHk8Bsf4c5RZRC7NmLEs+ohkdq8jFefuvIxAA==", + "dev": true, + "requires": { + "@babel/types": "^7.15.4" + } + }, + "@babel/helper-module-transforms": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.15.4.tgz", + "integrity": "sha512-9fHHSGE9zTC++KuXLZcB5FKgvlV83Ox+NLUmQTawovwlJ85+QMhk1CnVk406CQVj97LaWod6KVjl2Sfgw9Aktw==", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.15.4", + "@babel/helper-replace-supers": "^7.15.4", + "@babel/helper-simple-access": "^7.15.4", + "@babel/helper-split-export-declaration": "^7.15.4", + "@babel/helper-validator-identifier": "^7.14.9", + "@babel/template": "^7.15.4", + "@babel/traverse": "^7.15.4", + "@babel/types": "^7.15.4" + } + }, + "@babel/helper-optimise-call-expression": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.15.4.tgz", + "integrity": "sha512-E/z9rfbAOt1vDW1DR7k4SzhzotVV5+qMciWV6LaG1g4jeFrkDlJedjtV4h0i4Q/ITnUu+Pk08M7fczsB9GXBDw==", + "dev": true, + "requires": { + "@babel/types": "^7.15.4" + } + }, + "@babel/helper-plugin-utils": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.14.5.tgz", + "integrity": "sha512-/37qQCE3K0vvZKwoK4XU/irIJQdIfCJuhU5eKnNxpFDsOkgFaUAwbv+RYw6eYgsC0E4hS7r5KqGULUogqui0fQ==", + "dev": true + }, + "@babel/helper-replace-supers": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.15.4.tgz", + "integrity": "sha512-/ztT6khaXF37MS47fufrKvIsiQkx1LBRvSJNzRqmbyeZnTwU9qBxXYLaaT/6KaxfKhjs2Wy8kG8ZdsFUuWBjzw==", + "dev": true, + "requires": { + "@babel/helper-member-expression-to-functions": "^7.15.4", + "@babel/helper-optimise-call-expression": "^7.15.4", + "@babel/traverse": "^7.15.4", + "@babel/types": "^7.15.4" + } + }, + "@babel/helper-simple-access": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.15.4.tgz", + "integrity": "sha512-UzazrDoIVOZZcTeHHEPYrr1MvTR/K+wgLg6MY6e1CJyaRhbibftF6fR2KU2sFRtI/nERUZR9fBd6aKgBlIBaPg==", + "dev": true, + "requires": { + "@babel/types": "^7.15.4" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.15.4.tgz", + "integrity": "sha512-HsFqhLDZ08DxCpBdEVtKmywj6PQbwnF6HHybur0MAnkAKnlS6uHkwnmRIkElB2Owpfb4xL4NwDmDLFubueDXsw==", + "dev": true, + "requires": { + "@babel/types": "^7.15.4" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.14.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.9.tgz", + "integrity": "sha512-pQYxPY0UP6IHISRitNe8bsijHex4TWZXi2HwKVsjPiltzlhse2znVcm9Ace510VT1kxIHjGJCZZQBX2gJDbo0g==", + "dev": true + }, + "@babel/highlight": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.5.tgz", + "integrity": "sha512-qf9u2WFWVV0MppaL877j2dBtQIDgmidgjGk5VIMw3OadXvYaXn66U1BFlH2t4+t3i+8PhedppRv+i40ABzd+gg==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.14.5", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.15.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.15.5.tgz", + "integrity": "sha512-2hQstc6I7T6tQsWzlboMh3SgMRPaS4H6H7cPQsJkdzTzEGqQrpLDsE2BGASU5sBPoEQyHzeqU6C8uKbFeEk6sg==", + "dev": true + }, + "@babel/template": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.15.4.tgz", + "integrity": "sha512-UgBAfEa1oGuYgDIPM2G+aHa4Nlo9Lh6mGD2bDBGMTbYnc38vulXPuC1MGjYILIEmlwl6Rd+BPR9ee3gm20CBtg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.14.5", + "@babel/parser": "^7.15.4", + "@babel/types": "^7.15.4" + } + }, + "@babel/traverse": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.15.4.tgz", + "integrity": "sha512-W6lQD8l4rUbQR/vYgSuCAE75ADyyQvOpFVsvPPdkhf6lATXAsQIG9YdtOcu8BB1dZ0LKu+Zo3c1wEcbKeuhdlA==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.14.5", + "@babel/generator": "^7.15.4", + "@babel/helper-function-name": "^7.15.4", + "@babel/helper-hoist-variables": "^7.15.4", + "@babel/helper-split-export-declaration": "^7.15.4", + "@babel/parser": "^7.15.4", + "@babel/types": "^7.15.4", + "debug": "^4.1.0", + "globals": "^11.1.0" + } + }, + "@babel/types": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.15.4.tgz", + "integrity": "sha512-0f1HJFuGmmbrKTCZtbm3cU+b/AqdEYk5toj5iQur58xkVMlS0JWaKxTBSmCXd47uiN7vbcozAupm6Mvs80GNhw==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.14.9", + "to-fast-properties": "^2.0.0" + } + } } }, "@babel/plugin-transform-modules-systemjs": { @@ -2293,6 +2484,12 @@ } } }, + "currency-symbol-map": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/currency-symbol-map/-/currency-symbol-map-5.0.1.tgz", + "integrity": "sha512-2yHvoVBPIS9Dx7fjritu9l7UHWo4Z3D2oQnQj8AYwUee2j/PAcoHB4issCqwabfbipISZ/iG6vVYsfrAoks2eA==", + "dev": true + }, "d": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz", @@ -4385,6 +4582,12 @@ } } }, + "locale-currency": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/locale-currency/-/locale-currency-0.0.2.tgz", + "integrity": "sha1-4skGB1Y85HpZ+VWeRacOJOTbS20=", + "dev": true + }, "locate-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", diff --git a/static/package.json b/static/package.json index 137ff2e0d..ef9ed1aac 100644 --- a/static/package.json +++ b/static/package.json @@ -22,6 +22,7 @@ "babel-loader": "^8.1.0", "comment-json": "^4.1.1", "css-loader": "^3.4.2", + "currency-symbol-map": "^5.0.1", "esbuild-loader": "^2.13.1", "file-system": "^2.2.2", "fs-extra": "^9.0.1", @@ -32,6 +33,7 @@ "html-webpack-plugin": "^5.3.1", "jambo": "^1.12.0", "jsdom": "^16.4.0", + "locale-currency": "0.0.2", "mini-css-extract-plugin": "^1.6.0", "postcss": "^8.3.1", "resolve-url-loader": "^3.1.1", diff --git a/static/webpack/webpack.prod.js b/static/webpack/webpack.prod.js index 56789b453..36c6dc065 100644 --- a/static/webpack/webpack.prod.js +++ b/static/webpack/webpack.prod.js @@ -17,7 +17,7 @@ module.exports = (jamboConfig) => { { test: /\.js$/, exclude: [ - /node_modules\// + /node_modules\/(?!(currency-symbol-map)\/).*/ ], loader: 'babel-loader', options: { diff --git a/tests/static/js/formatters.js b/tests/static/js/formatters.js index 601a46cb3..b2c662001 100644 --- a/tests/static/js/formatters.js +++ b/tests/static/js/formatters.js @@ -108,6 +108,38 @@ describe('Formatters', () => { }); }); + describe('priceRange', () => { + it('Formats a price range in USD', () => { + const price = Formatters.priceRange('$', 'US'); + expect(price).toEqual('$'); + }); + + it('Formats a price range in EUR', () => { + const price = Formatters.priceRange('$$', 'BE'); + expect(price).toEqual('€€'); + }); + + it('Formats a price range in JPY', () => { + const price = Formatters.priceRange('$$$', 'JP'); + expect(price).toEqual('¥¥¥'); + }); + + it('Formats a price range in KRW', () => { + const price = Formatters.priceRange('$$$$', 'KR'); + expect(price).toEqual('₩₩₩₩'); + }); + + it('Formats a price range in GBP', () => { + const price = Formatters.priceRange('$', 'GB'); + expect(price).toEqual('£'); + }); + + it('Formats a price range in invalid input', () => { + const price = Formatters.priceRange('$', 'IDK'); + expect(price).toEqual('$'); + }); + }); + describe('highlightField', () => { it('Behaves correctly when there are no matchedSubstrings', () => { const plainText = 'No more straws';