From ae10f7b142d6c2207c4e45607520aa04a3a0e01d Mon Sep 17 00:00:00 2001 From: giancarlo Date: Sun, 21 Apr 2024 18:40:12 +0800 Subject: [PATCH] Refactor i18n settings and improve language load handling This update separates the creation of i18n settings into its own function (@kit/i18n) and enhances the handling of language and namespace loading in i18n.client. It tracks loaded languages and namespaces, and prevents rendering if none are loaded or after a maximum number of iterations. The usage of Suspense has also been modified in root-providers to employ a null fallback. --- apps/web/components/root-providers.tsx | 2 +- apps/web/lib/i18n/i18n.settings.ts | 23 +++++---------- packages/i18n/package.json | 1 + packages/i18n/src/create-i18n-settings.ts | 33 +++++++++++++++++++++ packages/i18n/src/i18n-provider.tsx | 34 ++++++++++----------- packages/i18n/src/i18n.client.ts | 36 +++++++++++++++++++++++ packages/i18n/src/index.ts | 1 + 7 files changed, 96 insertions(+), 34 deletions(-) create mode 100644 packages/i18n/src/create-i18n-settings.ts create mode 100644 packages/i18n/src/index.ts diff --git a/apps/web/components/root-providers.tsx b/apps/web/components/root-providers.tsx index 60bb5e159..a899c415f 100644 --- a/apps/web/components/root-providers.tsx +++ b/apps/web/components/root-providers.tsx @@ -46,7 +46,7 @@ export function RootProviders({ return ( - + diff --git a/apps/web/lib/i18n/i18n.settings.ts b/apps/web/lib/i18n/i18n.settings.ts index fc8fc6d1a..9df64efaa 100644 --- a/apps/web/lib/i18n/i18n.settings.ts +++ b/apps/web/lib/i18n/i18n.settings.ts @@ -1,4 +1,4 @@ -import { InitOptions } from 'i18next'; +import { createI18nSettings } from '@kit/i18n'; /** * The default language of the application. @@ -43,7 +43,7 @@ export const defaultI18nNamespaces = [ export function getI18nSettings( language: string | undefined, ns: string | string[] = defaultI18nNamespaces, -): InitOptions { +) { let lng = language ?? defaultLanguage; if (!languages.includes(lng)) { @@ -54,18 +54,9 @@ export function getI18nSettings( lng = defaultLanguage; } - return { - supportedLngs: languages, - fallbackLng: languages[0], - detection: undefined, - lng, - load: 'languageOnly', - preload: false, - lowerCaseLng: true, - fallbackNS: defaultI18nNamespaces, - ns, - react: { - useSuspense: true, - }, - }; + return createI18nSettings({ + language: lng, + namespaces: ns, + languages, + }); } diff --git a/packages/i18n/package.json b/packages/i18n/package.json index 370286341..78db3791e 100644 --- a/packages/i18n/package.json +++ b/packages/i18n/package.json @@ -10,6 +10,7 @@ }, "prettier": "@kit/prettier-config", "exports": { + ".": "./src/index.ts", "./server": "./src/i18n.server.ts", "./client": "./src/i18n.client.ts", "./provider": "./src/i18n-provider.tsx" diff --git a/packages/i18n/src/create-i18n-settings.ts b/packages/i18n/src/create-i18n-settings.ts new file mode 100644 index 000000000..92d3a634a --- /dev/null +++ b/packages/i18n/src/create-i18n-settings.ts @@ -0,0 +1,33 @@ +/** + * Get i18n settings for i18next. + * @param languages + * @param language + * @param namespaces + */ +export function createI18nSettings({ + languages, + language, + namespaces, +}: { + languages: string[]; + language: string; + namespaces?: string | string[]; +}) { + const lng = language; + const ns = namespaces; + + return { + supportedLngs: languages, + fallbackLng: languages[0], + detection: undefined, + lng, + load: 'languageOnly', + preload: false, + lowerCaseLng: true, + fallbackNS: ns, + ns, + react: { + useSuspense: true, + }, + }; +} diff --git a/packages/i18n/src/i18n-provider.tsx b/packages/i18n/src/i18n-provider.tsx index dcfd44d8d..810e0b298 100644 --- a/packages/i18n/src/i18n-provider.tsx +++ b/packages/i18n/src/i18n-provider.tsx @@ -1,7 +1,10 @@ 'use client'; -import { useSuspenseQuery } from '@tanstack/react-query'; -import type { InitOptions } from 'i18next'; +import type { InitOptions, i18n } from 'i18next'; + +import { initializeI18nClient } from './i18n.client'; + +let i18nInstance: i18n; type Resolver = ( lang: string, @@ -28,20 +31,17 @@ export function I18nProvider({ * @param resolver */ function useI18nClient(settings: InitOptions, resolver: Resolver) { - return useSuspenseQuery({ - queryKey: ['i18n', settings.lng], - queryFn: async () => { - const isBrowser = typeof window !== 'undefined'; + if ( + !i18nInstance || + i18nInstance.language !== settings.lng || + i18nInstance.options.ns?.length !== settings.ns?.length + ) { + throw loadI18nInstance(settings, resolver); + } - if (isBrowser) { - const { initializeI18nClient } = await import('./i18n.client'); - - return await initializeI18nClient(settings, resolver); - } else { - const { initializeServerI18n } = await import('./i18n.server'); - - return await initializeServerI18n(settings, resolver); - } - }, - }); + return i18nInstance; +} + +async function loadI18nInstance(settings: InitOptions, resolver: Resolver) { + i18nInstance = await initializeI18nClient(settings, resolver); } diff --git a/packages/i18n/src/i18n.client.ts b/packages/i18n/src/i18n.client.ts index c9713c625..8242c05c2 100644 --- a/packages/i18n/src/i18n.client.ts +++ b/packages/i18n/src/i18n.client.ts @@ -3,6 +3,12 @@ import LanguageDetector from 'i18next-browser-languagedetector'; import resourcesToBackend from 'i18next-resources-to-backend'; import { initReactI18next } from 'react-i18next'; +// Keep track of the number of iterations +let iteration = 0; + +// Maximum number of iterations +const MAX_ITERATIONS = 20; + /** * Initialize the i18n instance on the client. * @param settings - the i18n settings @@ -12,11 +18,22 @@ export async function initializeI18nClient( settings: InitOptions, resolver: (lang: string, namespace: string) => Promise, ): Promise { + const loadedLanguages: string[] = []; + const loadedNamespaces: string[] = []; + await i18next .use( resourcesToBackend(async (language, namespace, callback) => { const data = await resolver(language, namespace); + if (!loadedLanguages.includes(language)) { + loadedLanguages.push(language); + } + + if (!loadedNamespaces.includes(namespace)) { + loadedNamespaces.push(namespace); + } + return callback(null, data); }), ) @@ -41,5 +58,24 @@ export async function initializeI18nClient( }, ); + // to avoid infinite loops, we return the i18next instance after a certain number of iterations + // even if the languages and namespaces are not loaded + if (iteration >= MAX_ITERATIONS) { + console.debug(`Max iterations reached: ${MAX_ITERATIONS}`); + + return i18next; + } + + // keep component from rendering if no languages or namespaces are loaded + if (loadedLanguages.length === 0 || loadedNamespaces.length === 0) { + iteration++; + + console.debug( + `Keeping component from rendering if no languages or namespaces are loaded. Iteration: ${iteration}. Will stop after ${MAX_ITERATIONS} iterations.`, + ); + + throw new Error('No languages or namespaces loaded'); + } + return i18next; } diff --git a/packages/i18n/src/index.ts b/packages/i18n/src/index.ts new file mode 100644 index 000000000..93475c547 --- /dev/null +++ b/packages/i18n/src/index.ts @@ -0,0 +1 @@ +export * from './create-i18n-settings';