Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 16 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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 <span style='color:red'>HTML</span>" }
Expand Down Expand Up @@ -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: <b>Tony</b> }); // ["Hello ", React.createElement('b', null, Tony)]
```

### Plural Form and Number Thousands Separators

Expand Down
42 changes: 38 additions & 4 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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 = {
Expand All @@ -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");
Expand All @@ -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);
Expand All @@ -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];
Expand All @@ -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}'.`,
Expand Down
28 changes: 28 additions & 0 deletions test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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}.`);
});
1 change: 1 addition & 0 deletions test/locales/en-US.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
module.exports = ({
"SIMPLE": "Simple",
"HELLO": "Hello, {name}",
"HELLO_ADV": "Hello, {name}. The year is {year}.",
"TIP": "This is <span>HTML</span>",
"TIP_VAR": "This is<span>{message}</span>",
"SALE_START": "Sale begins {start, date}",
Expand Down
19 changes: 12 additions & 7 deletions typings/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -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;
}