From d085bc527c435934205285fd4548e0039d9be276 Mon Sep 17 00:00:00 2001 From: Endurance Idehen Date: Thu, 1 Nov 2018 11:36:49 -0500 Subject: [PATCH 1/5] react element support --- src/index.js | 42 ++++++++++++++++++++++++++++++++++++++---- test/index.js | 18 ++++++++++++++++++ typings/index.d.ts | 19 ++++++++++++------- 3 files changed, 68 insertions(+), 11 deletions(-) diff --git a/src/index.js b/src/index.js index dbd50ffc..6e111041 100644 --- a/src/index.js +++ b/src/index.js @@ -29,7 +29,6 @@ const COMMON_LOCALE_DATA_URLS = { tr: "https://g.alicdn.com/react-intl-universal/locale-data/1.0.0/tr.js", }; - const isBrowser = !isElectron() && !!(typeof window !== 'undefined' && window.document && window.document.createElement); @@ -38,6 +37,8 @@ String.prototype.defaultMessage = String.prototype.d = function (msg) { return this || msg || ""; }; +Array.prototype.d = String.prototype.defaultMessage = String.prototype.d; + class ReactIntlUniversal { constructor() { this.options = { @@ -56,7 +57,7 @@ class ReactIntlUniversal { * Get the formatted message by key * @param {string} key The string representing key in locale data file * @param {Object} variables Variables in message - * @returns {string} message + * @returns {(string | T)[]} message */ get(key, variables) { invariant(key, "key is required"); @@ -69,6 +70,7 @@ class ReactIntlUniversal { return ""; } let msg = this.getDescendantProp(locales[currentLocale], key); + if (msg == null) { if (this.options.fallbackLocale) { msg = this.getDescendantProp(locales[this.options.fallbackLocale], key); @@ -85,8 +87,33 @@ class ReactIntlUniversal { return ""; } } + + let tokenDelimiter; + let tokenizedValues; + let elements; + if (variables) { variables = Object.assign({}, variables); + const uid = Math.floor(Math.random() * 0x10000000000).toString(16); + tokenDelimiter = `@__${uid}__@`; + tokenizedValues = {}; + elements = {}; + + const generateToken = (() => { + let counter = 0; + return () => `ELEMENT-${uid}-${(counter += 1)}`; + })(); + + Object.keys(variables).forEach(name => { + const value = variables[name]; + if (React.isValidElement(value)) { + const token = generateToken(); + tokenizedValues[name] = tokenDelimiter + token + tokenDelimiter; + elements[token] = value; + } else { + tokenizedValues[name] = value; + } + }); // HTML message with variables. Escape it to avoid XSS attack. for (let i in variables) { let value = variables[i]; @@ -101,10 +128,17 @@ class ReactIntlUniversal { variables[i] = value; } } - + let hasElements = elements && Object.keys(elements).length > 0; try { const msgFormatter = new IntlMessageFormat(msg, currentLocale, formats); - return msgFormatter.format(variables); + const finalMessage = msgFormatter.format(tokenizedValues || variables); + if (hasElements) { + return finalMessage + .split(tokenDelimiter) + .filter(part => !!part) + .map(part => elements[part] || part); + } + return finalMessage; } catch (err) { this.options.warningHandler( `react-intl-universal format message failed for key='${key}'.`, diff --git a/test/index.js b/test/index.js index 41f46772..58f7483f 100644 --- a/test/index.js +++ b/test/index.js @@ -45,6 +45,17 @@ test("react-intl mirror API formatMessage:variables", () => { ).toBe(intl.get("HELLO", { name })); }); +test("react-intl mirror API formatMessage:variables with React Elements", () => { + intl.init({ locales, currentLocale: "en-US" }); + const name = React.createElement('b',null, "Tony"); + expect( + intl.formatMessage( + { id: "HELLO", defaultMessage: `Hello, {name}` }, + { name } + ) + ).toBe(intl.get("HELLO", { name })); +}); + test("react-intl mirror API formatMessage:defaultMessage", () => { intl.init({ locales, currentLocale: "en-US" }); expect(intl.formatMessage({ id: "not-exist-key" })).toBe( @@ -305,3 +316,10 @@ test("Uses default message if key not found in fallbackLocale", () => { expect(intl.get("not-exist-key").defaultMessage("this is default msg")).toBe("this is default msg"); }); +test("get with React Element as variable", () => { + intl.init({ locales, currentLocale: "en-US" }); + const name = React.createElement('b', null, "Tony"); + const answer = intl.get("HELLO",{ name }); + expect(answer[0]).toBe("Hello, "); + expect(answer[1].props.children).toBe('Tony'); +}); \ No newline at end of file diff --git a/typings/index.d.ts b/typings/index.d.ts index b244d061..262f5f31 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -11,22 +11,22 @@ declare module "react-intl-universal" { /** * Provide React-Intl compatibility, same as getHTML(...) API. */ - export function formatHTMLMessage(messageDescriptor: ReactIntlUniversalMessageDescriptor): string; + export function formatHTMLMessage(messageDescriptor: ReactIntlUniversalMessageDescriptor): string | [string | JSX.Element]; /** * Provide React-Intl compatibility, same as getHTML(...) API. */ - export function formatHTMLMessage(messageDescriptor: ReactIntlUniversalMessageDescriptor, variables: any): string; + export function formatHTMLMessage(messageDescriptor: ReactIntlUniversalMessageDescriptor, variables: any): string | [string | JSX.Element]; /** * Provide React-Intl compatibility, same as get(...) API. */ - export function formatMessage(messageDescriptor: ReactIntlUniversalMessageDescriptor): string; + export function formatMessage(messageDescriptor: ReactIntlUniversalMessageDescriptor): string | [string | JSX.Element]; /** * Provide React-Intl compatibility, same as get(...) API. */ - export function formatMessage(messageDescriptor: ReactIntlUniversalMessageDescriptor, variables: any): string; + export function formatMessage(messageDescriptor: ReactIntlUniversalMessageDescriptor, variables: any): string | [string | JSX.Element]; /** * Get the formatted message by key @@ -41,7 +41,7 @@ declare module "react-intl-universal" { * @param {Object} variables Variables in message * @returns {string} message */ - export function get(key: string, value: any): string; + export function get(key: string, value: any): string | [string | JSX.Element]; /** * Get the formatted html message by key. @@ -99,6 +99,11 @@ declare module "react-intl-universal" { } declare interface String { - defaultMessage(msg: string | JSX.Element): string; - d(msg: string | JSX.Element): string; + defaultMessage(msg: string | JSX.Element): string; + d(msg: string | JSX.Element): string; +} + +declare interface Array { + defaultMessage(msg: string | JSX.Element): string; + d(msg: string | JSX.Element): string; } From 25c121101c5652b592de1d66e40beda5850a40c3 Mon Sep 17 00:00:00 2001 From: Endurance Idehen Date: Thu, 1 Nov 2018 13:12:59 -0500 Subject: [PATCH 2/5] fix tests --- src/index.js | 2 +- test/index.js | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/index.js b/src/index.js index 6e111041..27c67945 100644 --- a/src/index.js +++ b/src/index.js @@ -37,7 +37,7 @@ String.prototype.defaultMessage = String.prototype.d = function (msg) { return this || msg || ""; }; -Array.prototype.d = String.prototype.defaultMessage = String.prototype.d; +Array.prototype.d = Array.prototype.defaultMessage = String.prototype.defaultMessage; class ReactIntlUniversal { constructor() { diff --git a/test/index.js b/test/index.js index 58f7483f..ab3318fa 100644 --- a/test/index.js +++ b/test/index.js @@ -47,13 +47,13 @@ test("react-intl mirror API formatMessage:variables", () => { test("react-intl mirror API formatMessage:variables with React Elements", () => { intl.init({ locales, currentLocale: "en-US" }); - const name = React.createElement('b',null, "Tony"); - expect( - intl.formatMessage( - { id: "HELLO", defaultMessage: `Hello, {name}` }, - { name } - ) - ).toBe(intl.get("HELLO", { name })); + const name = React.createElement('b', null, "Tony"); + const answer = intl.formatMessage( + { id: "HELLO", defaultMessage: `Hello, {name}` }, + { name } + ); + expect(answer[0]).toBe("Hello, "); + expect(answer[1].props.children).toBe('Tony'); }); test("react-intl mirror API formatMessage:defaultMessage", () => { From 10da2a8210063b0eb6f887db4310e5398072f9c5 Mon Sep 17 00:00:00 2001 From: Endurance Idehen Date: Fri, 2 Nov 2018 13:47:28 -0500 Subject: [PATCH 3/5] Add react element support to readme --- README.md | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 3e9d12af..8ece4507 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ - Display numbers, currency, dates and times for different locales. - Pluralize labels in strings. - Support variables in message. +- Support React Elements as variables in message. - Support HTML in message. - Automatically load [Common Locale Data Repository (CLDR)](http://cldr.unicode.org/) locale data on demand. It's used for displaying numbers, currency, dates and times accordingly. - Support for 150+ languages. @@ -134,10 +135,10 @@ class App extends Component { } ``` - ### HTML Message -As shown in above example, the `get` method returns string message. For HTML message, use `getHTML` instead. For example, - +The `get` method returns string message. For HTML message, use `getHTML` instead. + Avoid using this in favor of React Element embedding, if at all possible. For example, + Locale data: ```json { "TIP": "This is HTML" } @@ -179,6 +180,21 @@ intl.get('HELLO', {name:'Tony', where:'Alibaba'}) // "Hello, Tony. Welcome to Al ``` +### React Element in Message +When you want to add rich text as a variable to a message, use the following method: + +Locale data: +```json +{ "hello": "Hello, {name}" } +``` + +JS Code: +```js +intl.get('hello', { name: Tony }); // ["Hello ", React.createElement('b', null, Tony)] +``` + +You can embed a React Element as a variable as part of a message. React will know what to do with this, since this version returns an Array with strings and React Element objects, preserving the node structure. + ### Plural Form and Number Thousands Separators Locale data: From 5fbe698c2d0b3d034beda21b5e3bfac20bcce264 Mon Sep 17 00:00:00 2001 From: Endurance Idehen Date: Fri, 2 Nov 2018 13:50:16 -0500 Subject: [PATCH 4/5] fix readme --- README.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/README.md b/README.md index 8ece4507..97657f55 100644 --- a/README.md +++ b/README.md @@ -179,7 +179,6 @@ JS code: intl.get('HELLO', {name:'Tony', where:'Alibaba'}) // "Hello, Tony. Welcome to Alibaba!" ``` - ### React Element in Message When you want to add rich text as a variable to a message, use the following method: @@ -193,8 +192,6 @@ JS Code: intl.get('hello', { name: Tony }); // ["Hello ", React.createElement('b', null, Tony)] ``` -You can embed a React Element as a variable as part of a message. React will know what to do with this, since this version returns an Array with strings and React Element objects, preserving the node structure. - ### Plural Form and Number Thousands Separators Locale data: From 2229ab6659d95a335a508e09dc62eb083b3a2cda Mon Sep 17 00:00:00 2001 From: Endurance Idehen Date: Fri, 2 Nov 2018 15:45:22 -0500 Subject: [PATCH 5/5] add another test --- test/index.js | 10 ++++++++++ test/locales/en-US.js | 1 + 2 files changed, 11 insertions(+) diff --git a/test/index.js b/test/index.js index ab3318fa..87eda849 100644 --- a/test/index.js +++ b/test/index.js @@ -322,4 +322,14 @@ test("get with React Element as variable", () => { const answer = intl.get("HELLO",{ name }); expect(answer[0]).toBe("Hello, "); expect(answer[1].props.children).toBe('Tony'); +}); + +test("get with React element and normal variable", () => { + intl.init({ locales, currentLocale: "en-US" }); + const name = React.createElement('b', null, "Tony"); + const year = new Date().getFullYear(); + const answer = intl.get("HELLO_ADV",{ name, year }); + expect(answer[0]).toBe("Hello, "); + expect(answer[1].props.children).toBe('Tony'); + expect(answer[2]).toBe(`. The year is ${year}.`); }); \ No newline at end of file diff --git a/test/locales/en-US.js b/test/locales/en-US.js index 623beee6..a8dab7ed 100644 --- a/test/locales/en-US.js +++ b/test/locales/en-US.js @@ -1,6 +1,7 @@ module.exports = ({ "SIMPLE": "Simple", "HELLO": "Hello, {name}", + "HELLO_ADV": "Hello, {name}. The year is {year}.", "TIP": "This is HTML", "TIP_VAR": "This is{message}", "SALE_START": "Sale begins {start, date}",