react-i18next
Version:
Internationalization for react done right. Using the i18next i18n ecosystem.
152 lines (129 loc) • 4.76 kB
JavaScript
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;
};