diff --git a/worklenz-frontend/src/App.tsx b/worklenz-frontend/src/App.tsx index 1116b739..404cddd1 100644 --- a/worklenz-frontend/src/App.tsx +++ b/worklenz-frontend/src/App.tsx @@ -2,6 +2,7 @@ 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'; @@ -56,15 +57,25 @@ const App: React.FC = memo(() => { handleLanguageChange(language || Language.EN); }, [language, handleLanguageChange]); - // Initialize CSRF token on app startup - memoize to prevent re-initialization + // Initialize CSRF token and translations on app startup useEffect(() => { let isMounted = true; - initializeCsrfToken().catch(error => { - if (isMounted) { - logger.error('Failed to initialize CSRF token:', error); + const initializeApp = async () => { + try { + // Initialize CSRF token + await initializeCsrfToken(); + + // Preload essential translations + await ensureTranslationsLoaded(); + } catch (error) { + if (isMounted) { + logger.error('Failed to initialize app:', error); + } } - }); + }; + + initializeApp(); return () => { isMounted = false; diff --git a/worklenz-frontend/src/components/task-management/optimized-bulk-action-bar.tsx b/worklenz-frontend/src/components/task-management/optimized-bulk-action-bar.tsx index ea008987..9ce9f643 100644 --- a/worklenz-frontend/src/components/task-management/optimized-bulk-action-bar.tsx +++ b/worklenz-frontend/src/components/task-management/optimized-bulk-action-bar.tsx @@ -21,10 +21,10 @@ import { FlagOutlined, BulbOutlined } from '@ant-design/icons'; -import { useTranslation } from 'react-i18next'; import { useSelector } from 'react-redux'; import { RootState } from '@/app/store'; import { useAppSelector } from '@/hooks/useAppSelector'; +import { useBulkActionTranslations } from '@/hooks/useTranslationPreloader'; const { Text } = Typography; @@ -138,7 +138,7 @@ const OptimizedBulkActionBarContent: React.FC = Rea onBulkExport, onBulkSetDueDate, }) => { - const { t } = useTranslation('tasks/task-table-bulk-actions'); + const { t, ready, isLoading } = useBulkActionTranslations(); const isDarkMode = useSelector((state: RootState) => state.themeReducer?.mode === 'dark'); // Get data from Redux store @@ -324,6 +324,11 @@ const OptimizedBulkActionBarContent: React.FC = Rea whiteSpace: 'nowrap' as const, }), [isDarkMode]); + // Don't render until translations are ready to prevent Suspense + if (!ready || isLoading) { + return null; + } + if (!totalSelected || Number(totalSelected) < 1) { return null; } diff --git a/worklenz-frontend/src/components/task-management/task-list-board.tsx b/worklenz-frontend/src/components/task-management/task-list-board.tsx index 668dc961..ed344b4d 100644 --- a/worklenz-frontend/src/components/task-management/task-list-board.tsx +++ b/worklenz-frontend/src/components/task-management/task-list-board.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useState, useMemo, useCallback, useRef } from 'react'; import { useSelector, useDispatch } from 'react-redux'; -import { useTranslation } from 'react-i18next'; +import { useTaskManagementTranslations } from '@/hooks/useTranslationPreloader'; import { DndContext, DragOverlay, @@ -124,7 +124,7 @@ const throttle = void>(func: T, delay: number): T const TaskListBoard: React.FC = ({ projectId, className = '' }) => { const dispatch = useDispatch(); - const { t } = useTranslation('task-management'); + const { t, ready, isLoading } = useTaskManagementTranslations(); const { trackMixpanelEvent } = useMixpanelTracking(); const [dragState, setDragState] = useState({ activeTask: null, @@ -658,6 +658,17 @@ const TaskListBoard: React.FC = ({ projectId, className = '' }; }, []); + // Don't render until translations are ready to prevent Suspense + if (!ready || isLoading) { + return ( + +
+ +
+
+ ); + } + if (error) { return ( diff --git a/worklenz-frontend/src/hooks/useTranslationPreloader.ts b/worklenz-frontend/src/hooks/useTranslationPreloader.ts new file mode 100644 index 00000000..abf20b22 --- /dev/null +++ b/worklenz-frontend/src/hooks/useTranslationPreloader.ts @@ -0,0 +1,77 @@ +import { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { ensureTranslationsLoaded } from '@/i18n'; + +interface UseTranslationPreloaderOptions { + namespaces?: string[]; + fallback?: React.ReactNode; +} + +/** + * Hook to ensure translations are loaded before rendering components + * This prevents Suspense issues when components use useTranslation + */ +export const useTranslationPreloader = ( + namespaces: string[] = ['tasks/task-table-bulk-actions', 'task-management'], + options: UseTranslationPreloaderOptions = {} +) => { + const [isLoaded, setIsLoaded] = useState(false); + const [isLoading, setIsLoading] = useState(true); + const { t, ready } = useTranslation(namespaces); + + useEffect(() => { + let isMounted = true; + + const loadTranslations = async () => { + try { + setIsLoading(true); + + // Ensure translations are loaded + await ensureTranslationsLoaded(namespaces); + + // Wait for i18next to be ready + if (!ready) { + // If i18next is not ready, wait a bit and check again + await new Promise(resolve => setTimeout(resolve, 100)); + } + + if (isMounted) { + setIsLoaded(true); + setIsLoading(false); + } + } catch (error) { + if (isMounted) { + setIsLoaded(true); // Still set as loaded to prevent infinite loading + setIsLoading(false); + } + } + }; + + loadTranslations(); + + return () => { + isMounted = false; + }; + }, [namespaces, ready]); + + return { + t, + ready: isLoaded && ready, + isLoading, + isLoaded, + }; +}; + +/** + * Hook specifically for bulk action bar translations + */ +export const useBulkActionTranslations = () => { + return useTranslationPreloader(['tasks/task-table-bulk-actions']); +}; + +/** + * Hook for task management translations + */ +export const useTaskManagementTranslations = () => { + return useTranslationPreloader(['task-management', 'tasks/task-table-bulk-actions']); +}; \ No newline at end of file diff --git a/worklenz-frontend/src/i18n.ts b/worklenz-frontend/src/i18n.ts index f2bc0994..b325bb68 100644 --- a/worklenz-frontend/src/i18n.ts +++ b/worklenz-frontend/src/i18n.ts @@ -1,6 +1,16 @@ import i18n from 'i18next'; import { initReactI18next } from 'react-i18next'; import HttpApi from 'i18next-http-backend'; +import logger from './utils/errorLogger'; + +// Essential namespaces that should be preloaded to prevent Suspense +const ESSENTIAL_NAMESPACES = [ + 'common', + 'tasks/task-table-bulk-actions', + 'task-management', + 'auth/login', + 'settings' +]; i18n .use(HttpApi) @@ -11,9 +21,57 @@ i18n loadPath: '/locales/{{lng}}/{{ns}}.json', }, defaultNS: 'common', + ns: ESSENTIAL_NAMESPACES, interpolation: { escapeValue: false, }, + // Preload essential namespaces + preload: ['en', 'es', 'pt', 'alb', 'de'], + // Load all namespaces on initialization + load: 'languageOnly', + // Cache translations + cache: { + enabled: true, + expirationTime: 24 * 60 * 60 * 1000, // 24 hours + }, }); +// Utility function to ensure translations are loaded +export const ensureTranslationsLoaded = async (namespaces: string[] = ESSENTIAL_NAMESPACES) => { + const currentLang = 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}`); + }) + ) + ); + + // 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}`); + }) + ) + ) + ) + ); + + return true; + } catch (error) { + logger.error('Failed to load translations:', error); + return false; + } +}; + +// Initialize translations on app startup +ensureTranslationsLoaded(); + export default i18n;