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/package-lock.json b/packages/react-intl-universal/package-lock.json
index 0a2b624..9c4dcff 100644
--- a/packages/react-intl-universal/package-lock.json
+++ b/packages/react-intl-universal/package-lock.json
@@ -93,18 +93,49 @@
}
}
},
- "@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": "2.0.6",
@@ -2514,26 +2545,15 @@
"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": "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": {
@@ -6294,6 +6314,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 4f42217..94b6613 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"
diff --git a/packages/react-intl-universal/src/ReactIntlUniversal.js b/packages/react-intl-universal/src/ReactIntlUniversal.js
index 697dba6..f9af836 100644
--- a/packages/react-intl-universal/src/ReactIntlUniversal.js
+++ b/packages/react-intl-universal/src/ReactIntlUniversal.js
@@ -20,6 +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: 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
};
}
@@ -38,7 +39,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]) {
@@ -89,7 +90,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;
}
@@ -173,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 = {}) {
diff --git a/packages/react-intl-universal/test/index.test.js b/packages/react-intl-universal/test/index.test.js
index 2a7dc86..85ba9e9 100644
--- a/packages/react-intl-universal/test/index.test.js
+++ b/packages/react-intl-universal/test/index.test.js
@@ -114,13 +114,30 @@ 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 isyour message
"
+ );
+});
+
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>"
);
});
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 {