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:
chamikaJ
2025-07-02 15:37:24 +05:30
parent 0452dbd179
commit 365369cc31
5 changed files with 171 additions and 9 deletions

View File

@@ -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 => {
const initializeApp = async () => {
try {
// Initialize CSRF token
await initializeCsrfToken();
// Preload essential translations
await ensureTranslationsLoaded();
} catch (error) {
if (isMounted) {
logger.error('Failed to initialize CSRF token:', error);
logger.error('Failed to initialize app:', error);
}
});
}
};
initializeApp();
return () => {
isMounted = false;

View File

@@ -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<OptimizedBulkActionBarProps> = 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<OptimizedBulkActionBarProps> = 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;
}

View File

@@ -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 = <T extends (...args: any[]) => void>(func: T, delay: number): T
const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = '' }) => {
const dispatch = useDispatch<AppDispatch>();
const { t } = useTranslation('task-management');
const { t, ready, isLoading } = useTaskManagementTranslations();
const { trackMixpanelEvent } = useMixpanelTracking();
const [dragState, setDragState] = useState<DragState>({
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) {
return (
<Card className={className}>

View 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']);
};

View File

@@ -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;