From 37831e3f5b452f977b8a37f841190b099f1178b6 Mon Sep 17 00:00:00 2001 From: Nicolas De Boose Date: Thu, 13 May 2021 16:31:35 +0200 Subject: [PATCH] fix(1181): Get namespaces from current locale/all locales + all fallbackLng --- src/config/createConfig.test.ts | 20 +++- src/config/createConfig.ts | 49 +++++++-- src/serverSideTranslations.test.ts | 171 ++++++++++++++++++++++++++--- src/serverSideTranslations.ts | 31 +++++- 4 files changed, 245 insertions(+), 26 deletions(-) diff --git a/src/config/createConfig.test.ts b/src/config/createConfig.test.ts index 3861de6c..d29058b8 100644 --- a/src/config/createConfig.test.ts +++ b/src/config/createConfig.test.ts @@ -20,7 +20,7 @@ describe('createConfig', () => { describe('when filesystem is as expected', () => { beforeAll(() => { (fs.existsSync as jest.Mock).mockReturnValue(true); - (fs.readdirSync as jest.Mock).mockReturnValue([]) + (fs.readdirSync as jest.Mock).mockImplementation((locale)=>[`namespace-of-${locale.split('/').pop()}`]) }) it('throws when lng is not provided', () => { @@ -41,7 +41,7 @@ describe('createConfig', () => { expect(config.localePath).toEqual('./public/locales') expect(config.localeStructure).toEqual('{{lng}}/{{ns}}') expect(config.locales).toEqual(['en']) - expect(config.ns).toEqual([]) + expect(config.ns).toEqual(['namespace-of-en']) expect(config.preload).toEqual(['en']) expect(config.strictMode).toEqual(true) expect(config.use).toEqual([]) @@ -50,6 +50,22 @@ describe('createConfig', () => { expect(fs.readdirSync).toHaveBeenCalledTimes(1) }) + it('gets namespaces from current language + fallback (as string) when ns is not provided', ()=>{ + const config = createConfig({ fallbackLng:'en', lng: 'en-US' } as UserConfig) + expect(config.ns).toEqual(['namespace-of-en-US', 'namespace-of-en']) + }) + + it('gets namespaces from current language + fallback (as array) when ns is not provided', ()=>{ + const config = createConfig({ fallbackLng: ['en', 'fr'], lng: 'en-US' } as UserConfig) + expect(config.ns).toEqual(['namespace-of-en-US', 'namespace-of-en', 'namespace-of-fr']) + }) + + it('gets namespaces from current language + fallback (as object) when ns is not provided', ()=>{ + const fallbackLng = {default: ['fr'], 'en-US': ['en']} as unknown + const config = createConfig({ fallbackLng, lng: 'en-US' } as UserConfig) + expect(config.ns).toEqual(['namespace-of-en-US', 'namespace-of-fr', 'namespace-of-en']) + }) + it('deep merges backend', () => { const config = createConfig({ backend: { diff --git a/src/config/createConfig.ts b/src/config/createConfig.ts index 895c0c6d..003b9239 100644 --- a/src/config/createConfig.ts +++ b/src/config/createConfig.ts @@ -1,5 +1,6 @@ import { defaultConfig } from './defaultConfig' import { InternalConfig, UserConfig } from '../types' +import { FallbackLng } from 'i18next' const deepMergeObjects = ['backend', 'detection'] as (keyof Pick)[] @@ -40,7 +41,6 @@ export const createConfig = (userConfig: UserConfig): InternalConfig => { if (typeof combinedConfig.fallbackLng === 'undefined') { combinedConfig.fallbackLng = combinedConfig.defaultLocale } - if (!process.browser && typeof window === 'undefined') { combinedConfig.preload = locales @@ -76,12 +76,47 @@ export const createConfig = (userConfig: UserConfig): InternalConfig => { // // Set server side preload (namespaces) // - if (!combinedConfig.ns) { - const getAllNamespaces = (p: string) => - fs.readdirSync(p).map( - (file: string) => file.replace(`.${localeExtension}`, '') - ) - combinedConfig.ns = getAllNamespaces(path.resolve(process.cwd(), `${serverLocalePath}/${lng}`)) + if (!combinedConfig.ns && typeof lng !== 'undefined') { + const unique = (list:string[]) => Array.from(new Set(list)) + const getNamespaces = (locales: string[]): string[] => { + const getLocaleNamespaces = (p: string) => + fs.readdirSync(p).map( + (file: string) => file.replace(`.${localeExtension}`, '') + ) + + const namespaces: string[] = locales + .map(locale => getLocaleNamespaces(path.resolve(process.cwd(), `${serverLocalePath}/${locale}`))) + .reduce( + (flattenNamespaces, namespaces) => + [...flattenNamespaces,...namespaces], + [] + ) + + return unique(namespaces) + } + + const getAllLocales = ( + lng: string, + fallbackLng: false | FallbackLng + ): string[] => { + if (typeof fallbackLng === 'string') + return unique([lng, fallbackLng]) + + if (Array.isArray(fallbackLng)) + return unique([lng, ...fallbackLng]) + + if (typeof fallbackLng === 'object') { + const flattenedFallbacks = Object + .values(fallbackLng) + .reduce(((all, fallbackLngs) => [...all,...fallbackLngs]),[]) + return unique([lng, ...flattenedFallbacks]) + } + return [lng] + } + + combinedConfig.ns = getNamespaces( + getAllLocales(lng, combinedConfig.fallbackLng) + ) } } } else { diff --git a/src/serverSideTranslations.test.ts b/src/serverSideTranslations.test.ts index 3b9587a0..cc970db0 100644 --- a/src/serverSideTranslations.test.ts +++ b/src/serverSideTranslations.test.ts @@ -21,21 +21,162 @@ describe('serverSideTranslations', () => { .toThrow('Initial locale argument was not passed into serverSideTranslations') }) - it('returns all namespaces if namespacesRequired is not provided', async () => { - (fs.readdirSync as jest.Mock).mockReturnValue(['one', 'two', 'three']) - const props = await serverSideTranslations('en-US', undefined, { - i18n: { - defaultLocale: 'en-US', - locales: ['en-US', 'fr-CA'], - }, - } as UserConfig) - expect(fs.readdirSync).toHaveBeenCalledTimes(1) - expect(fs.readdirSync).toHaveBeenCalledWith(expect.stringMatching('/public/locales/en-US')) - expect(props._nextI18Next.initialI18nStore) - .toEqual({ - 'en-US': { one: {}, three: {}, two: {} }, - 'fr-CA': { one: {}, three: {}, two: {} }} - ) + describe('When namespacesRequired is not provided', ()=>{ + beforeEach(() =>{ + (fs.readdirSync as jest.Mock).mockImplementation((path)=>['common', `namespace-of-${path.split('/').pop()}`]) + }) + + it('returns all namespaces', async () => { + const props = await serverSideTranslations('en-US', undefined, { + i18n: { + defaultLocale: 'en-US', + locales: ['en-US', 'fr-CA'], + }, + } as UserConfig) + expect(fs.readdirSync).toHaveBeenCalledTimes(2) + expect(fs.readdirSync).toHaveBeenCalledWith(expect.stringMatching('/public/locales/en-US')) + expect(fs.readdirSync).toHaveBeenCalledWith(expect.stringMatching('/public/locales/fr-CA')) + expect(props._nextI18Next.initialI18nStore) + .toEqual({ + 'en-US': { + common: {}, + 'namespace-of-en-US': {}, + 'namespace-of-fr-CA': {}, + }, + 'fr-CA': { + common: {}, + 'namespace-of-en-US': {}, + 'namespace-of-fr-CA': {}, + }, + }) + }) + + it('returns all namespaces with fallbackLng (as string)', async () => { + const props = await serverSideTranslations('en-US', undefined, { + i18n: { + defaultLocale: 'fr-BE', + fallbackLng: 'fr', + locales: ['nl-BE', 'fr-BE'], + }, + } as UserConfig) + expect(fs.readdirSync).toHaveBeenCalledTimes(3) + expect(fs.readdirSync).toHaveBeenCalledWith(expect.stringMatching('/public/locales/nl-BE')) + expect(fs.readdirSync).toHaveBeenCalledWith(expect.stringMatching('/public/locales/fr-BE')) + expect(fs.readdirSync).toHaveBeenCalledWith(expect.stringMatching('/public/locales/fr')) + expect(props._nextI18Next.initialI18nStore) + .toEqual({ + fr: { + common: {}, + 'namespace-of-fr': {}, + 'namespace-of-fr-BE': {}, + 'namespace-of-nl-BE': {}, + }, + 'fr-BE': { + common: {}, + 'namespace-of-fr': {}, + 'namespace-of-fr-BE': {}, + 'namespace-of-nl-BE': {}, + }, + 'nl-BE': { + common: {}, + 'namespace-of-fr': {}, + 'namespace-of-fr-BE': {}, + 'namespace-of-nl-BE': {}, + }, + }) + }) + + it('returns all namespaces with fallbackLng (as array)', async () => { + const props = await serverSideTranslations('en-US', undefined, { + i18n: { + defaultLocale: 'en-US', + fallbackLng: ['en','fr'], + locales: ['en-US', 'fr-CA'], + }, + } as UserConfig) + expect(fs.readdirSync).toHaveBeenCalledTimes(4) + expect(fs.readdirSync).toHaveBeenCalledWith(expect.stringMatching('/public/locales/fr-CA')) + expect(fs.readdirSync).toHaveBeenCalledWith(expect.stringMatching('/public/locales/en-US')) + expect(fs.readdirSync).toHaveBeenCalledWith(expect.stringMatching('/public/locales/en')) + expect(fs.readdirSync).toHaveBeenCalledWith(expect.stringMatching('/public/locales/fr')) + expect(props._nextI18Next.initialI18nStore) + .toEqual({ + en: { + common: {}, + 'namespace-of-en': {}, + 'namespace-of-en-US': {}, + 'namespace-of-fr': {}, + 'namespace-of-fr-CA': {}, + }, + 'en-US': { + common: {}, + 'namespace-of-en': {}, + 'namespace-of-en-US': {}, + 'namespace-of-fr': {}, + 'namespace-of-fr-CA': {}, + }, + fr: { + common: {}, + 'namespace-of-en': {}, + 'namespace-of-en-US': {}, + 'namespace-of-fr': {}, + 'namespace-of-fr-CA': {}, + }, + 'fr-CA': { + common: {}, + 'namespace-of-en': {}, + 'namespace-of-en-US': {}, + 'namespace-of-fr': {}, + 'namespace-of-fr-CA': {}, + }, + }) + }) + + it('returns all namespaces with fallbackLng (as object)', async () => { + const props = await serverSideTranslations('en-US', undefined, { + i18n: { + defaultLocale: 'nl-BE', + fallbackLng: {default:['fr'], 'nl-BE':['en']}, + locales: ['nl-BE', 'fr-BE'], + }, + } as UserConfig) + expect(fs.readdirSync).toHaveBeenCalledTimes(4) + expect(fs.readdirSync).toHaveBeenCalledWith(expect.stringMatching('/public/locales/fr-BE')) + expect(fs.readdirSync).toHaveBeenCalledWith(expect.stringMatching('/public/locales/nl-BE')) + expect(fs.readdirSync).toHaveBeenCalledWith(expect.stringMatching('/public/locales/en')) + expect(fs.readdirSync).toHaveBeenCalledWith(expect.stringMatching('/public/locales/fr')) + expect(props._nextI18Next.initialI18nStore) + .toEqual({ + en: { + common: {}, + 'namespace-of-en': {}, + 'namespace-of-fr': {}, + 'namespace-of-fr-BE': {}, + 'namespace-of-nl-BE': {}, + }, + fr: { + common: {}, + 'namespace-of-en': {}, + 'namespace-of-fr': {}, + 'namespace-of-fr-BE': {}, + 'namespace-of-nl-BE': {}, + }, + 'fr-BE': { + common: {}, + 'namespace-of-en': {}, + 'namespace-of-fr': {}, + 'namespace-of-fr-BE': {}, + 'namespace-of-nl-BE': {}, + }, + 'nl-BE': { + common: {}, + 'namespace-of-en': {}, + 'namespace-of-fr': {}, + 'namespace-of-fr-BE': {}, + 'namespace-of-nl-BE': {}, + }, + }) + }) }) it('returns props', async () => { diff --git a/src/serverSideTranslations.ts b/src/serverSideTranslations.ts index 34ecd917..c9c09d1c 100644 --- a/src/serverSideTranslations.ts +++ b/src/serverSideTranslations.ts @@ -5,9 +5,25 @@ import { createConfig } from './config/createConfig' import createClient from './createClient' import { UserConfig, SSRConfig } from './types' +import { FallbackLng } from 'i18next' const DEFAULT_CONFIG_PATH = './next-i18next.config.js' +const getFallBackLocales = (fallbackLng: false | FallbackLng) => { + if (typeof fallbackLng === 'string') { + return [fallbackLng] + } + if (Array.isArray(fallbackLng)) { + return fallbackLng + } + if (typeof fallbackLng === 'object') { + return Object + .values(fallbackLng) + .reduce((all, locales) => [...all, ...locales],[]) + } + return [] +} + export const serverSideTranslations = async ( initialLocale: string, namespacesRequired: string[] = [], @@ -33,9 +49,9 @@ export const serverSideTranslations = async ( }) const { - defaultLocale, localeExtension, localePath, + fallbackLng, } = config const { i18n, initPromise } = createClient({ @@ -51,12 +67,23 @@ export const serverSideTranslations = async ( initialI18nStore[lng] = {} }) + getFallBackLocales(fallbackLng).forEach(lng => { + initialI18nStore[lng] = {} + }) + if (namespacesRequired.length === 0) { const getAllNamespaces = (path: string) => fs.readdirSync(path) .map(file => file.replace(`.${localeExtension}`, '')) - namespacesRequired = getAllNamespaces(path.resolve(process.cwd(), `${localePath}/${defaultLocale}`)) + const allNamespaces = Object.keys(initialI18nStore) + .map(locale => getAllNamespaces(path.resolve(process.cwd(), `${localePath}/${locale}`))) + .reduce( + (allNamespaces, namespaces) => [...allNamespaces,...namespaces], + [] + ) + + namespacesRequired = Array.from(new Set(allNamespaces)) } namespacesRequired.forEach((ns) => {