Skip to content

Commit

Permalink
optimization for optional lng prop for useTranslation, should now pre…
Browse files Browse the repository at this point in the history
…vent missings when lazy loading translations #1637
  • Loading branch information
adrai committed May 18, 2023
1 parent 6dd1056 commit 034e13e
Show file tree
Hide file tree
Showing 9 changed files with 216 additions and 40 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
### 12.3.1

- optimization for optional lng prop for useTranslation, should now prevent missings when lazy loading translations [1637](https://github.com/i18next/react-i18next/issues/1637)

### 12.3.0

- optional lng prop for useTranslation (helping on server side [1637](https://github.com/i18next/react-i18next/issues/1637))
Expand Down
14 changes: 7 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@
"eslint-plugin-react": "^7.16.0",
"eslint-plugin-testing-library": "^3.10.1",
"husky": "^3.0.3",
"i18next": "^22.4.3",
"i18next": "^22.5.0",
"jest": "^24.8.0",
"jest-cli": "^24.8.4",
"lint-staged": "^8.1.3",
Expand Down
42 changes: 34 additions & 8 deletions react-i18next.js
Original file line number Diff line number Diff line change
Expand Up @@ -341,8 +341,9 @@
if (typeof args[0] === 'string') alreadyWarned[args[0]] = new Date();
warn.apply(void 0, args);
}
function loadNamespaces(i18n, ns, cb) {
i18n.loadNamespaces(ns, function () {

var loadedClb = function loadedClb(i18n, cb) {
return function () {
if (i18n.isInitialized) {
cb();
} else {
Expand All @@ -355,7 +356,18 @@

i18n.on('initialized', initialized);
}
};
};

function loadNamespaces(i18n, ns, cb) {
i18n.loadNamespaces(ns, loadedClb(i18n, cb));
}
function loadLanguages(i18n, lng, ns, cb) {
if (typeof ns === 'string') ns = [ns];
ns.forEach(function (n) {
if (i18n.options.ns.indexOf(n) < 0) i18n.options.ns.push(n);
});
i18n.loadLanguages(lng, loadedClb(i18n, cb));
}

function oldI18nextHasLoadedNamespace(ns, i18n) {
Expand Down Expand Up @@ -392,6 +404,7 @@
}

return i18n.hasLoadedNamespace(ns, {
lng: options.lng,
precheck: function precheck(i18nInstance, loadNotPending) {
if (options.bindI18n && options.bindI18n.indexOf('languageChanging') > -1 && i18nInstance.services.backendConnector.backend && i18nInstance.isLanguageChangingTo && !loadNotPending(i18nInstance.isLanguageChangingTo, ns)) return false;
}
Expand Down Expand Up @@ -883,6 +896,7 @@
setT = _useState2[1];

var joinedNS = namespaces.join();
if (props.lng) joinedNS = "".concat(props.lng).concat(joinedNS);
var previousJoinedNS = usePrevious(joinedNS);
var isMounted = react.useRef(true);
react.useEffect(function () {
Expand All @@ -891,9 +905,15 @@
isMounted.current = true;

if (!ready && !useSuspense) {
loadNamespaces(i18n, namespaces, function () {
if (isMounted.current) setT(getT);
});
if (props.lng) {
loadLanguages(i18n, props.lng, namespaces, function () {
if (isMounted.current) setT(getT);
});
} else {
loadNamespaces(i18n, namespaces, function () {
if (isMounted.current) setT(getT);
});
}
}

if (ready && previousJoinedNS && previousJoinedNS !== joinedNS && isMounted.current) {
Expand Down Expand Up @@ -931,9 +951,15 @@
if (ready) return ret;
if (!ready && !useSuspense) return ret;
throw new Promise(function (resolve) {
loadNamespaces(i18n, namespaces, function () {
resolve();
});
if (props.lng) {
loadLanguages(i18n, props.lng, namespaces, function () {
return resolve();
});
} else {
loadNamespaces(i18n, namespaces, function () {
return resolve();
});
}
});
}

Expand Down
2 changes: 1 addition & 1 deletion react-i18next.min.js

Large diffs are not rendered by default.

25 changes: 17 additions & 8 deletions src/useTranslation.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useState, useEffect, useContext, useRef } from 'react';
import { getI18n, getDefaults, ReportNamespaces, I18nContext } from './context.js';
import { warnOnce, loadNamespaces, hasLoadedNamespace } from './utils.js';
import { warnOnce, loadNamespaces, loadLanguages, hasLoadedNamespace } from './utils.js';

const usePrevious = (value, ignore) => {
const ref = useRef();
Expand Down Expand Up @@ -65,7 +65,8 @@ export function useTranslation(ns, props = {}) {
}
const [t, setT] = useState(getT);

const joinedNS = namespaces.join();
let joinedNS = namespaces.join();
if (props.lng) joinedNS = `${props.lng}${joinedNS}`;
const previousJoinedNS = usePrevious(joinedNS);

const isMounted = useRef(true);
Expand All @@ -76,9 +77,15 @@ export function useTranslation(ns, props = {}) {
// if not ready and not using suspense load the namespaces
// in side effect and do not call resetT if unmounted
if (!ready && !useSuspense) {
loadNamespaces(i18n, namespaces, () => {
if (isMounted.current) setT(getT);
});
if (props.lng) {
loadLanguages(i18n, props.lng, namespaces, () => {
if (isMounted.current) setT(getT);
});
} else {
loadNamespaces(i18n, namespaces, () => {
if (isMounted.current) setT(getT);
});
}
}

if (ready && previousJoinedNS && previousJoinedNS !== joinedNS && isMounted.current) {
Expand Down Expand Up @@ -125,8 +132,10 @@ export function useTranslation(ns, props = {}) {

// not yet loaded namespaces -> load them -> and trigger suspense
throw new Promise((resolve) => {
loadNamespaces(i18n, namespaces, () => {
resolve();
});
if (props.lng) {
loadLanguages(i18n, props.lng, namespaces, () => resolve());
} else {
loadNamespaces(i18n, namespaces, () => resolve());
}
});
}
42 changes: 27 additions & 15 deletions src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,23 +21,34 @@ export function warnOnce(...args) {
// }
// }

export function loadNamespaces(i18n, ns, cb) {
i18n.loadNamespaces(ns, () => {
// delay ready if not yet initialized i18n instance
if (i18n.isInitialized) {
const loadedClb = (i18n, cb) => () => {
// delay ready if not yet initialized i18n instance
if (i18n.isInitialized) {
cb();
} else {
const initialized = () => {
// due to emitter removing issue in i18next we need to delay remove
setTimeout(() => {
i18n.off('initialized', initialized);
}, 0);
cb();
} else {
const initialized = () => {
// due to emitter removing issue in i18next we need to delay remove
setTimeout(() => {
i18n.off('initialized', initialized);
}, 0);
cb();
};

i18n.on('initialized', initialized);
}
};
i18n.on('initialized', initialized);
}
};

export function loadNamespaces(i18n, ns, cb) {
i18n.loadNamespaces(ns, loadedClb(i18n, cb));
}

// should work with I18NEXT >= v22.5.0
export function loadLanguages(i18n, lng, ns, cb) {
// eslint-disable-next-line no-param-reassign
if (typeof ns === 'string') ns = [ns];
ns.forEach((n) => {
if (i18n.options.ns.indexOf(n) < 0) i18n.options.ns.push(n);
});
i18n.loadLanguages(lng, loadedClb(i18n, cb));
}

// WAIT A LITTLE FOR I18NEXT BEING UPDATED IN THE WILD, before removing this old i18next version support
Expand Down Expand Up @@ -97,6 +108,7 @@ export function hasLoadedNamespace(ns, i18n, options = {}) {

// IN I18NEXT > v19.4.5 WE CAN (INTRODUCED JUNE 2020)
return i18n.hasLoadedNamespace(ns, {
lng: options.lng,
precheck: (i18nInstance, loadNotPending) => {
if (
options.bindI18n &&
Expand Down
40 changes: 40 additions & 0 deletions test/lngAwareBackendMock.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
class Backend {
constructor(services, options = {}) {
this.init(services, options);
this.type = 'backend';
this.queue = [];
}

init(services, options) {
this.services = services;
this.options = options;
}

read(language, namespace, callback) {
this.queue.push({ language, namespace, callback });
}

flush(what) {
let q = [...this.queue];
if (what) {
const filterFor = [];
if (what.language) filterFor.push('language');
if (what.namespace) filterFor.push('namespace');
if (filterFor.length > 0) {
q = q.filter((item) => {
const allOk = filterFor.map((ff) => item[ff] === what[ff]).every((r) => r);
if (allOk) return true;
return false;
});
}
}
q.forEach((item) => {
this.queue.splice(this.queue.indexOf(item), 1);
item.callback(null, {
key1: `${item.language}/${item.namespace} for key1`,
});
});
}
}

export default Backend;
85 changes: 85 additions & 0 deletions test/useTranslation.loadingLng.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { renderHook } from '@testing-library/react-hooks';
import i18n from './i18n';
import BackendMock from './lngAwareBackendMock';
import { useTranslation } from '../src/useTranslation';

jest.unmock('../src/useTranslation');

describe('useTranslation loading ns with lng via props', () => {
let newI18n;
let backend;

beforeEach(() => {
newI18n = i18n.createInstance();
backend = new BackendMock();
newI18n.use(backend).init({
lng: 'en',
fallbackLng: 'en',
});
});

it('should wait for correct translation with suspense', async () => {
const { result, waitForNextUpdate } = renderHook(() =>
useTranslation('common', { i18n: newI18n, useSuspense: true, lng: 'de' }),
);
expect(result.all).toHaveLength(0);
backend.flush();
await waitForNextUpdate();
const { t } = result.current;
expect(t('key1')).toBe('de/common for key1');
});

it('should wait for correct translation without suspense', async () => {
const { result } = renderHook(() =>
useTranslation('common', { i18n: newI18n, useSuspense: false, lng: 'it' }),
);
const { t } = result.current;
expect(t('key1')).toBe('key1');

backend.flush();
expect(t('key1')).toBe('it/common for key1');
});

it('should return defaultValue if resources not yet loaded', async () => {
const { result } = renderHook(() =>
useTranslation('common', { i18n: newI18n, useSuspense: false, lng: 'fr' }),
);
const { t } = result.current;
expect(t('key1', 'my default value')).toBe('my default value');
expect(t('key1', { defaultValue: 'my default value' })).toBe('my default value');

backend.flush({ language: 'en' });
expect(t('key1', 'my default value')).toBe('en/common for key1');
expect(t('key1', { defaultValue: 'my default value' })).toBe('en/common for key1');

backend.flush({ language: 'fr' });
expect(t('key1', 'my default value')).toBe('fr/common for key1');
expect(t('key1', { defaultValue: 'my default value' })).toBe('fr/common for key1');
});

it('should correctly return and render correct tranlations in multiple useTranslation usages', async () => {
const { result, waitForNextUpdate } = renderHook(() =>
useTranslation('newns', { i18n: newI18n, useSuspense: true, lng: 'pt' }),
);
backend.flush();
await waitForNextUpdate();
const { t } = result.current;
expect(t('key1')).toBe('pt/newns for key1');

const retDe = renderHook(() =>
useTranslation('newns', { i18n: newI18n, useSuspense: true, lng: 'de' }),
);
backend.flush({ language: 'de' });
await retDe.waitForNextUpdate();
const { t: tDE } = retDe.result.current;
expect(tDE('key1')).toBe('de/newns for key1');

const retPT = renderHook(() =>
useTranslation('newns', { i18n: newI18n, useSuspense: true, lng: 'pt' }),
);
backend.flush({ language: 'pt' });
// await retPT.waitForNextUpdate(); // already loaded
const { t: tPT } = retPT.result.current;
expect(tPT('key1')).toBe('pt/newns for key1');
});
});

0 comments on commit 034e13e

Please sign in to comment.