feat(i18n): enhance translation loading and preloading mechanism
- Introduced a utility function `ensureTranslationsLoaded` to preload essential translation namespaces, improving app initialization. - Updated `App` component to initialize translations alongside CSRF token on startup. - Created custom hooks `useTranslationPreloader`, `useBulkActionTranslations`, and `useTaskManagementTranslations` to manage translation readiness and prevent Suspense issues. - Refactored components to utilize new translation hooks, ensuring translations are ready before rendering. - Enhanced `OptimizedBulkActionBar` and `TaskListBoard` components to improve user experience during language switching.
This commit is contained in:
@@ -2,6 +2,7 @@
|
|||||||
import React, { Suspense, useEffect, memo, useMemo, useCallback } from 'react';
|
import React, { Suspense, useEffect, memo, useMemo, useCallback } from 'react';
|
||||||
import { RouterProvider } from 'react-router-dom';
|
import { RouterProvider } from 'react-router-dom';
|
||||||
import i18next from 'i18next';
|
import i18next from 'i18next';
|
||||||
|
import { ensureTranslationsLoaded } from './i18n';
|
||||||
|
|
||||||
// Components
|
// Components
|
||||||
import ThemeWrapper from './features/theme/ThemeWrapper';
|
import ThemeWrapper from './features/theme/ThemeWrapper';
|
||||||
@@ -56,15 +57,25 @@ const App: React.FC = memo(() => {
|
|||||||
handleLanguageChange(language || Language.EN);
|
handleLanguageChange(language || Language.EN);
|
||||||
}, [language, handleLanguageChange]);
|
}, [language, handleLanguageChange]);
|
||||||
|
|
||||||
// Initialize CSRF token on app startup - memoize to prevent re-initialization
|
// Initialize CSRF token and translations on app startup
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let isMounted = true;
|
let isMounted = true;
|
||||||
|
|
||||||
initializeCsrfToken().catch(error => {
|
const initializeApp = async () => {
|
||||||
if (isMounted) {
|
try {
|
||||||
logger.error('Failed to initialize CSRF token:', error);
|
// Initialize CSRF token
|
||||||
|
await initializeCsrfToken();
|
||||||
|
|
||||||
|
// Preload essential translations
|
||||||
|
await ensureTranslationsLoaded();
|
||||||
|
} catch (error) {
|
||||||
|
if (isMounted) {
|
||||||
|
logger.error('Failed to initialize app:', error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
|
||||||
|
initializeApp();
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
isMounted = false;
|
isMounted = false;
|
||||||
|
|||||||
@@ -21,10 +21,10 @@ import {
|
|||||||
FlagOutlined,
|
FlagOutlined,
|
||||||
BulbOutlined
|
BulbOutlined
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { RootState } from '@/app/store';
|
import { RootState } from '@/app/store';
|
||||||
import { useAppSelector } from '@/hooks/useAppSelector';
|
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||||
|
import { useBulkActionTranslations } from '@/hooks/useTranslationPreloader';
|
||||||
|
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
|
|
||||||
@@ -138,7 +138,7 @@ const OptimizedBulkActionBarContent: React.FC<OptimizedBulkActionBarProps> = Rea
|
|||||||
onBulkExport,
|
onBulkExport,
|
||||||
onBulkSetDueDate,
|
onBulkSetDueDate,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation('tasks/task-table-bulk-actions');
|
const { t, ready, isLoading } = useBulkActionTranslations();
|
||||||
const isDarkMode = useSelector((state: RootState) => state.themeReducer?.mode === 'dark');
|
const isDarkMode = useSelector((state: RootState) => state.themeReducer?.mode === 'dark');
|
||||||
|
|
||||||
// Get data from Redux store
|
// Get data from Redux store
|
||||||
@@ -324,6 +324,11 @@ const OptimizedBulkActionBarContent: React.FC<OptimizedBulkActionBarProps> = Rea
|
|||||||
whiteSpace: 'nowrap' as const,
|
whiteSpace: 'nowrap' as const,
|
||||||
}), [isDarkMode]);
|
}), [isDarkMode]);
|
||||||
|
|
||||||
|
// Don't render until translations are ready to prevent Suspense
|
||||||
|
if (!ready || isLoading) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
if (!totalSelected || Number(totalSelected) < 1) {
|
if (!totalSelected || Number(totalSelected) < 1) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useEffect, useState, useMemo, useCallback, useRef } from 'react';
|
import React, { useEffect, useState, useMemo, useCallback, useRef } from 'react';
|
||||||
import { useSelector, useDispatch } from 'react-redux';
|
import { useSelector, useDispatch } from 'react-redux';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTaskManagementTranslations } from '@/hooks/useTranslationPreloader';
|
||||||
import {
|
import {
|
||||||
DndContext,
|
DndContext,
|
||||||
DragOverlay,
|
DragOverlay,
|
||||||
@@ -124,7 +124,7 @@ const throttle = <T extends (...args: any[]) => void>(func: T, delay: number): T
|
|||||||
|
|
||||||
const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = '' }) => {
|
const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = '' }) => {
|
||||||
const dispatch = useDispatch<AppDispatch>();
|
const dispatch = useDispatch<AppDispatch>();
|
||||||
const { t } = useTranslation('task-management');
|
const { t, ready, isLoading } = useTaskManagementTranslations();
|
||||||
const { trackMixpanelEvent } = useMixpanelTracking();
|
const { trackMixpanelEvent } = useMixpanelTracking();
|
||||||
const [dragState, setDragState] = useState<DragState>({
|
const [dragState, setDragState] = useState<DragState>({
|
||||||
activeTask: null,
|
activeTask: null,
|
||||||
@@ -658,6 +658,17 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Don't render until translations are ready to prevent Suspense
|
||||||
|
if (!ready || isLoading) {
|
||||||
|
return (
|
||||||
|
<Card className={className}>
|
||||||
|
<div className="flex justify-center items-center py-8">
|
||||||
|
<Spin size="large" />
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<Card className={className}>
|
<Card className={className}>
|
||||||
|
|||||||
77
worklenz-frontend/src/hooks/useTranslationPreloader.ts
Normal file
77
worklenz-frontend/src/hooks/useTranslationPreloader.ts
Normal file
@@ -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']);
|
||||||
|
};
|
||||||
@@ -1,6 +1,16 @@
|
|||||||
import i18n from 'i18next';
|
import i18n from 'i18next';
|
||||||
import { initReactI18next } from 'react-i18next';
|
import { initReactI18next } from 'react-i18next';
|
||||||
import HttpApi from 'i18next-http-backend';
|
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
|
i18n
|
||||||
.use(HttpApi)
|
.use(HttpApi)
|
||||||
@@ -11,9 +21,57 @@ i18n
|
|||||||
loadPath: '/locales/{{lng}}/{{ns}}.json',
|
loadPath: '/locales/{{lng}}/{{ns}}.json',
|
||||||
},
|
},
|
||||||
defaultNS: 'common',
|
defaultNS: 'common',
|
||||||
|
ns: ESSENTIAL_NAMESPACES,
|
||||||
interpolation: {
|
interpolation: {
|
||||||
escapeValue: false,
|
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;
|
export default i18n;
|
||||||
|
|||||||
Reference in New Issue
Block a user