-
-
Notifications
You must be signed in to change notification settings - Fork 1k
/
useTranslation.js
165 lines (142 loc) · 5.58 KB
/
useTranslation.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
import { useState, useEffect, useContext, useRef, useCallback } from 'react';
import { getI18n, getDefaults, ReportNamespaces, I18nContext } from './context.js';
import {
warnOnce,
loadNamespaces,
loadLanguages,
hasLoadedNamespace,
isString,
isObject,
} from './utils.js';
const usePrevious = (value, ignore) => {
const ref = useRef();
useEffect(() => {
ref.current = ignore ? ref.current : value;
}, [value, ignore]);
return ref.current;
};
const alwaysNewT = (i18n, language, namespace, keyPrefix) =>
i18n.getFixedT(language, namespace, keyPrefix);
const useMemoizedT = (i18n, language, namespace, keyPrefix) =>
useCallback(alwaysNewT(i18n, language, namespace, keyPrefix), [
i18n,
language,
namespace,
keyPrefix,
]);
export const useTranslation = (ns, props = {}) => {
// assert we have the needed i18nInstance
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('You will need to pass in an i18next instance by using initReactI18next');
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 retNotReady = [notReadyT, {}, false];
retNotReady.t = notReadyT;
retNotReady.i18n = {};
retNotReady.ready = false;
return retNotReady;
}
if (i18n.options.react?.wait)
warnOnce(
'It seems you are still using the old wait option, you may migrate to the new useSuspense behaviour.',
);
const i18nOptions = { ...getDefaults(), ...i18n.options.react, ...props };
const { useSuspense, keyPrefix } = i18nOptions;
// prepare having a namespace
let namespaces = ns || defaultNSFromContext || i18n.options?.defaultNS;
namespaces = isString(namespaces) ? [namespaces] : namespaces || ['translation'];
// report namespaces as used
i18n.reportNamespaces.addUsedNamespaces?.(namespaces);
// are we ready? yes if all namespaces in first language are loaded already (either with data or empty object on failed load)
const ready =
(i18n.isInitialized || i18n.initializedStoreOnce) &&
namespaces.every((n) => hasLoadedNamespace(n, i18n, i18nOptions));
// binding t function to namespace (acts also as rerender trigger *when* args have changed)
const memoGetT = useMemoizedT(
i18n,
props.lng || null,
i18nOptions.nsMode === 'fallback' ? namespaces : namespaces[0],
keyPrefix,
);
// using useState with a function expects an initializer, not the function itself:
const getT = () => memoGetT;
const getNewT = () =>
alwaysNewT(
i18n,
props.lng || null,
i18nOptions.nsMode === 'fallback' ? namespaces : namespaces[0],
keyPrefix,
);
const [t, setT] = useState(getT);
let joinedNS = namespaces.join();
if (props.lng) joinedNS = `${props.lng}${joinedNS}`;
const previousJoinedNS = usePrevious(joinedNS);
const isMounted = useRef(true);
useEffect(() => {
const { bindI18n, bindI18nStore } = i18nOptions;
isMounted.current = true;
// if not ready and not using suspense load the namespaces
// in side effect and do not call resetT if unmounted
if (!ready && !useSuspense) {
if (props.lng) {
loadLanguages(i18n, props.lng, namespaces, () => {
if (isMounted.current) setT(getNewT);
});
} else {
loadNamespaces(i18n, namespaces, () => {
if (isMounted.current) setT(getNewT);
});
}
}
if (ready && previousJoinedNS && previousJoinedNS !== joinedNS && isMounted.current) {
setT(getNewT);
}
const boundReset = () => {
if (isMounted.current) setT(getNewT);
};
// bind events to trigger change, like languageChanged
if (bindI18n) i18n?.on(bindI18n, boundReset);
if (bindI18nStore) i18n?.store.on(bindI18nStore, boundReset);
// unbinding on unmount
return () => {
isMounted.current = false;
if (i18n) bindI18n?.split(' ').forEach((e) => i18n.off(e, boundReset));
if (bindI18nStore && i18n)
bindI18nStore.split(' ').forEach((e) => i18n.store.off(e, boundReset));
};
}, [i18n, joinedNS]); // re-run effect whenever list of namespaces changes
// t is correctly initialized by useState hook. We only need to update it after i18n
// instance was replaced (for example in the provider).
useEffect(() => {
if (isMounted.current && ready) {
// not getNewT: depend on dependency list of the useCallback call within
// useMemoizedT to only provide a newly-bound t *iff* i18n instance was
// replaced; see bug 1691 https://github.com/i18next/react-i18next/issues/1691
setT(getT);
}
}, [i18n, keyPrefix, ready]); // re-run when i18n instance or keyPrefix were replaced
const ret = [t, i18n, ready];
ret.t = t;
ret.i18n = i18n;
ret.ready = ready;
// return hook stuff if ready
if (ready) return ret;
// not yet loaded namespaces -> load them -> and return if useSuspense option set false
if (!ready && !useSuspense) return ret;
// not yet loaded namespaces -> load them -> and trigger suspense
throw new Promise((resolve) => {
if (props.lng) {
loadLanguages(i18n, props.lng, namespaces, () => resolve());
} else {
loadNamespaces(i18n, namespaces, () => resolve());
}
});
};