diff --git a/examples/simple/yarn.lock b/examples/simple/yarn.lock index f7a3a85b7..79478be23 100644 --- a/examples/simple/yarn.lock +++ b/examples/simple/yarn.lock @@ -74,17 +74,17 @@ chalk "^2.0.0" js-tokens "^4.0.0" -"@babel/runtime@7.12.5", "@babel/runtime@^7.12.0": +"@babel/runtime@7.12.5": version "7.12.5" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.12.5.tgz#410e7e487441e1b360c29be715d870d9b985882e" integrity sha512-plcc+hbExy3McchJCEQG3knOsuh3HH+Prx1P6cLIkET/0dLuQDEnrT+s27Axgc9bqfsmNUNHfscgMUdBpC9xfg== dependencies: regenerator-runtime "^0.13.4" -"@babel/runtime@^7.13.6": - version "7.13.7" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.13.7.tgz#d494e39d198ee9ca04f4dcb76d25d9d7a1dc961a" - integrity sha512-h+ilqoX998mRVM5FtB5ijRuHUDVt5l3yfoOi2uh18Z/O3hvyaHQ39NpxVkCIG5yFs+mLq/ewFp8Bss6zmWv6ZA== +"@babel/runtime@^7.12.0", "@babel/runtime@^7.13.6": + version "7.13.10" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.13.10.tgz#47d42a57b6095f4468da440388fdbad8bebf0d7d" + integrity sha512-4QPkjJq6Ns3V/RgpEahRk+AGfL0eO6RHHtTWoNNr5mO49G6B5+X6d6THgWEAvTrznU5xYpbAlVKRYcsCgh/Akw== dependencies: regenerator-runtime "^0.13.4" @@ -1054,9 +1054,9 @@ copy-descriptor@^0.1.0: integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40= core-js@^3: - version "3.7.0" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.7.0.tgz#b0a761a02488577afbf97179e4681bf49568520f" - integrity sha512-NwS7fI5M5B85EwpWuIwJN4i/fbisQUwLwiSNUWeXlkAZ0sbBjLEvLvFLf1uzAUV66PcEPt4xCGCmOZSxVf3xzA== + version "3.9.1" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.9.1.tgz#cec8de593db8eb2a85ffb0dbdeb312cb6e5460ae" + integrity sha512-gSjRvzkxQc1zjM/5paAmL4idJBFzuJoo+jDjF1tStYFMV2ERfD02HhahhCGXUyHxQRG4yFKVSdO6g62eoRMcDg== core-util-is@~1.0.0: version "1.0.2" @@ -1870,21 +1870,14 @@ https-proxy-agent@5.0.0: debug "4" i18next-fs-backend@^1.0.7: - version "1.0.7" - resolved "https://registry.yarnpkg.com/i18next-fs-backend/-/i18next-fs-backend-1.0.7.tgz#00ca4587e306f8948740408389dda73461a5d07f" - integrity sha512-aAZ3rvshe1Zbl6JSCWrWWqbZS5JpmVNG+84YqLcgdYcm9uAxzw4xWxnA/a3044Nm2PKXE62CT+pIZjk7OEYtTw== - -i18next@^19.7.0: - version "19.9.1" - resolved "https://registry.yarnpkg.com/i18next/-/i18next-19.9.1.tgz#7a072b75daf677aa51fd4ce55214f21702af3ffd" - integrity sha512-9Azzyb3DvMJUMd7sPhwVEs6PQcogvdHmLQTjMQ+P+h3XwW4O66/8lgZTmYShgkjPOCqTw4Fwl5LOp/VhZgPo5A== - dependencies: - "@babel/runtime" "^7.12.0" + version "1.1.0" + resolved "https://registry.yarnpkg.com/i18next-fs-backend/-/i18next-fs-backend-1.1.0.tgz#3baa63bfb6fd00a331b91d186776cd886b46d2f6" + integrity sha512-QfzfrEYEsLsDC5sZsdSQl5fVYg8I5KrJynnWN7xgSU5yfClbBJ009mtNxUszR0uABQZ8PRr2gj3bN9+RNORBlg== -i18next@^19.8.4: - version "19.8.4" - resolved "https://registry.yarnpkg.com/i18next/-/i18next-19.8.4.tgz#447718f2a26319b8debdbcc6fbc1a9761be7316b" - integrity sha512-FfVPNWv+felJObeZ6DSXZkj9QM1Ivvh7NcFCgA8XPtJWHz0iXVa9BUy+QY8EPrCLE+vWgDfV/sc96BgXVo6HAA== +i18next@^19.7.0, i18next@^19.8.4: + version "19.9.2" + resolved "https://registry.yarnpkg.com/i18next/-/i18next-19.9.2.tgz#ea5a124416e3c5ab85fddca2c8e3c3669a8da397" + integrity sha512-0i6cuo6ER6usEOtKajUUDj92zlG+KArFia0857xxiEHAQcUwh/RtOQocui1LPJwunSYT574Pk64aNva1kwtxZg== dependencies: "@babel/runtime" "^7.12.0" @@ -2485,15 +2478,8 @@ neo-async@^2.5.0, neo-async@^2.6.1, neo-async@^2.6.2: integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== "next-i18next@link:../..": - version "8.1.0" - dependencies: - "@types/hoist-non-react-statics" "^3.3.1" - "@types/i18next-fs-backend" "^1.0.0" - core-js "^3" - hoist-non-react-statics "^3.2.0" - i18next "^19.8.4" - i18next-fs-backend "^1.0.7" - react-i18next "^11.8.8" + version "0.0.0" + uid "" next-tick@~1.0.0: version "1.0.0" @@ -3073,10 +3059,10 @@ rc@^1.2.7: minimist "^1.2.0" strip-json-comments "~2.0.1" -react-i18next@^11.8.8: - version "11.8.8" - resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-11.8.8.tgz#23d34518c784f2ada7cec41cfe439ac4ae51875c" - integrity sha512-Z8Daifh+FRpcQsCp48mWQViYSlojv0WiL2bf6e9DOzpfVMDaTT6qsYRbHCjLEeDeEioxoaWHMiWu2JPTW3Ni4w== +react-i18next@^11.8.10: + version "11.8.10" + resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-11.8.10.tgz#aa64bc20410ee8f660a5b918d53f4e41271edf00" + integrity sha512-ckjNzMjYkmx4fQ8zzuaYTosYN3Co6ebrgCQJzuZCcGFYSR/kGHZzSu0xw9VhtnbjJVKx0gEMV3DLRvzi4xDZUw== dependencies: "@babel/runtime" "^7.13.6" html-parse-stringify2 "^2.0.1" diff --git a/package.json b/package.json index c95cbcdea..5a8ca4886 100644 --- a/package.json +++ b/package.json @@ -108,7 +108,7 @@ "hoist-non-react-statics": "^3.2.0", "i18next": "^19.8.4", "i18next-fs-backend": "^1.0.7", - "react-i18next": "^11.8.8" + "react-i18next": "^11.8.10" }, "peerDependencies": { "next": ">= 10.0.0", diff --git a/src/appWithTranslation.client.test.tsx b/src/appWithTranslation.client.test.tsx index 33e6ad709..ddf87c774 100644 --- a/src/appWithTranslation.client.test.tsx +++ b/src/appWithTranslation.client.test.tsx @@ -2,8 +2,10 @@ import React from 'react' import fs from 'fs' import { screen, render } from '@testing-library/react' import { I18nextProvider } from 'react-i18next' +import createClient from './createClient' import { appWithTranslation } from './appWithTranslation' +import { NextRouter } from 'next/router' jest.mock('fs', () => ({ existsSync: jest.fn(), @@ -19,15 +21,16 @@ jest.mock('react-i18next', () => ({ __esmodule: true, })) +jest.mock('./createClient', () => jest.fn()) const DummyApp = appWithTranslation(() => (
Hello world
)) -const props = { +const createProps = (locale = 'en', router: Partial = {}) => ({ pageProps: { _nextI18Next: { - initialLocale: 'en', + initialLocale: locale, userConfig: { i18n: { defaultLocale: 'en', @@ -36,9 +39,15 @@ const props = { }, }, } as any, -} as any - -const renderComponent = () => + router: { + locale: locale, + route: '/', + ...router, + }, +} as any) + +const defaultRenderProps = createProps() +const renderComponent = (props = defaultRenderProps) => render( { (fs.existsSync as jest.Mock).mockReturnValue(true); (fs.readdirSync as jest.Mock).mockReturnValue([]); (I18nextProvider as jest.Mock).mockImplementation(DummyI18nextProvider) + const actualCreateClient = jest.requireActual('./createClient'); + (createClient as jest.Mock).mockImplementation(actualCreateClient) }) afterEach(jest.resetAllMocks) @@ -69,6 +80,7 @@ describe('appWithTranslation', () => { }, } as any) const customProps = { + ...createProps(), pageProps: { _nextI18Next: { initialLocale: 'en', @@ -91,6 +103,7 @@ describe('appWithTranslation', () => {
Hello world
)) const customProps = { + ...createProps(), pageProps: { _nextI18Next: { initialLocale: 'en', @@ -124,4 +137,35 @@ describe('appWithTranslation', () => { expect(fs.readdirSync).toHaveBeenCalledTimes(0) }) + it('should use locale from router', () => { + renderComponent(createProps('de')) + const [args] = (I18nextProvider as jest.Mock).mock.calls + expect(args[0].i18n.language).toEqual('de') + }) + + it('does not re-call createClient on re-renders unless locale or props have changed', () => { + const { rerender } = renderComponent() + expect(createClient).toHaveBeenCalledTimes(1) + rerender( + + ) + expect(createClient).toHaveBeenCalledTimes(1) + const newProps = createProps() + rerender( + + ) + expect(createClient).toHaveBeenCalledTimes(2) + newProps.router.locale = 'de'; + rerender( + + ) + expect(createClient).toHaveBeenCalledTimes(3) + }) + }) diff --git a/src/appWithTranslation.server.test.tsx b/src/appWithTranslation.server.test.tsx index 2f4d8b792..883738d2b 100644 --- a/src/appWithTranslation.server.test.tsx +++ b/src/appWithTranslation.server.test.tsx @@ -40,6 +40,10 @@ const props = { }, }, } as any, + router: { + locale: 'en', + route: '/', + }, } as any const renderComponent = () => diff --git a/src/appWithTranslation.tsx b/src/appWithTranslation.tsx index 0e70fb895..67f81ad67 100644 --- a/src/appWithTranslation.tsx +++ b/src/appWithTranslation.tsx @@ -9,6 +9,7 @@ import createClient from './createClient' import { SSRConfig, UserConfig } from './types' import { i18n as I18NextClient } from 'i18next' +import { useRouter } from 'next/router' export { Trans, useTranslation, withTranslation } from 'react-i18next' type AppProps = NextJsAppProps & { @@ -22,12 +23,17 @@ export const appWithTranslation = ( configOverride: UserConfig | null = null, ) => { const AppWithTranslation = (props: AppProps) => { - let i18n: I18NextClient | null = null - let locale = null + const { _nextI18Next } = props.pageProps + const { locale } = props.router - if (props?.pageProps?._nextI18Next) { - let { userConfig } = props.pageProps._nextI18Next - const { initialI18nStore, initialLocale } = props.pageProps._nextI18Next + // Memoize the instance and only re-initialize when either: + // 1. The route changes (non-shallowly) + // 2. Router locale changes + const i18n: I18NextClient | null = useMemo(() => { + if (!locale || !_nextI18Next) return null + + let { userConfig } = _nextI18Next + const { initialI18nStore } = _nextI18Next if (userConfig === null && configOverride === null) { throw new Error('appWithTranslation was called without a next-i18next config') @@ -41,21 +47,17 @@ export const appWithTranslation = ( throw new Error('appWithTranslation was called without config.i18n') } - locale = initialLocale; - - ({ i18n } = createClient({ + return createClient({ ...createConfig({ ...userConfig, - lng: initialLocale, + lng: locale, }), - lng: initialLocale, + lng: locale, resources: initialI18nStore, - })) + }).i18n + }, [_nextI18Next, locale]) - useMemo(() => { - globalI18n = i18n - }, [i18n]) - } + globalI18n = i18n return i18n !== null ? (