import React from 'react';
import IntlMessageFormat from 'intl-messageformat';
import escapeHtml from 'escape-html';
import cookie from 'cookie';
import queryParser from 'querystring';
import invariant from 'invariant';
import { getLocalStorageItem } from 'utils/local-storage';
import { merge } from 'lodash';
String.prototype.defaultMessage = function (msg) {
return this || msg || '';
};
class SLI18n {
constructor() {
this.options = {
currentLocale: null,
locales: {},
warningHandler: function warn(...msg) {
console.warn(...msg);
},
escapeHtml: true,
fallbackLocale: null,
};
}
* 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
*/
get(key, variables) {
invariant(key, 'key is required');
const { locales, currentLocale, formats } = this.options;
if (!locales || !locales[currentLocale]) {
this.options.warningHandler(
`translate locales data "${currentLocale}" not exists.`
);
return '';
}
let msg = this.getDescendantProp(locales[currentLocale], key);
if (msg == null || msg === '') {
if (this.options.fallbackLocale) {
msg = this.getDescendantProp(locales[this.options.fallbackLocale], key);
if (msg == null) {
this.options.warningHandler(
`translate key "${key}" not defined in ${currentLocale}`
);
msg = key;
}
} else {
this.options.warningHandler(
`translate key "${key}" not defined in ${currentLocale}`
);
msg = key;
}
}
if (variables) {
variables = { ...variables };
Object.keys(variables).forEach((i) => {
let value = variables[i];
if (
this.options.escapeHtml === true &&
(typeof value === 'string' || value instanceof String) &&
value.indexOf('<') >= 0 &&
value.indexOf('>') >= 0
) {
value = escapeHtml(value);
}
variables[i] = value;
});
}
try {
const msgFormatter = new IntlMessageFormat(msg, currentLocale, formats);
return msgFormatter.format(variables);
} catch (err) {
this.options.warningHandler(
`translate format message failed for key='${key}'.`,
err.message
);
return msg;
}
}
* Get the formatted html message by key.
* @param {string} key The string representing key in locale data file
* @param {Object} variables Variables in message
* @returns {React.Element} message
*/
getHTML(key, variables) {
const msg = this.get(key, variables);
if (msg) {
const el = React.createElement('span', {
dangerouslySetInnerHTML: {
__html: msg,
},
});
const defaultMessage = () => el;
return {
defaultMessage,
d: defaultMessage,
...el,
};
}
return '';
}
* As same as get(...) API
* @param {Object} options
* @param {string} options.id
* @param {string} options.defaultMessage
* @param {Object} variables Variables in message
* @returns {string} message
*/
formatMessage(messageDescriptor, variables) {
const { id, defaultMessage } = messageDescriptor;
return this.get(id, variables).defaultMessage(defaultMessage);
}
* As same as getHTML(...) API
* @param {Object} options
* @param {string} options.id
* @param {React.Element} options.defaultMessage
* @param {Object} variables Variables in message
* @returns {React.Element} message
*/
formatHTMLMessage(messageDescriptor, variables) {
const { id, defaultMessage } = messageDescriptor;
return this.getHTML(id, variables).defaultMessage(defaultMessage);
}
* Helper: determine user's locale via URL, cookie, localStorage, and browser's language.
* You may not this API, if you have other rules to determine user's locale.
* @param {string} options.urlLocaleKey URL's query Key to determine locale. Example: if URL=http://localhost?lang=en, then set it 'lang'
* @param {string} options.cookieLocaleKey Cookie's Key to determine locale. Example: if cookie=lang:en, then set it 'lang'
* @param {string} options.localStorageLocaleKey LocalStorage's Key to determine locale such as 'lang'
* @returns {string} determined locale such as 'en'
*/
determineLocale(options = {}) {
return (
this.getLocaleFromLocalStorage(options) ||
this.getLocaleFromURL(options) ||
this.getLocaleFromCookie(options) ||
this.getLocaleFromBrowser()
);
}
* Initialize properties and load CLDR locale data according to currentLocale
* @param {Object} options
* @param {string} options.currentLocale Current locale such as 'en'
* @param {string} options.locales App locale data like {"en":{"key1":"value1"},"zh-hans":{"key1":"值1"}}
* @returns {Promise}
*/
init(options = {}) {
invariant(options.currentLocale, 'options.currentLocale is required');
invariant(options.locales, 'options.locales is required');
Object.assign(this.options, options);
this.options.formats = {
...this.options.formats,
};
return new Promise((resolve) => {
resolve();
});
}
* Get the initial options
*/
getInitOptions() {
return this.options;
}
* Load more locales after init
*/
load(locales) {
merge(this.options.locales, locales);
}
getLocaleFromCookie(options) {
const { cookieLocaleKey } = options;
if (cookieLocaleKey) {
const params = cookie.parse(document.cookie);
return params && params[cookieLocaleKey];
}
}
getLocaleFromLocalStorage(options) {
const { localStorageLocaleKey } = options;
if (localStorageLocaleKey && window.localStorage) {
return getLocalStorageItem(localStorageLocaleKey);
}
}
getLocaleFromURL(options) {
const { urlLocaleKey } = options;
if (urlLocaleKey) {
const query = window.location.search.split('?');
if (query.length >= 2) {
const params = queryParser.parse(query[1]);
return params && params[urlLocaleKey];
}
}
}
getDescendantProp(locale, key) {
if (locale[key]) {
return locale[key];
}
const msg = key
.split('.')
.reduce((a, b) => (a !== undefined ? a[b] : a), locale);
return msg;
}
getLocaleFromBrowser() {
return navigator.language || navigator.userLanguage;
}
}
export default SLI18n;