Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enhance locale fallback mechanism #104

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
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
3 changes: 2 additions & 1 deletion examples/browser-example/public/locales/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@
"COUPON": "Coupon expires at {expires, time, medium}",
"SALE_PRICE": "The price is {price, number, USD}",
"PHOTO": "You have {photoNum, plural, =0 {no photos.} =1 {one photo.} other {# photos.}}",
"MESSAGE_NOT_IN_COMPONENT": "react-intl-universal is able to internationalize message not in React.Component"
"MESSAGE_NOT_IN_COMPONENT": "react-intl-universal is able to internationalize message not in React.Component",
"FALLBACK_ONLY_EXIST_IN_EN": "Fallback Test, Only exist in English."
}
3 changes: 2 additions & 1 deletion examples/browser-example/public/locales/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,6 @@
"TIME": "时间是{theTime, time}",
"SALE_PRICE": "售价{price, number, CNY}",
"PHOTO": "你有{photoNum, number}张照片",
"MESSAGE_NOT_IN_COMPONENT": "react-intl-universal可以在非React.Component的js文件进行国际化"
"MESSAGE_NOT_IN_COMPONENT": "react-intl-universal可以在非React.Component的js文件进行国际化",
"FALLBACK_NOT_EXIST_IN_ZH_TW": "文案兜底测试"
}
35 changes: 26 additions & 9 deletions examples/browser-example/src/App.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import intl from "react-intl-universal";
import intl from "react-intl-universal"
import _ from "lodash";
import http from "axios";
import React, { Component } from "react";
Expand All @@ -8,6 +8,7 @@ import HtmlComponent from "./Html";
import DateComponent from "./Date";
import CurrencyComponent from "./Currency";
import MessageNotInComponent from "./MessageNotInComponent";
import FallbackComponent from "./Fallback";
import "./app.css";

const SUPPOER_LOCALES = [
Expand Down Expand Up @@ -56,6 +57,7 @@ class App extends Component {
<DateComponent />
<CurrencyComponent />
<MessageNotInComponent />
<FallbackComponent />
</div>
);
}
Expand All @@ -69,18 +71,33 @@ class App extends Component {
currentLocale = "en-US";
}

let fallbackLocales = currentLocale === 'zh-TW' ? ['zh-CN', 'en-US'] : ['en-US'];

let allLocales = [currentLocale, ...fallbackLocales];

function getMessagesByLocale(locale) {
return http.get(`locales/${locale}.json`);
}

let promises = allLocales.map(item => getMessagesByLocale(item));

http
.get(`locales/${currentLocale}.json`)
.then(res => {
console.log("App locale data", res.data);
.all(promises)
.then(http.spread((...results) => {
// init method will load CLDR locale data according to currentLocale
let locales = {}
for (let i=0; i<allLocales.length; i++) {
Object.assign(locales, {
[allLocales[i]]: results[i].data
})
}

return intl.init({
currentLocale,
locales: {
[currentLocale]: res.data
}
});
})
fallbackLocales,
locales
})
}))
.then(() => {
// After loading CLDR locale data, start to render
this.setState({ initDone: true });
Expand Down
16 changes: 16 additions & 0 deletions examples/browser-example/src/Fallback.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import React, { Component } from 'react'
import intl from 'react-intl-universal';

class FallbackComponent extends Component {
render () {
return (
<div>
<div className="title">Language Fallback:</div>
<div>{intl.get('FALLBACK_NOT_EXIST_IN_ZH_TW')}</div>
<div>{intl.get('FALLBACK_ONLY_EXIST_IN_EN')}</div>
</div>
)
}
}

export default FallbackComponent;
2 changes: 2 additions & 0 deletions examples/node-js-example/src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import HtmlComponent from "./Html";
import DateComponent from "./Date";
import CurrencyComponent from "./Currency";
import MessageNotInComponent from "./MessageNotInComponent";
import FallbackComponent from "./Fallback";
import IntlPolyfill from "intl";

// For Node.js, common locales should be added in the application
Expand Down Expand Up @@ -61,6 +62,7 @@ class App extends Component {
<DateComponent />
<CurrencyComponent />
<MessageNotInComponent />
<FallbackComponent />
</div>
);
}
Expand Down
16 changes: 16 additions & 0 deletions examples/node-js-example/src/Fallback.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import React, { Component } from 'react'
import intl from 'react-intl-universal';

class FallbackComponent extends Component {
render () {
return (
<div>
<div className="title">Language Fallback:</div>
<div>{intl.get('FALLBACK_NOT_EXIST_IN_ZH_TW')}</div>
<div>{intl.get('FALLBACK_ONLY_EXIST_IN_EN')}</div>
</div>
)
}
}

export default FallbackComponent;
3 changes: 2 additions & 1 deletion examples/node-js-example/src/locales/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@
"COUPON": "Coupon expires at {expires, time, medium}",
"SALE_PRICE": "The price is {price, number, USD}",
"PHOTO": "You have {photoNum, plural, =0 {no photos.} =1 {one photo.} other {# photos.}}",
"MESSAGE_NOT_IN_COMPONENT": "react-intl-universal is able to internationalize message not in React.Component"
"MESSAGE_NOT_IN_COMPONENT": "react-intl-universal is able to internationalize message not in React.Component",
"FALLBACK_ONLY_EXIST_IN_EN": "Fallback Test, Only exist in English."
}
3 changes: 2 additions & 1 deletion examples/node-js-example/src/locales/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,6 @@
"TIME": "时间是{theTime, time}",
"SALE_PRICE": "售价{price, number, CNY}",
"PHOTO": "你有{photoNum, number}张照片",
"MESSAGE_NOT_IN_COMPONENT": "react-intl-universal可以在非React.Component的js文件进行国际化"
"MESSAGE_NOT_IN_COMPONENT": "react-intl-universal可以在非React.Component的js文件进行国际化",
"FALLBACK_NOT_EXIST_IN_ZH_TW": "文案兜底测试"
}
21 changes: 19 additions & 2 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ class ReactIntlUniversal {
warningHandler: console.warn, // ability to accumulate missing messages using third party services like Sentry
escapeHtml: true, // disable escape html in variable mode
commonLocaleDataUrls: COMMON_LOCALE_DATA_URLS,
fallbackLocale: null, // Locale to use if a key is not found in the current locale
fallbackLocale: '', /** @deprecated Locale to use if a key is not found in the current locale */
Copy link

@the-architect the-architect Apr 30, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would suggest not to deprecate this option. It seems that your use case is very specific and this config would require changes for an otherwise stable feature.

A suggestion: Your case could also be handled by using a comma separated fallbackLocale sequence like: 'zh-CN,en-US' which would require some extra effort for your case, but would keep the config simple for most users.

fallbackLocales: [], // Locales to use if a key is not found in the current locale, such as ['zh-CN', 'en-US'] will use the key in locale 'zh-CN' first, if the specific key not exist in 'zh-CN', will fallback to locale 'en-US'
};
}

Expand All @@ -69,8 +70,23 @@ class ReactIntlUniversal {
return "";
}
let msg = this.getDescendantProp(locales[currentLocale], key);

if (msg == null) {
if (this.options.fallbackLocale) {
if (Array.isArray(this.options.fallbackLocales) && this.options.fallbackLocales.length > 0) {
for (let locale of this.options.fallbackLocales) {
msg = this.getDescendantProp(locales[locale], key);
if (msg == null) {
this.options.warningHandler(
`react-intl-universal key "${key}" not defined in ${currentLocale} or the fallback locale, ${locale}`
);
} else {
break;
}
}
if (msg == null) {
return "";
}
} else if (this.options.fallbackLocale) {
msg = this.getDescendantProp(locales[this.options.fallbackLocale], key);
if (msg == null) {
this.options.warningHandler(
Expand All @@ -85,6 +101,7 @@ class ReactIntlUniversal {
return "";
}
}

if (variables) {
variables = Object.assign({}, variables);
// HTML message with variables. Escape it to avoid XSS attack.
Expand Down
9 changes: 8 additions & 1 deletion test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ import intl from "../src/index";
import zhCN from "./locales/zh-CN";
import enUS from "./locales/en-US";
import enUSMore from "./locales/en-US-more";
import zhTW from "./locales/zh-TW";

const locales = {
"en-US": enUS,
"zh-CN": zhCN
"zh-CN": zhCN,
"zh-TW": zhTW
};

test("Set specific locale", () => {
Expand Down Expand Up @@ -305,3 +307,8 @@ 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("Test for enhanced fallback mechnanism", () => {
intl.init({ locales, currentLocale: "zh-TW", fallbackLocales: ["zh-CN", "en-US"] });
expect(intl.get("NOT_IN_zh-TW")).toBe("NOT_IN_zh-TW");
expect(intl.get("ONLY_IN_ENGLISH")).toBe("ONLY_IN_ENGLISH");
})
3 changes: 2 additions & 1 deletion test/locales/zh-CN.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ module.exports = ({
"COUPON": "优惠卷将在{expires, time, medium}过期",
"TIME": "时间是{theTime, time}",
"SALE_PRICE": "售价{price, number, CNY}",
"PHOTO": "你有{num}张照片"
"PHOTO": "你有{num}张照片",
"NOT_IN_zh-TW": "NOT_IN_zh-TW"
});
12 changes: 12 additions & 0 deletions test/locales/zh-TW.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
module.exports = ({
"HELLO": "你好, {name}。歡迎來到{where}!",
"TIP": "這是<span style='color:red'>HTML</span>",
"TIP_VAR": "這是<span style='color:red'>{message}</span>",
"SALE_START": "拍賣將在{start, date}開始",
"SALE_END": "拍賣將在{end, date, full}結束",
"COUPON": "優惠卷將在{expires, time, medium}過期",
"TIME": "時間是{theTime, time}",
"SALE_PRICE": "售價{price, number, TWD}",
"PHOTO": "你有{photoNum, number}張照片",
"MESSAGE_NOT_IN_COMPONENT": "react-intl-universal可以在非React.Component的js文件進行國際化"
})
6 changes: 4 additions & 2 deletions typings/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,8 @@ declare module "react-intl-universal" {
* @param {string} options.currentLocale Current locale such as 'en-US'
* @param {Object} options.locales App locale data like {"en-US":{"key1":"value1"},"zh-CN":{"key1":"值1"}}
* @param {Object} options.warningHandler Ability to accumulate missing messages using third party services like Sentry
* @param {string} options.fallbackLocale Fallback locale such as 'zh-CN' to use if a key is not found in the current locale
* @param {string} options.fallbackLocale @deprecated Fallback locale such as 'zh-CN' to use if a key is not found in the current locale
* @param {string[]} options.fallbackLocales Locales to use if a key is not found in the current locale, such as ['zh-CN', 'en-US'] will use the key in locale 'zh-CN' first, if the specific key not exist in 'zh-CN', will fallback to locale 'en-US
* @param {boolean} options.escapeHtml To escape html. Default value is true.
* @returns {Promise}
*/
Expand All @@ -85,7 +86,8 @@ declare module "react-intl-universal" {
export interface ReactIntlUniversalOptions {
currentLocale?: string;
locales?: { [key: string]: any };
fallbackLocale?: string;
fallbackLocale?: string; /** @deprecated Please use fallbackLocales instead **/
fallbackLocales?: string[];
commonLocaleDataUrls?: { [key: string]: string };
cookieLocaleKey?: string;
urlLocaleKey?: string;
Expand Down