diff --git a/README.md b/README.md index 3e9d12af..97657f55 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" } @@ -178,6 +179,18 @@ 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: + +Locale data: +```json +{ "hello": "Hello, {name}" } +``` + +JS Code: +```js +intl.get('hello', { name: Tony }); // ["Hello ", React.createElement('b', null, Tony)] +``` ### Plural Form and Number Thousands Separators diff --git a/src/index.js b/src/index.js index dbd50ffc..27c67945 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 = Array.prototype.defaultMessage = String.prototype.defaultMessage; + 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..87eda849 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"); + 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", () => { intl.init({ locales, currentLocale: "en-US" }); expect(intl.formatMessage({ id: "not-exist-key" })).toBe( @@ -305,3 +316,20 @@ 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'); +}); + +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}", 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; }