From 3b9f891dc9d1ed6acf68a600619505094338cca3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20B=C3=B6rjesson=20Skrivanos?= Date: Sun, 14 Mar 2021 12:41:04 +0100 Subject: [PATCH] fix: support shallow route changes * Memoize the i18n client and do not re-create it unless the route or locale has changed (fix #1059). * Let Next's router locale take precedence over initialLocale (fix #1023). * Bump the minimum version of react-i18next. --- examples/simple/package.json | 2 +- examples/simple/yarn.lock | 56 ++++++++++---------------- package.json | 2 +- src/appWithTranslation.client.test.tsx | 47 +++++++++++++++++++-- src/appWithTranslation.tsx | 35 +++++++++------- yarn.lock | 17 +++++--- 6 files changed, 99 insertions(+), 60 deletions(-) diff --git a/examples/simple/package.json b/examples/simple/package.json index 252ce3445..97c7e6d9b 100644 --- a/examples/simple/package.json +++ b/examples/simple/package.json @@ -10,7 +10,7 @@ }, "dependencies": { "next": "10.0.5", - "next-i18next": "link:../../" + "next-i18next": "link:../.." }, "devDependencies": { "prop-types": "^15.7.2" 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 8c0c06781..e6fba88e0 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..d3d581d0a 100644 --- a/src/appWithTranslation.client.test.tsx +++ b/src/appWithTranslation.client.test.tsx @@ -2,6 +2,8 @@ import React from 'react' import fs from 'fs' import { screen, render } from '@testing-library/react' import { I18nextProvider } from 'react-i18next' +import { useRouter } from 'next/router' +import createClient from './createClient' import { appWithTranslation } from './appWithTranslation' @@ -19,15 +21,17 @@ jest.mock('react-i18next', () => ({ __esmodule: true, })) +jest.mock('next/router') +jest.mock('./createClient', () => jest.fn()) const DummyApp = appWithTranslation(() => (
Hello world
)) -const props = { +const createProps = (locale = 'en') => ({ pageProps: { _nextI18Next: { - initialLocale: 'en', + initialLocale: locale, userConfig: { i18n: { defaultLocale: 'en', @@ -36,7 +40,9 @@ const props = { }, }, } as any, -} as any +} as any) + +const props = createProps() const renderComponent = () => render( @@ -50,6 +56,8 @@ describe('appWithTranslation', () => { (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) @@ -124,4 +132,37 @@ describe('appWithTranslation', () => { expect(fs.readdirSync).toHaveBeenCalledTimes(0) }) + it('should let next router locale take precedence', () => { + (useRouter as jest.Mock).mockReturnValue({ locale: 'de' }) + renderComponent() + 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', () => { + (useRouter as jest.Mock).mockReturnValue({ route: '/route' }) + const { rerender } = renderComponent() + expect(createClient).toHaveBeenCalledTimes(1) + rerender( + + ) + expect(createClient).toHaveBeenCalledTimes(1) + const newProps = createProps() + rerender( + + ) + expect(createClient).toHaveBeenCalledTimes(2); + (useRouter as jest.Mock).mockReturnValue({ locale: 'de' }) + rerender( + + ) + expect(createClient).toHaveBeenCalledTimes(3) + }) + }) diff --git a/src/appWithTranslation.tsx b/src/appWithTranslation.tsx index 0e70fb895..f19abec3e 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,20 @@ export const appWithTranslation = ( configOverride: UserConfig | null = null, ) => { const AppWithTranslation = (props: AppProps) => { - let i18n: I18NextClient | null = null - let locale = null + const router = useRouter() + const { _nextI18Next } = props.pageProps + const initialLocale = _nextI18Next?.initialLocale || null + const locale = router?.locale || initialLocale - 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 TODO: probably don't do this after #1049 is solved + // and http backend is used by default. + // 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 +50,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 ? (