UNPKG

@ices/react-locale

Version:
563 lines (554 loc) 20.9 kB
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