From 8132efd6a620ee3a74cc5e1efc42757bdca75058 Mon Sep 17 00:00:00 2001 From: Adriano Raiano Date: Sun, 3 Apr 2022 12:02:55 +0200 Subject: [PATCH] feat: allow client side translation loading (#1726) * feat: allow client side translation loading * use typeof window instead of process.browser * update deps * update react-i18next * update i18next dep * Update src/appWithTranslation.tsx Co-authored-by: Isaac Hinman * use older next.js version also in this example * update react-i18next * update react-i18next * move client-loading example out of here * remove extra readme * revert simple/yarn.lock * Update src/appWithTranslation.client.test.tsx Co-authored-by: Isaac Hinman * Update src/appWithTranslation.tsx Co-authored-by: Isaac Hinman * bring back missing test for configOverride.resources * next version seems to be major * update react-i18next Co-authored-by: Isaac Hinman --- README.md | 6 ++ examples/simple/yarn.lock | 23 ++--- package.json | 6 +- src/appWithTranslation.client.test.tsx | 131 +++++++++++++++++++++++-- src/appWithTranslation.tsx | 25 +++-- yarn.lock | 15 ++- 6 files changed, 167 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index 92ad64bf..3227bd11 100644 --- a/README.md +++ b/README.md @@ -213,6 +213,12 @@ export const getStaticProps = async ({ locale }) => ({ }); ``` +#### Client side loading of translations via HTTP + +Since v11.0.0 next-i18next also provides support for client side loading of translations. + +More information about that can be found [here](https://github.com/i18next/i18next-http-backend/tree/master/example/next). + #### Reloading Resources in Development Because resources are loaded once when the server is started, any changes made to your translation JSON files in development will not be loaded until the server is restarted. diff --git a/examples/simple/yarn.lock b/examples/simple/yarn.lock index 695ce415..b337dbf2 100644 --- a/examples/simple/yarn.lock +++ b/examples/simple/yarn.lock @@ -1874,12 +1874,12 @@ https-proxy-agent@5.0.0: agent-base "6" 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-fs-backend@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/i18next-fs-backend/-/i18next-fs-backend-1.1.4.tgz#d0e9b9ed2fa7a0f11002d82b9fa69c3c3d6482da" + integrity sha512-/MfAGMP0jHonV966uFf9PkWWuDjPYLIcsipnSO3NxpNtAgRUKLTwvm85fEmsF6hGeu0zbZiCQ3W74jwO6K9uXA== -i18next@^21.6.12: +i18next@^21.6.14: version "21.6.14" resolved "https://registry.yarnpkg.com/i18next/-/i18next-21.6.14.tgz#2bc199fba7f4da44b5952d7df0a3814a6e5c3943" integrity sha512-XL6WyD+xlwQwbieXRlXhKWoLb/rkch50/rA+vl6untHnJ+aYnkQ0YDZciTWE78PPhOpbi2gR0LTJCJpiAhA+uQ== @@ -2483,15 +2483,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 "10.5.0" - dependencies: - "@babel/runtime" "^7.13.17" - "@types/hoist-non-react-statics" "^3.3.1" - core-js "^3" - hoist-non-react-statics "^3.2.0" - i18next "^21.6.12" - i18next-fs-backend "^1.0.7" - react-i18next "^11.15.5" + version "0.0.0" + uid "" next-tick@~1.0.0: version "1.0.0" @@ -3080,7 +3073,7 @@ rc@^1.2.7: minimist "^1.2.0" strip-json-comments "~2.0.1" -react-i18next@^11.15.5: +react-i18next@^11.16.1: version "11.16.2" resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-11.16.2.tgz#650b18c12a624057ee2651ba4b4a989b526be554" integrity sha512-1iuZduvARUelL5ux663FvIoDZExwFO+9QtRAAt4uvs1/aun4cUZt8XBrVg7iiDgNls9cOSORAhE7Ri5KA9RMvg== diff --git a/package.json b/package.json index e122c316..32bde7d5 100644 --- a/package.json +++ b/package.json @@ -107,9 +107,9 @@ "@types/hoist-non-react-statics": "^3.3.1", "core-js": "^3", "hoist-non-react-statics": "^3.2.0", - "i18next": "^21.6.12", - "i18next-fs-backend": "^1.0.7", - "react-i18next": "^11.15.5" + "i18next": "^21.6.14", + "i18next-fs-backend": "^1.1.4", + "react-i18next": "^11.16.2" }, "peerDependencies": { "next": ">= 10.0.0", diff --git a/src/appWithTranslation.client.test.tsx b/src/appWithTranslation.client.test.tsx index 262f77d8..c073d4ce 100644 --- a/src/appWithTranslation.client.test.tsx +++ b/src/appWithTranslation.client.test.tsx @@ -25,7 +25,12 @@ jest.mock('./createClient', () => jest.fn()) const DummyApp = appWithTranslation(() => (
Hello world
-)) +), { + i18n: { + defaultLocale: 'en', + locales: ['en', 'de'], + }, +}) const createProps = (locale = 'en', router: Partial = {}) => ({ pageProps: { @@ -148,6 +153,113 @@ describe('appWithTranslation', () => { ).toThrow('appWithTranslation was called without a next-i18next config') }) + it('throws an error if userConfig and configOverride are both missing an i18n property', () => { + const DummyAppConfigOverride = appWithTranslation(() => ( +
Hello world
+ ), {} as any) + const customProps = { + ...createProps(), + pageProps: { + _nextI18Next: { + initialLocale: 'en', + userConfig: {}, + }, + } as any, + } as any + expect( + () => render( + + ) + ).toThrow('appWithTranslation was called without config.i18n') + }) + + it('throws an error if userConfig and configOverride are both missing a defaultLocale property', () => { + const DummyAppConfigOverride = appWithTranslation(() => ( +
Hello world
+ ), {i18n: {} as any}) + const customProps = { + ...createProps(), + pageProps: { + _nextI18Next: { + initialLocale: 'en', + userConfig: {i18n: {}}, + }, + } as any, + } as any + expect( + () => render( + + ) + ).toThrow('config.i18n does not include a defaultLocale property') + }) + + it('should use the initialLocale property if the router locale is undefined', () => { + const DummyAppConfigOverride = appWithTranslation(() => ( +
Hello world
+ )) + const customProps = { + ...createProps(), + pageProps: { + _nextI18Next: { + initialLocale: 'en', + userConfig: {i18n: { + defaultLocale: 'fr', + }}, + }, + } as any, + } as any + + customProps.router = { + ...customProps.router, + locale: undefined, + } + + render( + + ) + + const [args] = (I18nextProvider as jest.Mock).mock.calls + expect(args[0].i18n.language).toEqual('en') + }) + + it('should use the userConfig defaltLocale property if the router locale is undefined and initialLocale is undefined', () => { + const DummyAppConfigOverride = appWithTranslation(() => ( +
Hello world
+ )) + const customProps = { + ...createProps(), + + pageProps: { + _nextI18Next: { + initialLocale: undefined, + userConfig: {i18n: { + defaultLocale: 'fr', + }}, + }, + } as any, + } as any + + customProps.router = { + ...customProps.router, + locale: undefined, + } + + render( + + ) + + const [args] = (I18nextProvider as jest.Mock).mock.calls + expect(args[0].i18n.language).toEqual('fr') + }) + it('returns an I18nextProvider', () => { renderComponent() expect(I18nextProvider).toHaveBeenCalledTimes(1) @@ -165,6 +277,12 @@ 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) @@ -181,16 +299,11 @@ describe('appWithTranslation', () => { /> ) expect(createClient).toHaveBeenCalledTimes(2) - const deProps = createProps('de') + newProps.pageProps._nextI18Next.initialLocale = 'de' + newProps.router.locale = 'de' rerender( - ) - expect(createClient).toHaveBeenCalledTimes(3) - rerender( - ) expect(createClient).toHaveBeenCalledTimes(3) diff --git a/src/appWithTranslation.tsx b/src/appWithTranslation.tsx index 8e486134..9c1b095e 100644 --- a/src/appWithTranslation.tsx +++ b/src/appWithTranslation.tsx @@ -23,20 +23,19 @@ export const appWithTranslation = ( ) => { const AppWithTranslation = (props: Props) => { const { _nextI18Next } = props.pageProps as SSRConfig - const locale: string | null = _nextI18Next?.initialLocale ?? null + let locale: string | null = + _nextI18Next?.initialLocale ?? props?.router?.locale // Memoize the instance and only re-initialize when either: // 1. The route changes (non-shallowly) // 2. Router locale changes + // 3. UserConfig override changes const i18n: I18NextClient | null = useMemo(() => { - if (!_nextI18Next) return null + if (!_nextI18Next && !configOverride) return null - let { userConfig } = _nextI18Next - const { initialI18nStore } = _nextI18Next - const resources = - configOverride?.resources ? configOverride.resources : initialI18nStore + let userConfig = configOverride ?? _nextI18Next?.userConfig - if (userConfig === null && configOverride === null) { + if (!userConfig && configOverride === null) { throw new Error('appWithTranslation was called without a next-i18next config') } @@ -48,6 +47,16 @@ export const appWithTranslation = ( throw new Error('appWithTranslation was called without config.i18n') } + if (!userConfig?.i18n?.defaultLocale) { + throw new Error('config.i18n does not include a defaultLocale property') + } + + const { initialI18nStore } = _nextI18Next || {} + const resources = + configOverride?.resources ? configOverride.resources : initialI18nStore + + if (!locale) locale = userConfig.i18n.defaultLocale + const instance = createClient({ ...createConfig({ ...userConfig, @@ -60,7 +69,7 @@ export const appWithTranslation = ( globalI18n = instance return instance - }, [_nextI18Next, locale]) + }, [_nextI18Next, locale, configOverride]) return i18n !== null ? ( =1.1.4, i18next-fs-backend@^1.0.7: +i18next-fs-backend@>=1.1.4, i18next-fs-backend@^1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/i18next-fs-backend/-/i18next-fs-backend-1.1.4.tgz#d0e9b9ed2fa7a0f11002d82b9fa69c3c3d6482da" integrity sha512-/MfAGMP0jHonV966uFf9PkWWuDjPYLIcsipnSO3NxpNtAgRUKLTwvm85fEmsF6hGeu0zbZiCQ3W74jwO6K9uXA== -i18next@^21.0.1, i18next@^21.6.12: +i18next@^21.0.1: + version "21.6.13" + resolved "https://registry.yarnpkg.com/i18next/-/i18next-21.6.13.tgz#e881b05f156ac06997e9b63379d8b2674bb4a4f2" + integrity sha512-MVjNttw+5mIuu2/fwTpSU0EeI7iU/6pnDvGQboCzkILiv0/gD+FLZaF7qSHmUHO4ZkE6xJQ9SlBgGvMHxhC82Q== + dependencies: + "@babel/runtime" "^7.12.0" + +i18next@^21.6.14: version "21.6.14" resolved "https://registry.yarnpkg.com/i18next/-/i18next-21.6.14.tgz#2bc199fba7f4da44b5952d7df0a3814a6e5c3943" integrity sha512-XL6WyD+xlwQwbieXRlXhKWoLb/rkch50/rA+vl6untHnJ+aYnkQ0YDZciTWE78PPhOpbi2gR0LTJCJpiAhA+uQ== @@ -6241,7 +6248,7 @@ react-dom@^17.0.1: object-assign "^4.1.1" scheduler "^0.20.2" -react-i18next@^11.15.5: +react-i18next@^11.16.2: version "11.16.2" resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-11.16.2.tgz#650b18c12a624057ee2651ba4b4a989b526be554" integrity sha512-1iuZduvARUelL5ux663FvIoDZExwFO+9QtRAAt4uvs1/aun4cUZt8XBrVg7iiDgNls9cOSORAhE7Ri5KA9RMvg==