diff --git a/examples/browser-example/public/locales/en-US.json b/examples/browser-example/public/locales/en-US.json index cbe2c9f..9471132 100644 --- a/examples/browser-example/public/locales/en-US.json +++ b/examples/browser-example/public/locales/en-US.json @@ -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." } \ No newline at end of file diff --git a/examples/browser-example/public/locales/zh-CN.json b/examples/browser-example/public/locales/zh-CN.json index 34fb02a..3c6d357 100644 --- a/examples/browser-example/public/locales/zh-CN.json +++ b/examples/browser-example/public/locales/zh-CN.json @@ -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": "文案兜底测试" } \ No newline at end of file diff --git a/examples/browser-example/src/App.js b/examples/browser-example/src/App.js index 5a8e962..cbb0d4c 100644 --- a/examples/browser-example/src/App.js +++ b/examples/browser-example/src/App.js @@ -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"; @@ -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 = [ @@ -56,6 +57,7 @@ class App extends Component { + ); } @@ -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 { // After loading CLDR locale data, start to render this.setState({ initDone: true }); diff --git a/examples/browser-example/src/Fallback.js b/examples/browser-example/src/Fallback.js new file mode 100644 index 0000000..ec41bdd --- /dev/null +++ b/examples/browser-example/src/Fallback.js @@ -0,0 +1,16 @@ +import React, { Component } from 'react' +import intl from 'react-intl-universal'; + +class FallbackComponent extends Component { + render () { + return ( +
+
Language Fallback:
+
{intl.get('FALLBACK_NOT_EXIST_IN_ZH_TW')}
+
{intl.get('FALLBACK_ONLY_EXIST_IN_EN')}
+
+ ) + } +} + +export default FallbackComponent; \ No newline at end of file diff --git a/examples/node-js-example/src/App.js b/examples/node-js-example/src/App.js index d9cb5db..39a05e5 100644 --- a/examples/node-js-example/src/App.js +++ b/examples/node-js-example/src/App.js @@ -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 @@ -61,6 +62,7 @@ class App extends Component { + ); } diff --git a/examples/node-js-example/src/Fallback.js b/examples/node-js-example/src/Fallback.js new file mode 100644 index 0000000..ec41bdd --- /dev/null +++ b/examples/node-js-example/src/Fallback.js @@ -0,0 +1,16 @@ +import React, { Component } from 'react' +import intl from 'react-intl-universal'; + +class FallbackComponent extends Component { + render () { + return ( +
+
Language Fallback:
+
{intl.get('FALLBACK_NOT_EXIST_IN_ZH_TW')}
+
{intl.get('FALLBACK_ONLY_EXIST_IN_EN')}
+
+ ) + } +} + +export default FallbackComponent; \ No newline at end of file diff --git a/examples/node-js-example/src/locales/en-US.json b/examples/node-js-example/src/locales/en-US.json index cbe2c9f..9471132 100644 --- a/examples/node-js-example/src/locales/en-US.json +++ b/examples/node-js-example/src/locales/en-US.json @@ -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." } \ No newline at end of file diff --git a/examples/node-js-example/src/locales/zh-CN.json b/examples/node-js-example/src/locales/zh-CN.json index 34fb02a..3c6d357 100644 --- a/examples/node-js-example/src/locales/zh-CN.json +++ b/examples/node-js-example/src/locales/zh-CN.json @@ -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": "文案兜底测试" } \ No newline at end of file diff --git a/src/index.js b/src/index.js index dbd50ff..4f0dbc8 100644 --- a/src/index.js +++ b/src/index.js @@ -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 */ + 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' }; } @@ -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( @@ -85,6 +101,7 @@ class ReactIntlUniversal { return ""; } } + if (variables) { variables = Object.assign({}, variables); // HTML message with variables. Escape it to avoid XSS attack. diff --git a/test/index.js b/test/index.js index 41f4677..60013e9 100644 --- a/test/index.js +++ b/test/index.js @@ -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", () => { @@ -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"); +}) \ No newline at end of file diff --git a/test/locales/zh-CN.js b/test/locales/zh-CN.js index 76cc519..56a4f94 100644 --- a/test/locales/zh-CN.js +++ b/test/locales/zh-CN.js @@ -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" }); diff --git a/test/locales/zh-TW.js b/test/locales/zh-TW.js new file mode 100644 index 0000000..6f921b4 --- /dev/null +++ b/test/locales/zh-TW.js @@ -0,0 +1,12 @@ +module.exports = ({ + "HELLO": "你好, {name}。歡迎來到{where}!", + "TIP": "這是HTML", + "TIP_VAR": "這是{message}", + "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文件進行國際化" +}) \ No newline at end of file diff --git a/typings/index.d.ts b/typings/index.d.ts index 3371228..0469a0a 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -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} */ @@ -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;