From dfbfeae24abc757f4de7f1184d3f7cacdf21c081 Mon Sep 17 00:00:00 2001 From: Chevindu Wickramathilaka Date: Sun, 4 Feb 2024 16:29:48 +0530 Subject: [PATCH 1/6] Update `intl-messageformat` to 10.5.11 Signed-off-by: Chevindu Wickramathilaka --- .../react-intl-universal/package-lock.json | 86 ++++++++++++------- packages/react-intl-universal/package.json | 2 +- 2 files changed, 54 insertions(+), 34 deletions(-) diff --git a/packages/react-intl-universal/package-lock.json b/packages/react-intl-universal/package-lock.json index 7311523..1dddc3e 100644 --- a/packages/react-intl-universal/package-lock.json +++ b/packages/react-intl-universal/package-lock.json @@ -1,21 +1,52 @@ { "name": "react-intl-universal", - "version": "2.9.0", + "version": "2.10.1", "lockfileVersion": 1, "requires": true, "dependencies": { - "@formatjs/intl-unified-numberformat": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/@formatjs/intl-unified-numberformat/-/intl-unified-numberformat-3.3.7.tgz", - "integrity": "sha512-KnWgLRHzCAgT9eyt3OS34RHoyD7dPDYhRcuKn+/6Kv2knDF8Im43J6vlSW6Hm1w63fNq3ZIT1cFk7RuVO3Psag==", + "@formatjs/ecma402-abstract": { + "version": "1.18.2", + "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.18.2.tgz", + "integrity": "sha512-+QoPW4csYALsQIl8GbN14igZzDbuwzcpWrku9nyMXlaqAlwRBgl5V+p0vWMGFqHOw37czNXaP/lEk4wbLgcmtA==", "requires": { - "@formatjs/intl-utils": "^2.3.0" + "@formatjs/intl-localematcher": "0.5.4", + "tslib": "^2.4.0" } }, - "@formatjs/intl-utils": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@formatjs/intl-utils/-/intl-utils-2.3.0.tgz", - "integrity": "sha512-KWk80UPIzPmUg+P0rKh6TqspRw0G6eux1PuJr+zz47ftMaZ9QDwbGzHZbtzWkl5hgayM/qrKRutllRC7D/vVXQ==" + "@formatjs/fast-memoize": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-2.2.0.tgz", + "integrity": "sha512-hnk/nY8FyrL5YxwP9e4r9dqeM6cAbo8PeU9UjyXojZMNvVad2Z06FAVHyR3Ecw6fza+0GH7vdJgiKIVXTMbSBA==", + "requires": { + "tslib": "^2.4.0" + } + }, + "@formatjs/icu-messageformat-parser": { + "version": "2.7.6", + "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.7.6.tgz", + "integrity": "sha512-etVau26po9+eewJKYoiBKP6743I1br0/Ie00Pb/S/PtmYfmjTcOn2YCh2yNkSZI12h6Rg+BOgQYborXk46BvkA==", + "requires": { + "@formatjs/ecma402-abstract": "1.18.2", + "@formatjs/icu-skeleton-parser": "1.8.0", + "tslib": "^2.4.0" + } + }, + "@formatjs/icu-skeleton-parser": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.0.tgz", + "integrity": "sha512-QWLAYvM0n8hv7Nq5BEs4LKIjevpVpbGLAJgOaYzg9wABEoX1j0JO1q2/jVkO6CVlq0dbsxZCngS5aXbysYueqA==", + "requires": { + "@formatjs/ecma402-abstract": "1.18.2", + "tslib": "^2.4.0" + } + }, + "@formatjs/intl-localematcher": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.5.4.tgz", + "integrity": "sha512-zTwEpWOzZ2CiKcB93BLngUX59hQkuZjT2+SAQEscSm52peDW/getsawMcWF1rGRpMCX6D7nSJA3CzJ8gn13N/g==", + "requires": { + "tslib": "^2.4.0" + } }, "abab": { "version": "1.0.4", @@ -1979,31 +2010,15 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "dev": true }, - "intl": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/intl/-/intl-1.2.5.tgz", - "integrity": "sha1-giRKIZDE5Bn4Nx9ao02qNCDiq94=" - }, - "intl-format-cache": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/intl-format-cache/-/intl-format-cache-4.3.1.tgz", - "integrity": "sha512-OEUYNA7D06agqPOYhbTkl0T8HA3QKSuwWh1HiClEnpd9vw7N+3XsQt5iZ0GUEchp5CW1fQk/tary+NsbF3yQ1Q==" - }, "intl-messageformat": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-7.8.4.tgz", - "integrity": "sha512-yS0cLESCKCYjseCOGXuV4pxJm/buTfyCJ1nzQjryHmSehlptbZbn9fnlk1I9peLopZGGbjj46yHHiTAEZ1qOTA==", + "version": "10.5.11", + "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.5.11.tgz", + "integrity": "sha512-eYq5fkFBVxc7GIFDzpFQkDOZgNayNTQn4Oufe8jw6YY6OHVw70/4pA3FyCsQ0Gb2DnvEJEMmN2tOaXUGByM+kg==", "requires": { - "intl-format-cache": "^4.2.21", - "intl-messageformat-parser": "^3.6.4" - } - }, - "intl-messageformat-parser": { - "version": "3.6.4", - "resolved": "https://registry.npmjs.org/intl-messageformat-parser/-/intl-messageformat-parser-3.6.4.tgz", - "integrity": "sha512-RgPGwue0mJtoX2Ax8EmMzJzttxjnva7gx0Q7mKJ4oALrTZvtmCeAw5Msz2PcjW4dtCh/h7vN/8GJCxZO1uv+OA==", - "requires": { - "@formatjs/intl-unified-numberformat": "^3.2.0" + "@formatjs/ecma402-abstract": "1.18.2", + "@formatjs/fast-memoize": "2.2.0", + "@formatjs/icu-messageformat-parser": "2.7.6", + "tslib": "^2.4.0" } }, "invariant": { @@ -4618,6 +4633,11 @@ "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=", "dev": true }, + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + }, "tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", diff --git a/packages/react-intl-universal/package.json b/packages/react-intl-universal/package.json index a0aa3b1..f5344b0 100644 --- a/packages/react-intl-universal/package.json +++ b/packages/react-intl-universal/package.json @@ -36,7 +36,7 @@ "dependencies": { "cookie": "^0.3.1", "escape-html": "^1.0.3", - "intl-messageformat": "^7.8.4", + "intl-messageformat": "^10.5.11", "invariant": "^2.2.2", "lodash.merge": "^4.6.2", "object-keys": "^1.0.11", From cf240b1f87ffff6f69cc4545f2eaaed59dcb1f75 Mon Sep 17 00:00:00 2001 From: Chevindu Wickramathilaka Date: Sun, 4 Feb 2024 16:32:39 +0530 Subject: [PATCH 2/6] Add new init option `xmlParser` to support XML tags in template strings Signed-off-by: Chevindu Wickramathilaka --- .../react-intl-universal/src/ReactIntlUniversal.js | 5 +++-- packages/react-intl-universal/test/index.test.js | 10 ++++++++++ packages/react-intl-universal/test/locales/en-US.js | 1 + packages/react-intl-universal/typings/index.d.ts | 2 ++ 4 files changed, 16 insertions(+), 2 deletions(-) diff --git a/packages/react-intl-universal/src/ReactIntlUniversal.js b/packages/react-intl-universal/src/ReactIntlUniversal.js index 46f0a29..d735c81 100644 --- a/packages/react-intl-universal/src/ReactIntlUniversal.js +++ b/packages/react-intl-universal/src/ReactIntlUniversal.js @@ -21,6 +21,7 @@ class ReactIntlUniversal { fallbackLocale: null, // Locale to use if a key is not found in the current locale debug: false, // If debugger mode is on, the message will be wrapped by a span dataKey: 'data-i18n-key', // If debugger mode is on, the message will be wrapped by a span with this data key + xmlParser: { span: chunks => `${chunks}` }, // If there are XML tags present in the message, parsers should be added per tag }; } @@ -39,7 +40,7 @@ class ReactIntlUniversal { } } invariant(key, "key is required"); - const { locales, currentLocale, formats } = this.options; + const { locales, currentLocale, formats, xmlParser } = this.options; // 1. check if the locale data and key exists if (!locales || !locales[currentLocale]) { @@ -91,7 +92,7 @@ class ReactIntlUniversal { let finalMsg; if (variables) { // format message with variables const msgFormatter = new IntlMessageFormat(msg, currentLocale, formats); - finalMsg = msgFormatter.format(variables); + finalMsg = msgFormatter.format(Object.assign(xmlParser, variables)); } else { // no variables, just return the message finalMsg = msg; } diff --git a/packages/react-intl-universal/test/index.test.js b/packages/react-intl-universal/test/index.test.js index 1a1d92c..1e12e18 100644 --- a/packages/react-intl-universal/test/index.test.js +++ b/packages/react-intl-universal/test/index.test.js @@ -114,6 +114,16 @@ test("HTML Message with variables", () => { ); }); +test("HTML Message with variables and custom XML parser", () => { + intl.init({ locales, currentLocale: "en-US", xmlParser: { div: children => '
' + children + '
' } }); + let reactEl = intl.getHTML("TIP_VAR_DIV", { + message: "your message" + }); + expect(reactEl.props.dangerouslySetInnerHTML.__html).toBe( + "This is
your message
" + ); +}); + test("HTML Message with XSS attack", () => { intl.init({ locales, currentLocale: "en-US" }); let reactEl = intl.getHTML("TIP_VAR", { diff --git a/packages/react-intl-universal/test/locales/en-US.js b/packages/react-intl-universal/test/locales/en-US.js index 96b32f3..895d35f 100644 --- a/packages/react-intl-universal/test/locales/en-US.js +++ b/packages/react-intl-universal/test/locales/en-US.js @@ -3,6 +3,7 @@ module.exports = ({ "HELLO": "Hello, {name}", "TIP": "This is HTML", "TIP_VAR": "This is{message}", + "TIP_VAR_DIV": "This is
{message}
", "SALE_START": "Sale begins {start, date}", "SALE_END": "Sale begins {start, date, long}", "COUPON": "Coupon expires at {expires, time, medium}", diff --git a/packages/react-intl-universal/typings/index.d.ts b/packages/react-intl-universal/typings/index.d.ts index a167551..4520017 100644 --- a/packages/react-intl-universal/typings/index.d.ts +++ b/packages/react-intl-universal/typings/index.d.ts @@ -67,6 +67,7 @@ declare module "react-intl-universal" { * @param {string} options.fallbackLocale Fallback locale such as 'zh-CN' to use if a key is not found in the current locale * @param {boolean} options.escapeHtml To escape html. Default value is true. * @param {boolean} options.debug debug mode + * @param {Object} options.xmlParser Parser for the XML tags used in messages (eg: `{ span: children => '' + children + '' }`) * @returns {Promise} */ export function init(options: ReactIntlUniversalOptions): Promise; @@ -89,6 +90,7 @@ declare module "react-intl-universal" { escapeHtml?: boolean; debug?: boolean; dataKey?: string; + xmlParser?: { [tag: string]: (children: string) => string }; } export interface ReactIntlUniversalMessageDescriptor { From 0d76e9ff87ded5d4cf4127ffaaf6a05fd1735031 Mon Sep 17 00:00:00 2001 From: Chevindu Wickramathilaka Date: Mon, 26 Feb 2024 10:36:59 +0530 Subject: [PATCH 3/6] Sync package-lock file with package.json Signed-off-by: Chevindu Wickramathilaka --- packages/react-intl-universal/package-lock.json | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/packages/react-intl-universal/package-lock.json b/packages/react-intl-universal/package-lock.json index c4d5fc1..9c4dcff 100644 --- a/packages/react-intl-universal/package-lock.json +++ b/packages/react-intl-universal/package-lock.json @@ -93,10 +93,10 @@ } } }, - "@formatjs/intl-unified-numberformat": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/@formatjs/intl-unified-numberformat/-/intl-unified-numberformat-3.3.7.tgz", - "integrity": "sha512-KnWgLRHzCAgT9eyt3OS34RHoyD7dPDYhRcuKn+/6Kv2knDF8Im43J6vlSW6Hm1w63fNq3ZIT1cFk7RuVO3Psag==", + "@formatjs/ecma402-abstract": { + "version": "1.18.2", + "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.18.2.tgz", + "integrity": "sha512-+QoPW4csYALsQIl8GbN14igZzDbuwzcpWrku9nyMXlaqAlwRBgl5V+p0vWMGFqHOw37czNXaP/lEk4wbLgcmtA==", "requires": { "@formatjs/intl-localematcher": "0.5.4", "tslib": "^2.4.0" @@ -2545,11 +2545,6 @@ "side-channel": "^1.0.4" } }, - "intl-format-cache": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/intl-format-cache/-/intl-format-cache-4.3.1.tgz", - "integrity": "sha512-OEUYNA7D06agqPOYhbTkl0T8HA3QKSuwWh1HiClEnpd9vw7N+3XsQt5iZ0GUEchp5CW1fQk/tary+NsbF3yQ1Q==" - }, "intl-messageformat": { "version": "10.5.11", "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.5.11.tgz", From 6f5cc8bd349a32a7747bedf380279c21ff463bce Mon Sep 17 00:00:00 2001 From: Chevindu Wickramathilaka Date: Mon, 26 Feb 2024 10:39:10 +0530 Subject: [PATCH 4/6] Update tests with XML escaping Signed-off-by: Chevindu Wickramathilaka --- packages/react-intl-universal/test/index.test.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/react-intl-universal/test/index.test.js b/packages/react-intl-universal/test/index.test.js index 040e8dc..85ba9e9 100644 --- a/packages/react-intl-universal/test/index.test.js +++ b/packages/react-intl-universal/test/index.test.js @@ -125,12 +125,19 @@ test("HTML Message with variables and custom XML parser", () => { }); test("HTML Message with XSS attack", () => { - intl.init({ locales, currentLocale: "en-US" }); + intl.init({ + locales, + currentLocale: "en-US", + xmlParser: { + span: children => `${children}`, + script: children => `` + } + }); let reactEl = intl.getHTML("TIP_VAR", { - message: "alert(1)" + message: "" }); expect(reactEl.props.dangerouslySetInnerHTML.__html).toBe( - "This is<sctipt>alert(1)</script>" + "This is<script>alert(1)</script>" ); }); From db8f74d7e90752ddb68b69a67c1b330ccd582cdc Mon Sep 17 00:00:00 2001 From: Chevindu Wickramathilaka Date: Mon, 26 Feb 2024 10:40:27 +0530 Subject: [PATCH 5/6] Rename variable name for more clarity Signed-off-by: Chevindu Wickramathilaka --- packages/react-intl-universal/src/ReactIntlUniversal.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-intl-universal/src/ReactIntlUniversal.js b/packages/react-intl-universal/src/ReactIntlUniversal.js index c6206f6..c485776 100644 --- a/packages/react-intl-universal/src/ReactIntlUniversal.js +++ b/packages/react-intl-universal/src/ReactIntlUniversal.js @@ -20,7 +20,7 @@ class ReactIntlUniversal { fallbackLocale: null, // Locale to use if a key is not found in the current locale debug: false, // If debugger mode is on, the message will be wrapped by a span dataKey: 'data-i18n-key', // If debugger mode is on, the message will be wrapped by a span with this data key - xmlParser: { span: chunks => `${chunks}` }, // If there are XML tags present in the message, parsers should be added per tag + xmlParser: { span: children => `${children}` }, // If there are XML tags present in the message, parsers should be added per tag. https://formatjs.io/docs/intl-messageformat/#rich-text-support }; } From f0310ce3cb11ba2dd335f2fbf4d238556cceba33 Mon Sep 17 00:00:00 2001 From: Chevindu Wickramathilaka Date: Wed, 10 Apr 2024 10:17:41 +0530 Subject: [PATCH 6/6] Update README and JSDoc regarding `xmlParser` Signed-off-by: Chevindu Wickramathilaka --- README.md | 19 ++++++++++++++++++- .../src/ReactIntlUniversal.js | 9 +++++---- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 5af6e18..1690f9c 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ - Display numbers, currency, dates and times for different locales. - Pluralize labels in strings. - Support variables in message. -- Support HTML in message. +- Support HTML in message ([read more](#html-message)). - Support for 150+ languages. - Runs in the browser and Node.js. - Message format is strictly implemented by [ICU standards](http://userguide.icu-project.org/formatparse/messages). @@ -216,6 +216,23 @@ JS code: intl.getHTML('TIP'); // {React.Element} ``` +Before using HTML tags inside the messages, you need to define how the HTML/XML tags you intend to use should be parsed, during the library initializing. + +Defining the XML parser: + +```js +intl.init({ + // ... + xmlParser: { + div: children => '
' + children + '
', + span: children => '' + children + '', + // ... + } +}) +``` + +Use this feature with caution as there are a set of restrictions. [Click here to learn more](https://formatjs.io/docs/intl-messageformat/#rich-text-support). + ### Helper [react-intl-universal](https://www.npmjs.com/package/react-intl-universal) provides a utility helping developer determine the user's `currentLocale`. As the running examples, when user select a new locale, it redirect user new location like `http://localhost:3000?lang=en-US`. Then, we can use `intl.determineLocale` to get the locale from URL. It can also support determine user's locale via cookie, localStorage, and browser default language. Refer to the APIs section for more detail. diff --git a/packages/react-intl-universal/src/ReactIntlUniversal.js b/packages/react-intl-universal/src/ReactIntlUniversal.js index c485776..f9af836 100644 --- a/packages/react-intl-universal/src/ReactIntlUniversal.js +++ b/packages/react-intl-universal/src/ReactIntlUniversal.js @@ -174,10 +174,11 @@ class ReactIntlUniversal { /** * Initialize properties and load CLDR locale data according to currentLocale - * @param {Object} options - * @param {string} options.currentLocale Current locale such as 'en-US' - * @param {any} options.locales App locale data like {"en-US":{"key1":"value1"},"zh-CN":{"key1":"值1"}} - * @param {boolean} [options.debug] debug mode + * @param {Object} options Initialization options + * @param {string} options.currentLocale Current locale (eg. `'en-US'`) + * @param {any} options.locales App locale data (eg. `{ "en-US": { "key1": "value1" }, "zh-CN": { "key1": "值1" }}`) + * @param {boolean} [options.debug] Enable debug mode + * @param {{[tag: string]: (children: any) => string}} [options.xmlParser] Indicates how to parse XML tags inside the messages. (eg. `{ span: children => '' + children + '' }`) * @returns {Promise} */ init(options = {}) {