diff --git a/worklenz-frontend/src/App.tsx b/worklenz-frontend/src/App.tsx index 37a581b6..0658c25c 100644 --- a/worklenz-frontend/src/App.tsx +++ b/worklenz-frontend/src/App.tsx @@ -2,7 +2,6 @@ import React, { Suspense, useEffect, memo, useMemo, useCallback } from 'react'; import { RouterProvider } from 'react-router-dom'; import i18next from 'i18next'; -import { ensureTranslationsLoaded } from './i18n'; // Components import ThemeWrapper from './features/theme/ThemeWrapper'; @@ -66,8 +65,8 @@ const App: React.FC = memo(() => { // Initialize CSRF token await initializeCsrfToken(); - // Preload essential translations - await ensureTranslationsLoaded(); + // Note: Translation preloading is handled in i18n.ts initialization + // No need to call ensureTranslationsLoaded here to avoid duplicate requests } catch (error) { if (isMounted) { logger.error('Failed to initialize app:', error); diff --git a/worklenz-frontend/src/hooks/useTranslationPreloader.ts b/worklenz-frontend/src/hooks/useTranslationPreloader.ts index 7d2ae56c..46b3ad86 100644 --- a/worklenz-frontend/src/hooks/useTranslationPreloader.ts +++ b/worklenz-frontend/src/hooks/useTranslationPreloader.ts @@ -26,7 +26,7 @@ export const useTranslationPreloader = ( try { setIsLoading(true); - // Ensure translations are loaded + // Only load translations for current language to avoid multiple requests await ensureTranslationsLoaded(namespaces); // Wait for i18next to be ready @@ -47,12 +47,18 @@ export const useTranslationPreloader = ( } }; - loadTranslations(); + // Only load if not already loaded + if (!isLoaded && !ready) { + loadTranslations(); + } else if (ready && !isLoaded) { + setIsLoaded(true); + setIsLoading(false); + } return () => { isMounted = false; }; - }, [namespaces, ready]); + }, [namespaces, ready, isLoaded]); return { t, diff --git a/worklenz-frontend/src/i18n.ts b/worklenz-frontend/src/i18n.ts index 7c336dd0..eebcb6d2 100644 --- a/worklenz-frontend/src/i18n.ts +++ b/worklenz-frontend/src/i18n.ts @@ -12,6 +12,10 @@ const ESSENTIAL_NAMESPACES = [ 'settings', ]; +// Cache to track loaded translations and prevent duplicate requests +const loadedTranslations = new Set(); +const loadingPromises = new Map>(); + i18n .use(HttpApi) .use(initReactI18next) @@ -37,33 +41,67 @@ i18n }); // Utility function to ensure translations are loaded -export const ensureTranslationsLoaded = async (namespaces: string[] = ESSENTIAL_NAMESPACES) => { - const currentLang = i18n.language || 'en'; - +export const ensureTranslationsLoaded = async ( + namespaces: string[] = ESSENTIAL_NAMESPACES, + languages: string[] = [i18n.language || 'en'] +) => { try { - // Load all essential namespaces for the current language - await Promise.all( - namespaces.map(ns => - i18n.loadNamespaces(ns).catch(() => { - logger.error(`Failed to load namespace: ${ns}`); - }) - ) - ); + const loadPromises: Promise[] = []; - // Also preload for other languages to prevent delays on language switch - const otherLangs = ['en', 'es', 'pt', 'alb', 'de'].filter(lang => lang !== currentLang); - await Promise.all( - otherLangs.map(lang => - Promise.all( - namespaces.map(ns => - i18n.loadNamespaces(ns).catch(() => { - logger.error(`Failed to load namespace: ${ns}`); - }) - ) - ) - ) - ); + for (const lang of languages) { + for (const ns of namespaces) { + const key = `${lang}:${ns}`; + + // Skip if already loaded + if (loadedTranslations.has(key)) { + continue; + } + // Check if already loading + if (loadingPromises.has(key)) { + loadPromises.push(loadingPromises.get(key)!); + continue; + } + + // Create loading promise + const loadingPromise = new Promise((resolve, reject) => { + // Switch to the target language temporarily if needed + const currentLang = i18n.language; + const shouldSwitchLang = currentLang !== lang; + + const loadForLanguage = async () => { + try { + if (shouldSwitchLang) { + await i18n.changeLanguage(lang); + } + + await i18n.loadNamespaces(ns); + + // Switch back to original language if we changed it + if (shouldSwitchLang && currentLang) { + await i18n.changeLanguage(currentLang); + } + + loadedTranslations.add(key); + resolve(); + } catch (error) { + logger.error(`Failed to load namespace: ${ns} for language: ${lang}`, error); + reject(error); + } finally { + loadingPromises.delete(key); + } + }; + + loadForLanguage(); + }); + + loadingPromises.set(key, loadingPromise); + loadPromises.push(loadingPromise); + } + } + + // Wait for all loading promises to complete + await Promise.all(loadPromises); return true; } catch (error) { logger.error('Failed to load translations:', error); @@ -71,7 +109,13 @@ export const ensureTranslationsLoaded = async (namespaces: string[] = ESSENTIAL_ } }; -// Initialize translations on app startup -ensureTranslationsLoaded(); +// Preload essential translations for current language only on startup +const initializeTranslations = async () => { + const currentLang = i18n.language || 'en'; + await ensureTranslationsLoaded(ESSENTIAL_NAMESPACES, [currentLang]); +}; + +// Initialize translations on app startup (only once) +initializeTranslations(); export default i18n;