UNPKG

react-i18next

Version:

Internationalization for react done right. Using the i18next i18n ecosystem.

152 lines 5.76 kB
import { useContext, useCallback, useMemo, useEffect, useRef, useState } from 'react'; import { useSyncExternalStore } from 'use-sync-external-store/shim'; import { getI18n, getDefaults, ReportNamespaces, I18nContext } from './context.js'; import { warnOnce, loadNamespaces, loadLanguages, hasLoadedNamespace, isString, isObject } from './utils.js'; const notReadyT = (k, optsOrDefaultValue) => { if (isString(optsOrDefaultValue)) return optsOrDefaultValue; if (isObject(optsOrDefaultValue) && isString(optsOrDefaultValue.defaultValue)) return optsOrDefaultValue.defaultValue; return Array.isArray(k) ? k[k.length - 1] : k; }; const notReadySnapshot = { t: notReadyT, ready: false }; const dummySubscribe = () => () => {}; export const useTranslation = (ns, props = {}) => { const { i18n: i18nFromProps } = props; const { i18n: i18nFromContext, defaultNS: defaultNSFromContext } = useContext(I18nContext) || {}; const i18n = i18nFromProps || i18nFromContext || getI18n(); if (i18n && !i18n.reportNamespaces) i18n.reportNamespaces = new ReportNamespaces(); if (!i18n) { warnOnce(i18n, 'NO_I18NEXT_INSTANCE', 'useTranslation: You will need to pass in an i18next instance by using initReactI18next'); } const i18nOptions = useMemo(() => ({ ...getDefaults(), ...i18n?.options?.react, ...props }), [i18n, props]); const { useSuspense, keyPrefix } = i18nOptions; const nsOrContext = ns || defaultNSFromContext || i18n?.options?.defaultNS; const unstableNamespaces = isString(nsOrContext) ? [nsOrContext] : nsOrContext || ['translation']; const namespaces = useMemo(() => unstableNamespaces, unstableNamespaces); i18n?.reportNamespaces?.addUsedNamespaces?.(namespaces); const revisionRef = useRef(0); const subscribe = useCallback(callback => { if (!i18n) return dummySubscribe; const { bindI18n, bindI18nStore } = i18nOptions; const wrappedCallback = () => { revisionRef.current += 1; callback(); }; if (bindI18n) i18n.on(bindI18n, wrappedCallback); if (bindI18nStore) i18n.store.on(bindI18nStore, wrappedCallback); return () => { if (bindI18n) bindI18n.split(' ').forEach(e => i18n.off(e, wrappedCallback)); if (bindI18nStore) bindI18nStore.split(' ').forEach(e => i18n.store.off(e, wrappedCallback)); }; }, [i18n, i18nOptions]); const snapshotRef = useRef(); const getSnapshot = useCallback(() => { if (!i18n) { return notReadySnapshot; } const calculatedReady = !!(i18n.isInitialized || i18n.initializedStoreOnce) && namespaces.every(n => hasLoadedNamespace(n, i18n, i18nOptions)); const currentLng = props.lng || i18n.language; const currentRevision = revisionRef.current; const lastSnapshot = snapshotRef.current; if (lastSnapshot && lastSnapshot.ready === calculatedReady && lastSnapshot.lng === currentLng && lastSnapshot.keyPrefix === keyPrefix && lastSnapshot.revision === currentRevision) { return lastSnapshot; } const calculatedT = i18n.getFixedT(currentLng, i18nOptions.nsMode === 'fallback' ? namespaces : namespaces[0], keyPrefix); const newSnapshot = { t: calculatedT, ready: calculatedReady, lng: currentLng, keyPrefix, revision: currentRevision }; snapshotRef.current = newSnapshot; return newSnapshot; }, [i18n, namespaces, keyPrefix, i18nOptions, props.lng]); const [loadCount, setLoadCount] = useState(0); const { t, ready } = useSyncExternalStore(subscribe, getSnapshot, getSnapshot); useEffect(() => { if (i18n && !ready && !useSuspense) { const onLoaded = () => setLoadCount(c => c + 1); if (props.lng) { loadLanguages(i18n, props.lng, namespaces, onLoaded); } else { loadNamespaces(i18n, namespaces, onLoaded); } } }, [i18n, props.lng, namespaces, ready, useSuspense, loadCount]); const finalI18n = i18n || {}; const wrapperRef = useRef(null); const wrapperLangRef = useRef(); const createI18nWrapper = original => { const descriptors = Object.getOwnPropertyDescriptors(original); if (descriptors.__original) delete descriptors.__original; const wrapper = Object.create(Object.getPrototypeOf(original), descriptors); if (!Object.prototype.hasOwnProperty.call(wrapper, '__original')) { try { Object.defineProperty(wrapper, '__original', { value: original, writable: false, enumerable: false, configurable: false }); } catch (_) {} } return wrapper; }; const ret = useMemo(() => { const original = finalI18n; const lang = original?.language; let i18nWrapper = original; if (original) { if (wrapperRef.current && wrapperRef.current.__original === original) { if (wrapperLangRef.current !== lang) { i18nWrapper = createI18nWrapper(original); wrapperRef.current = i18nWrapper; wrapperLangRef.current = lang; } else { i18nWrapper = wrapperRef.current; } } else { i18nWrapper = createI18nWrapper(original); wrapperRef.current = i18nWrapper; wrapperLangRef.current = lang; } } const arr = [t, i18nWrapper, ready]; arr.t = t; arr.i18n = i18nWrapper; arr.ready = ready; return arr; }, [t, finalI18n, ready, finalI18n.resolvedLanguage, finalI18n.language, finalI18n.languages]); if (i18n && useSuspense && !ready) { throw new Promise(resolve => { const onLoaded = () => resolve(); if (props.lng) { loadLanguages(i18n, props.lng, namespaces, onLoaded); } else { loadNamespaces(i18n, namespaces, onLoaded); } }); } return ret; };