diff --git a/src/appWithTranslation.client.test.tsx b/src/appWithTranslation.client.test.tsx index 33e6ad70..c809b877 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 2f4d8b79..883738d2 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 b220dda4..1afae49b 100644 --- a/src/appWithTranslation.tsx +++ b/src/appWithTranslation.tsx @@ -22,12 +22,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 +46,20 @@ export const appWithTranslation = ( throw new Error('appWithTranslation was called without config.i18n') } - locale = initialLocale; - ({ i18n } = createClient({ + const instance = createClient({ ...createConfig({ ...userConfig, - lng: initialLocale, + lng: locale, }), - lng: initialLocale, + lng: locale, resources: initialI18nStore, - })) - } + }).i18n + + globalI18n = instance - useMemo(() => { - globalI18n = i18n - }, [i18n]) + return instance + }, [_nextI18Next, locale]) return i18n !== null ? ( ( const props = { pageProps: { _nextI18Next: { - initialLocale: 'en', userConfig: { i18n: { defaultLocale: 'en', @@ -27,6 +26,9 @@ const props = { }, }, } as any, + router: { + locale: 'en', + }, } as any const renderDummyComponent = () => @@ -182,7 +184,6 @@ describe('serverSideTranslations', () => { initialI18nStore: { 'en-US': {}, }, - initialLocale: 'en-US', userConfig: { i18n: { defaultLocale: 'en-US', diff --git a/src/serverSideTranslations.ts b/src/serverSideTranslations.ts index 12978fbb..65fa671c 100644 --- a/src/serverSideTranslations.ts +++ b/src/serverSideTranslations.ts @@ -106,7 +106,6 @@ export const serverSideTranslations = async ( return { _nextI18Next: { initialI18nStore, - initialLocale, userConfig: config.serializeConfig ? userConfig : null, }, } diff --git a/src/types.ts b/src/types.ts index c9d013a4..ace67efe 100644 --- a/src/types.ts +++ b/src/types.ts @@ -52,7 +52,6 @@ export type CreateClientReturn = { export type SSRConfig = { _nextI18Next: { initialI18nStore: any - initialLocale: string userConfig: UserConfig | null } }