@ices/react-locale
Version:
React components for locale
563 lines (554 loc) • 20.9 kB
JavaScript
import * as React from 'react';
import { useState, useEffect, useContext, useMemo, useRef, useCallback } from 'react';
function escapeRegExpCharacters(str) {
return str.replace(/[|/\\{}()[\]^$+*?.]/g, '\\$&').replace(/-/g, '\\x2d');
}
function getLocaleFromURL(queryName = 'lang') {
const escapedName = escapeRegExpCharacters(queryName);
const query = new RegExp(`\\b${escapedName}=([^&#]+)`).exec(location.search);
return query ? decodeURIComponent(query[1]) : '';
}
function getLocaleFromCookie(key = 'lang') {
const escapedKey = escapeRegExpCharacters(key);
const regx = new RegExp(`^${escapedKey}=(.+)`);
let cookie;
for (const item of document.cookie.split(/;\s*/)) {
if ((cookie = regx.exec(item))) {
return decodeURIComponent(cookie[1]);
}
}
return '';
}
function getLocaleFromLocalStorage(key = 'lang') {
if (window.localStorage) {
return localStorage.getItem(key);
}
return '';
}
function getLocaleFromBrowser() {
return navigator.language || navigator.userLanguage || '';
}
function determineLocale(options) {
const defaultKey = 'lang';
const { urlLocaleKey = defaultKey, cookieLocaleKey = defaultKey, storageLocaleKey = defaultKey, fallbackLocale, } = Object.assign({}, options);
for (const getLocale of [
() => getLocaleFromURL(urlLocaleKey),
() => getLocaleFromCookie(cookieLocaleKey),
() => getLocaleFromLocalStorage(storageLocaleKey),
() => getLocaleFromBrowser(),
]) {
const [locale] = normalizeLocale(getLocale());
if (locale) {
return locale;
}
}
if (fallbackLocale) {
const [fallback] = normalizeLocale(fallbackLocale);
if (fallback) {
return fallback;
}
}
return '';
}
function normalizeLocale(locale) {
if (typeof locale !== 'string') {
locale = '';
}
const [langArea] = locale.split('.');
const [lang, area = ''] = langArea.split(/[-_]/);
const lowerLang = lang.toLowerCase();
const upperArea = area.toUpperCase();
return [`${lowerLang}${area ? '-' + upperArea : ''}`, lowerLang, upperArea];
}
function isObject(obj) {
return obj !== null && typeof obj === 'object';
}
function hasOwnProp(obj, prop) {
return Object.hasOwn ? Object.hasOwn(obj, prop) : Object.getOwnPropertyNames(obj).includes(prop);
}
function hasOwnProperty(obj, prop) {
return !(obj === null || obj === undefined) && hasOwnProp(obj, prop);
}
let fallbackLocale = 'zh';
let currentLocale = determineLocale({ fallbackLocale });
let isUpdating = false;
const unregisterProp = '__localeChangeUnregister';
let listenerId = 1;
const localeChangeListeners = Object.create(null);
const loadErrorListeners = Object.create(null);
const loadFinishListeners = Object.create(null);
const loadStartListeners = Object.create(null);
const localeLoadStatus = Object.create(null);
const debugMessageFilter = {
warning: !window.__suspendReactLocaleWarning,
error: !window.__suspendReactLocaleError,
emptyKeyError: !window.__suspendReactLocaleEmptyKeyError,
};
function setDebugMessageFilter(filter) {
Object.assign(debugMessageFilter, filter);
}
function getFallbackLocale() {
return fallbackLocale;
}
function getLocale() {
return currentLocale;
}
function validateLocale(locale, isFallback, original = locale) {
if (!locale || typeof locale !== 'string') {
throw new Error(`${isFallback ? 'Fallback locale' : 'Locale'} code must be a valid string value. (currType: ${typeof original} , currValue: ${JSON.stringify(original)})`);
}
}
function setFallbackLocale(locale) {
if (locale !== fallbackLocale) {
const [localeCode] = normalizeLocale(locale);
validateLocale(localeCode, true, locale);
fallbackLocale = localeCode;
}
}
function getLocaleLoadKey(locale, fallback) {
return `${locale}$$${fallback}`;
}
function setLocale(locale) {
if (isUpdating || locale === currentLocale) {
return;
}
const [localeCode] = normalizeLocale(locale);
validateLocale(localeCode, false, locale);
isUpdating = true;
currentLocale = localeCode;
localeLoadStatus[getLocaleLoadKey(currentLocale, fallbackLocale)] = 'prepared';
for (const handle of Object.values(localeChangeListeners)) {
try {
handle(currentLocale);
}
catch (e) {
console.error(e);
}
}
isUpdating = false;
}
function registerListener(listeners, handle) {
if (typeof handle !== 'function') {
throw new Error('Handle is not a function');
}
let unregister = handle[unregisterProp];
if (!unregister) {
let id = listenerId++;
unregister = () => {
if (id) {
listeners[id][unregisterProp] = null;
delete listeners[id];
id = 0;
}
};
handle[unregisterProp] = unregister;
listeners[id] = handle;
}
return unregister;
}
function subscribe(handler) {
return registerListener(localeChangeListeners, handler);
}
function addLoadErrorListener(listener) {
return registerListener(loadErrorListeners, listener);
}
function addLoadStartListener(listener) {
return registerListener(loadStartListeners, listener);
}
function addLoadFinishListener(listener) {
return registerListener(loadFinishListeners, listener);
}
function emitLoadEvent(listeners, status, locale, fallback, error) {
const key = getLocaleLoadKey(locale, fallback);
if (localeLoadStatus[key] === status) {
return;
}
localeLoadStatus[key] = status;
for (const handle of Object.values(listeners)) {
try {
handle(locale, fallback, error);
}
catch (e) {
console.error(e);
}
}
}
function emitLoadError(locale, fallback, error) {
if (localeLoadStatus[getLocaleLoadKey(locale, fallback)] === 'finished') {
emitLoadEvent(loadErrorListeners, 'failed', locale, fallback, error);
}
}
function emitLoadStart(locale, fallback) {
if (localeLoadStatus[getLocaleLoadKey(locale, fallback)] === 'prepared') {
emitLoadEvent(loadStartListeners, 'loading', locale, fallback);
}
}
function emitLoadFinish(locale, fallback, error) {
if (localeLoadStatus[getLocaleLoadKey(locale, fallback)] === 'loading') {
emitLoadEvent(loadFinishListeners, 'finished', locale, fallback, error);
}
}
function fetchLocaleData(locale, fallback, fetch) {
emitLoadStart(locale, fallback);
return Promise.all([fetch(locale), fetch(fallback)]).then((res) => {
emitLoadFinish(locale, fallback);
return Object.assign({}, res[1], res[0]);
}, (err) => {
emitLoadFinish(locale, fallback, err);
emitLoadError(locale, fallback, err);
throw err;
});
}
const LocaleContext = React.createContext(currentLocale);
const LocaleProvider = function LocaleProvider({ value, ...props }) {
const [state, setState] = useState(value);
useEffect(() => subscribe(setState), []);
useEffect(() => setLocale(value), [value]);
return React.createElement(LocaleContext.Provider, { ...props, value: state });
};
const placeholder = (message, args) => {
if (!isObject(args[0]) || typeof message !== 'string') {
return message;
}
const data = args[0];
return message.replace(/(.?){\s*(.*?)\s*(\\|)}/g, (sub, g1, g2, g3) => {
if (g1 === '\\' || g3 === '\\') {
return g1 === '\\' ? sub.substring(1) : sub;
}
if (!g2 || !hasOwnProperty(data, g2)) {
return sub;
}
const str = data[g2];
return `${g1}${str !== undefined ? str : ''}`;
});
};
function toMessageValue(val) {
return typeof val === 'number' ? val : `${val}`;
}
function normalizeDefinitions(dataSet) {
const definitions = {};
for (const entry of Object.entries(Object.assign({}, dataSet))) {
const [locale, data] = entry;
if (data === null || typeof data !== 'object') {
continue;
}
const localeCode = normalizeLocale(locale)[0];
if (!hasOwnProperty(definitions, localeCode)) {
definitions[localeCode] = data;
}
}
return definitions;
}
function filterMessage(key, dataList, preference) {
for (const { locale, data } of dataList) {
if (hasOwnProperty(data, key)) {
const message = data[key];
if (locale !== preference && debugMessageFilter.warning) {
console.warn(`Missing message with key of "${key}" for locale ${preference}, using default message of locale ${locale} as fallback.`);
}
return { locale, message };
}
}
return { locale: preference, message: '' };
}
function getPluginTranslate(locale, fallback) {
return (key, definitions, options) => {
let plugins;
let pluginArgs;
let pluginFallback;
if (typeof options === 'string') {
pluginFallback = options;
}
else {
({ plugins, pluginArgs, fallback: pluginFallback } = Object.assign({}, options));
}
return withDefinitions(definitions, {
locale,
plugins,
fallback: pluginFallback || fallback,
})(key, pluginArgs);
};
}
function printErrorMessage(key, locale) {
if (debugMessageFilter.error) {
if (key || debugMessageFilter.emptyKeyError) {
console.error(`Unknown localized message with key of "${key}" for [${locale}]`);
return `<key>${key}</key>`;
}
}
return '';
}
function getLocaleMessage(key, pluginArgs, context, silent) {
const { locale, fallback, plugins, dataList } = context;
if (!key) {
return printErrorMessage(key, locale);
}
const localizedMessage = filterMessage(key, dataList, locale);
const { locale: messageLocale, message: messageDataValue } = localizedMessage;
const translate = getPluginTranslate(messageLocale, fallback);
const value = plugins.reduce((message, plugin) => plugin(message, [...pluginArgs], translate, key), toMessageValue(messageDataValue));
if (!silent && (value === '' || value === undefined)) {
return printErrorMessage(key, locale);
}
return `${value}`;
}
function getTranslateContext(data, context) {
const definitions = normalizeDefinitions(data);
const { plugins, locale, fallback } = context;
let usedPlugins = Array.isArray(plugins) ? plugins : [plugins];
usedPlugins = usedPlugins.filter((plugin) => typeof plugin === 'function');
const defaultPlugIndex = usedPlugins.indexOf(placeholder);
if (defaultPlugIndex !== -1) {
usedPlugins.splice(defaultPlugIndex, 1);
}
usedPlugins.push(placeholder);
const fallbackLang = fallback || getFallbackLocale();
const [preference, prefLang] = normalizeLocale(locale);
const [backLangArea, backLang] = normalizeLocale(fallbackLang);
const dataList = [preference, prefLang, backLangArea, backLang].reduce((list, loc) => {
if (!list.some(({ locale }) => locale === loc)) {
list.push({ locale: loc, data: definitions[loc] });
}
return list;
}, []);
return {
locale,
fallback: fallbackLang,
dataList,
plugins: usedPlugins,
};
}
function withDefinitions(data, context, fallbackTranslate) {
if (data === null) {
if (fallbackTranslate) {
fallbackTranslate.__toBeInvalidTranslateFunction = true;
}
return fallbackTranslate || (() => '');
}
const translateContext = getTranslateContext(data, context);
const translate = function (key, ...pluginArgs) {
return getLocaleMessage(key, pluginArgs, translateContext, translate.__toBeInvalidTranslateFunction);
};
translate.__boundLocale = translateContext.locale;
translate.__fallbackLocale = translateContext.fallback;
return translate;
}
function getUpdatedPlugins(prevPlugins, pluginsProp) {
let nextPlugins;
if (!pluginsProp) {
nextPlugins = [];
}
else if (typeof pluginsProp === 'function') {
nextPlugins = [pluginsProp];
}
else {
nextPlugins = [...pluginsProp];
}
if (prevPlugins.length !== nextPlugins.length) {
return nextPlugins;
}
for (let i = 0; i < prevPlugins.length; i++) {
if (prevPlugins[i] !== nextPlugins[i]) {
return nextPlugins;
}
}
return prevPlugins;
}
function useTranslate(context, fallbackTranslate) {
const pluginsRef = useRef([]);
const plugins = getUpdatedPlugins(pluginsRef.current, context.plugins);
pluginsRef.current = plugins;
const { data, locale, fallback } = context;
return useMemo(() => {
return withDefinitions(data, { locale, fallback, plugins }, fallbackTranslate);
}, [data, locale, fallback, plugins, fallbackTranslate]);
}
function useLocale(expectedLocale, definitions, plugins, fallback) {
const isAsyncResources = typeof definitions === 'function';
const fallbackLocale = useMemo(() => {
if (fallback) {
setFallbackLocale(fallback);
}
return getFallbackLocale();
}, [fallback]);
const [state, setState] = useState({
resourceLoader: isAsyncResources ? definitions : null,
asyncData: null,
asyncLocale: expectedLocale,
asyncFallback: fallbackLocale,
useFallbackTranslate: false,
});
let { asyncData, resourceLoader, asyncLocale, asyncFallback, useFallbackTranslate } = state;
if (isAsyncResources) {
if (resourceLoader !== definitions ||
asyncLocale !== expectedLocale ||
asyncFallback !== fallbackLocale) {
useFallbackTranslate = resourceLoader === definitions;
asyncData = null;
resourceLoader = definitions;
asyncLocale = expectedLocale;
asyncFallback = fallbackLocale;
setState({ resourceLoader, asyncData, asyncLocale, asyncFallback, useFallbackTranslate });
}
}
else if (resourceLoader || asyncData || useFallbackTranslate) {
useFallbackTranslate = false;
setState({ ...state, resourceLoader: null, asyncData: null, useFallbackTranslate: false });
}
const fallbackTranslateRef = useRef(null);
const translate = useTranslate({
data: isAsyncResources ? asyncData : definitions || null,
locale: expectedLocale,
fallback: fallbackLocale,
plugins,
}, useFallbackTranslate ? fallbackTranslateRef.current : null);
fallbackTranslateRef.current = translate;
useEffect(() => {
if (typeof definitions !== 'function') {
fallbackTranslateRef.current = null;
return;
}
fetchLocaleData(expectedLocale, fallbackLocale, definitions).then((data) => {
setState((prevState) => {
if (prevState.asyncLocale === expectedLocale &&
prevState.asyncFallback === fallbackLocale &&
prevState.resourceLoader === definitions) {
fallbackTranslateRef.current = null;
return { ...prevState, asyncData: data, useFallbackTranslate: false };
}
return prevState;
});
}, () => {
setState((prevState) => {
if (prevState.asyncLocale === expectedLocale &&
prevState.asyncFallback === fallbackLocale &&
prevState.resourceLoader === definitions) {
fallbackTranslateRef.current = null;
return { ...prevState, asyncData: null, useFallbackTranslate: false };
}
return prevState;
});
});
}, [definitions, expectedLocale, fallbackLocale]);
return [translate, expectedLocale, setLocale];
}
function useContextLocaleTrans(contextType, plugins, fallback, definitions) {
const locale = useContext(contextType);
let localeCode;
if (contextType !== LocaleContext) {
[localeCode] = normalizeLocale(locale);
validateLocale(localeCode, false, locale);
}
else {
localeCode = locale;
}
return useLocale(localeCode, definitions, plugins, fallback);
}
function useLocaleTrans(plugins, fallback, definitions) {
const [locale, setLocale] = useState(() => getLocale());
useEffect(() => subscribe(setLocale), []);
return useLocale(locale, definitions, plugins, fallback);
}
function withDefinitionsHook(definitions) {
let init = (initLocale, initialFallback) => {
init = () => { };
if (typeof initLocale === 'function') {
setLocale(initLocale());
}
if (typeof initialFallback === 'string') {
setFallbackLocale(initialFallback);
}
};
function useTrans(...args) {
const plugins = args[0];
let initLocale;
let initialFallback;
let fallback;
if (args.length >= 3) {
initLocale = typeof args[1] === 'function' ? args[1] : () => args[1];
initialFallback = args[2];
}
else {
fallback = args[1];
}
init(initLocale, initialFallback);
return useLocaleTrans(plugins, fallback, definitions);
}
return useTrans;
}
function withDefinitionsContextHook(definitions) {
return function useContextTrans(contextType, plugins, fallback) {
return useContextLocaleTrans(contextType, plugins, fallback, definitions);
};
}
function isTHMLText(text) {
return !/^<key>.*<\/key>$/.test(text) && /<.*?\/?>/.test(text);
}
function RenderTextOrHTML({ tagName, content, enableHTML, forwardedRef, ...props }) {
if (enableHTML && isTHMLText(content)) {
return React.createElement(tagName || 'span', {
...props,
ref: forwardedRef,
dangerouslySetInnerHTML: {
__html: content,
},
});
}
if (tagName) {
return React.createElement(tagName, { ...props, ref: forwardedRef }, content);
}
return content;
}
function LocaleTrans(props) {
const { id, fallback, plugins, data, definitions, ...rest } = props;
const [translate] = useLocaleTrans(plugins, fallback, definitions);
return (React.createElement(RenderTextOrHTML, { ...rest, content: translate(id, data) }));
}
function ContextLocaleTrans(props) {
const { id, fallback, plugins, data, definitions, contextType, ...rest } = props;
const [translate] = useContextLocaleTrans(contextType, plugins, fallback, definitions);
return (React.createElement(RenderTextOrHTML, { ...rest, content: translate(id, data) }));
}
function withDefinitionsComponent(definitions) {
return class TranslateComponent extends React.PureComponent {
static contextType = LocaleContext;
static enableDangerouslySetInnerHTML = false;
render() {
const { contextType, enableDangerouslySetInnerHTML: globalEnableHTML } = TranslateComponent;
const { enableHTML = globalEnableHTML, ...rest } = this.props;
return contextType ? (React.createElement(ContextLocaleTrans, { ...rest, enableHTML: enableHTML, contextType: contextType, definitions: definitions })) : (React.createElement(LocaleTrans, { ...rest, enableHTML: enableHTML, definitions: definitions }));
}
};
}
function useTranslator(locales) {
const loader = useCallback(async (lang) => {
let data;
if (typeof locales === 'function') {
return locales(lang);
}
if (locales && hasOwnProperty(locales, lang)) {
const resource = locales[lang];
if (typeof resource === 'function') {
({ default: data = {} } = await resource());
}
else {
data = resource || {};
}
}
else {
data = {};
}
return { [lang]: data };
}, [locales]);
return useMemo(() => ({
useTrans: withDefinitionsHook(loader),
useContextTrans: withDefinitionsContextHook(loader),
Translate: withDefinitionsComponent(loader),
}), [loader]);
}
const Trans = withDefinitionsComponent();
const useTrans = withDefinitionsHook();
const useContextTrans = withDefinitionsContextHook();
const definitions = {};
export { LocaleContext, LocaleProvider, Trans, addLoadErrorListener, addLoadFinishListener, addLoadStartListener, definitions, determineLocale, getFallbackLocale, getLocale, setDebugMessageFilter, setFallbackLocale, setLocale, subscribe, useContextTrans, useTrans, useTranslator, withDefinitionsComponent, withDefinitionsContextHook, withDefinitionsHook };
//# sourceMappingURL=index.es.js.map