UNPKG

react-i18next

Version:

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

152 lines (129 loc) 4.76 kB
import { useContext, useCallback, useMemo, useEffect, useRef, useState } from 'react'; // eslint-disable-next-line import/no-extraneous-dependencies 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 namespaces = useMemo(() => { const nsOrContext = ns || defaultNSFromContext || i18n?.options?.defaultNS; return isString(nsOrContext) ? [nsOrContext] : nsOrContext || ['translation']; }, [ns, defaultNSFromContext, i18n]); 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 // Check revision ) { 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, // Store revision }; snapshotRef.current = newSnapshot; return newSnapshot; }, [i18n, namespaces, keyPrefix, i18nOptions, props.lng]); // We still need a state to manually trigger a re-render on load when the store doesn't emit an event. 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 ret = useMemo(() => { const arr = [t, finalI18n, ready]; arr.t = t; arr.i18n = finalI18n; arr.ready = ready; return arr; }, [t, finalI18n, ready]); 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; };