Skip to content

Commit dc7225e

Browse files
committed
fix regression in v16.2.0: bindI18nStore #1879
1 parent aa66a89 commit dc7225e

File tree

5 files changed

+79
-21
lines changed

5 files changed

+79
-21
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
### 16.2.1
2+
3+
- fix regression in v16.2.0: bindI18nStore does not work correctly [1879](https://github.com/i18next/react-i18next/issues/1879)
4+
15
### 16.2.0
26

37
- try to address: useTranslation hook violates React's rules of hooks by conditionally calling inner hooks [1863](https://github.com/i18next/react-i18next/issues/1863)

react-i18next.js

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3398,17 +3398,22 @@
33983398
return isString(nsOrContext) ? [nsOrContext] : nsOrContext || ['translation'];
33993399
}, [ns, defaultNSFromContext, i18n]);
34003400
i18n?.reportNamespaces?.addUsedNamespaces?.(namespaces);
3401+
const revisionRef = React.useRef(0);
34013402
const subscribe = React.useCallback(callback => {
34023403
if (!i18n) return dummySubscribe;
34033404
const {
34043405
bindI18n,
34053406
bindI18nStore
34063407
} = i18nOptions;
3407-
if (bindI18n) i18n.on(bindI18n, callback);
3408-
if (bindI18nStore) i18n.store.on(bindI18nStore, callback);
3408+
const wrappedCallback = () => {
3409+
revisionRef.current += 1;
3410+
callback();
3411+
};
3412+
if (bindI18n) i18n.on(bindI18n, wrappedCallback);
3413+
if (bindI18nStore) i18n.store.on(bindI18nStore, wrappedCallback);
34093414
return () => {
3410-
if (bindI18n) bindI18n.split(' ').forEach(e => i18n.off(e, callback));
3411-
if (bindI18nStore) bindI18nStore.split(' ').forEach(e => i18n.store.off(e, callback));
3415+
if (bindI18n) bindI18n.split(' ').forEach(e => i18n.off(e, wrappedCallback));
3416+
if (bindI18nStore) bindI18nStore.split(' ').forEach(e => i18n.store.off(e, wrappedCallback));
34123417
};
34133418
}, [i18n, i18nOptions]);
34143419
const snapshotRef = React.useRef();
@@ -3417,16 +3422,19 @@
34173422
return notReadySnapshot;
34183423
}
34193424
const calculatedReady = !!(i18n.isInitialized || i18n.initializedStoreOnce) && namespaces.every(n => hasLoadedNamespace(n, i18n, i18nOptions));
3420-
const calculatedT = i18n.getFixedT(props.lng || i18n.language, i18nOptions.nsMode === 'fallback' ? namespaces : namespaces[0], keyPrefix);
3425+
const currentLng = props.lng || i18n.language;
3426+
const currentRevision = revisionRef.current;
34213427
const lastSnapshot = snapshotRef.current;
3422-
if (lastSnapshot && lastSnapshot.ready === calculatedReady && lastSnapshot.lng === (props.lng || i18n.language) && lastSnapshot.keyPrefix === keyPrefix) {
3428+
if (lastSnapshot && lastSnapshot.ready === calculatedReady && lastSnapshot.lng === currentLng && lastSnapshot.keyPrefix === keyPrefix && lastSnapshot.revision === currentRevision) {
34233429
return lastSnapshot;
34243430
}
3431+
const calculatedT = i18n.getFixedT(currentLng, i18nOptions.nsMode === 'fallback' ? namespaces : namespaces[0], keyPrefix);
34253432
const newSnapshot = {
34263433
t: calculatedT,
34273434
ready: calculatedReady,
3428-
lng: props.lng || i18n.language,
3429-
keyPrefix
3435+
lng: currentLng,
3436+
keyPrefix,
3437+
revision: currentRevision
34303438
};
34313439
snapshotRef.current = newSnapshot;
34323440
return newSnapshot;

react-i18next.min.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/useTranslation.js

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -50,15 +50,23 @@ export const useTranslation = (ns, props = {}) => {
5050

5151
i18n?.reportNamespaces?.addUsedNamespaces?.(namespaces);
5252

53+
const revisionRef = useRef(0);
5354
const subscribe = useCallback(
5455
(callback) => {
5556
if (!i18n) return dummySubscribe;
5657
const { bindI18n, bindI18nStore } = i18nOptions;
57-
if (bindI18n) i18n.on(bindI18n, callback);
58-
if (bindI18nStore) i18n.store.on(bindI18nStore, callback);
58+
59+
const wrappedCallback = () => {
60+
revisionRef.current += 1;
61+
callback();
62+
};
63+
64+
if (bindI18n) i18n.on(bindI18n, wrappedCallback);
65+
if (bindI18nStore) i18n.store.on(bindI18nStore, wrappedCallback);
5966
return () => {
60-
if (bindI18n) bindI18n.split(' ').forEach((e) => i18n.off(e, callback));
61-
if (bindI18nStore) bindI18nStore.split(' ').forEach((e) => i18n.store.off(e, callback));
67+
if (bindI18n) bindI18n.split(' ').forEach((e) => i18n.off(e, wrappedCallback));
68+
if (bindI18nStore)
69+
bindI18nStore.split(' ').forEach((e) => i18n.store.off(e, wrappedCallback));
6270
};
6371
},
6472
[i18n, i18nOptions],
@@ -72,25 +80,32 @@ export const useTranslation = (ns, props = {}) => {
7280
const calculatedReady =
7381
!!(i18n.isInitialized || i18n.initializedStoreOnce) &&
7482
namespaces.every((n) => hasLoadedNamespace(n, i18n, i18nOptions));
75-
const calculatedT = i18n.getFixedT(
76-
props.lng || i18n.language,
77-
i18nOptions.nsMode === 'fallback' ? namespaces : namespaces[0],
78-
keyPrefix,
79-
);
83+
const currentLng = props.lng || i18n.language;
84+
const currentRevision = revisionRef.current;
85+
8086
const lastSnapshot = snapshotRef.current;
8187
if (
8288
lastSnapshot &&
8389
lastSnapshot.ready === calculatedReady &&
84-
lastSnapshot.lng === (props.lng || i18n.language) &&
85-
lastSnapshot.keyPrefix === keyPrefix
90+
lastSnapshot.lng === currentLng &&
91+
lastSnapshot.keyPrefix === keyPrefix &&
92+
lastSnapshot.revision === currentRevision // Check revision
8693
) {
8794
return lastSnapshot;
8895
}
96+
97+
const calculatedT = i18n.getFixedT(
98+
currentLng,
99+
i18nOptions.nsMode === 'fallback' ? namespaces : namespaces[0],
100+
keyPrefix,
101+
);
102+
89103
const newSnapshot = {
90104
t: calculatedT,
91105
ready: calculatedReady,
92-
lng: props.lng || i18n.language,
106+
lng: currentLng,
93107
keyPrefix,
108+
revision: currentRevision, // Store revision
94109
};
95110
snapshotRef.current = newSnapshot;
96111
return newSnapshot;
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { describe, it, expect } from 'vitest';
2+
import React from 'react';
3+
import { act, render, screen } from '@testing-library/react';
4+
import { createInstance } from 'i18next';
5+
import { useTranslation } from '../src/useTranslation';
6+
import { I18nextProvider } from '../src/I18nextProvider';
7+
8+
describe('useTranslation with bindI18nStore', () => {
9+
it('should correctly return the correct translation', async () => {
10+
const i18next = createInstance();
11+
await i18next.init({
12+
fallbackLng: ['en'],
13+
react: { bindI18nStore: 'added' },
14+
});
15+
function TranslateAKey() {
16+
const { t } = useTranslation();
17+
return <h1>{t('key', { ns: 'namespace' })}</h1>;
18+
}
19+
i18next.addResourceBundle('en', 'namespace', { key: 'english' }, false, true);
20+
render(
21+
<I18nextProvider i18n={i18next}>
22+
<TranslateAKey />
23+
</I18nextProvider>,
24+
);
25+
expect(screen.getByRole('heading')).toHaveTextContent('english');
26+
await act(() => i18next.changeLanguage('de'));
27+
expect(screen.getByRole('heading')).toHaveTextContent('english');
28+
await act(() => i18next.addResourceBundle('de', 'namespace', { key: 'deutsch' }, false, true));
29+
expect(screen.getByRole('heading')).toHaveTextContent('deutsch'); // this assertion fails
30+
});
31+
});

0 commit comments

Comments
 (0)