chore: moved locale files
This commit is contained in:
@@ -18,7 +18,7 @@ import { RootState } from '@/app/store';
|
||||
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||
import { selectTasks } from '@/features/projects/bulkActions/bulkActionSlice';
|
||||
import { IProjectTask } from '@/types/project/projectTasksViewModel.types';
|
||||
import { useBulkActionTranslations } from '@/hooks/useTranslationPreloader';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import LabelsDropdown from '@/components/taskListCommon/task-list-bulk-actions-bar/components/LabelsDropdown';
|
||||
import AssigneesDropdown from '@/components/taskListCommon/task-list-bulk-actions-bar/components/AssigneesDropdown';
|
||||
import { ITaskLabel } from '@/types/tasks/taskLabel.types';
|
||||
@@ -165,7 +165,7 @@ const OptimizedBulkActionBarContent: React.FC<OptimizedBulkActionBarProps> = Rea
|
||||
onBulkExport,
|
||||
onBulkSetDueDate,
|
||||
}) => {
|
||||
const { t, ready, isLoading } = useBulkActionTranslations();
|
||||
const { t } = useTranslation(['tasks/task-table-bulk-actions', 'task-management']);
|
||||
const dispatch = useDispatch();
|
||||
const isDarkMode = useSelector((state: RootState) => state.themeReducer?.mode === 'dark');
|
||||
|
||||
@@ -549,10 +549,7 @@ const OptimizedBulkActionBarContent: React.FC<OptimizedBulkActionBarProps> = Rea
|
||||
[isDarkMode]
|
||||
);
|
||||
|
||||
// Don't render until translations are ready to prevent Suspense
|
||||
if (!ready || isLoading) {
|
||||
return null;
|
||||
}
|
||||
// Remove translation loading check since we're using simple load-as-you-go approach
|
||||
|
||||
if (!totalSelected || Number(totalSelected) < 1) {
|
||||
return null;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useEffect, useState, useMemo, useCallback, useRef } from 'react';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { useTaskManagementTranslations } from '@/hooks/useTranslationPreloader';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
DndContext,
|
||||
DragOverlay,
|
||||
@@ -123,7 +123,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, ready, isLoading } = useTaskManagementTranslations();
|
||||
const { t } = useTranslation(['task-management', 'task-list-table']);
|
||||
const { trackMixpanelEvent } = useMixpanelTracking();
|
||||
const [dragState, setDragState] = useState<DragState>({
|
||||
activeTask: null,
|
||||
@@ -757,16 +757,7 @@ 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>
|
||||
);
|
||||
}
|
||||
// Remove translation loading check since we're using simple load-as-you-go approach
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
|
||||
@@ -1,283 +0,0 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
ensureTranslationsLoaded,
|
||||
preloadPageTranslations,
|
||||
getPerformanceMetrics,
|
||||
changeLanguageOptimized
|
||||
} from '../i18n';
|
||||
import logger from '../utils/errorLogger';
|
||||
|
||||
// Cache for preloaded translation states
|
||||
const preloadCache = new Map<string, boolean>();
|
||||
const loadingStates = new Map<string, boolean>();
|
||||
|
||||
interface TranslationHookOptions {
|
||||
preload?: boolean;
|
||||
priority?: number;
|
||||
fallbackReady?: boolean;
|
||||
}
|
||||
|
||||
interface TranslationHookReturn {
|
||||
t: (key: string, defaultValue?: string) => string;
|
||||
ready: boolean;
|
||||
isLoading: boolean;
|
||||
error: Error | null;
|
||||
retryLoad: () => Promise<void>;
|
||||
performanceMetrics: any;
|
||||
}
|
||||
|
||||
// Enhanced translation hook with better performance
|
||||
export const useOptimizedTranslation = (
|
||||
namespace: string | string[],
|
||||
options: TranslationHookOptions = {}
|
||||
): TranslationHookReturn => {
|
||||
const { preload = true, priority = 5, fallbackReady = true } = options;
|
||||
|
||||
const namespaces = Array.isArray(namespace) ? namespace : [namespace];
|
||||
const namespaceKey = namespaces.join(',');
|
||||
|
||||
const [ready, setReady] = useState(fallbackReady);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
const hasInitialized = useRef(false);
|
||||
const loadingPromise = useRef<Promise<void> | null>(null);
|
||||
|
||||
const { t, i18n } = useTranslation(namespaces);
|
||||
|
||||
// Memoized preload function
|
||||
const preloadTranslations = useCallback(async () => {
|
||||
const cacheKey = `${i18n.language}:${namespaceKey}`;
|
||||
|
||||
// Skip if already preloaded or currently loading
|
||||
if (preloadCache.get(cacheKey) || loadingStates.get(cacheKey)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
loadingStates.set(cacheKey, true);
|
||||
|
||||
const startTime = performance.now();
|
||||
|
||||
// Use the optimized preload function
|
||||
await preloadPageTranslations(namespaces);
|
||||
|
||||
const endTime = performance.now();
|
||||
const loadTime = endTime - startTime;
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.log(
|
||||
`✅ Preloaded translations for ${namespaceKey} in ${loadTime.toFixed(2)}ms`
|
||||
);
|
||||
}
|
||||
|
||||
preloadCache.set(cacheKey, true);
|
||||
setReady(true);
|
||||
} catch (err) {
|
||||
const error = err instanceof Error ? err : new Error('Failed to preload translations');
|
||||
setError(error);
|
||||
logger.error(`Failed to preload translations for ${namespaceKey}:`, error);
|
||||
|
||||
// Fallback to ready state even on error to prevent blocking UI
|
||||
if (fallbackReady) {
|
||||
setReady(true);
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
loadingStates.set(cacheKey, false);
|
||||
}
|
||||
}, [namespaces, namespaceKey, i18n.language, fallbackReady]);
|
||||
|
||||
// Initialize preloading
|
||||
useEffect(() => {
|
||||
if (!hasInitialized.current && preload) {
|
||||
hasInitialized.current = true;
|
||||
|
||||
if (!loadingPromise.current) {
|
||||
loadingPromise.current = preloadTranslations();
|
||||
}
|
||||
}
|
||||
}, [preload, preloadTranslations]);
|
||||
|
||||
// Handle language changes
|
||||
useEffect(() => {
|
||||
const handleLanguageChange = () => {
|
||||
const cacheKey = `${i18n.language}:${namespaceKey}`;
|
||||
if (!preloadCache.get(cacheKey) && preload) {
|
||||
setReady(false);
|
||||
preloadTranslations();
|
||||
}
|
||||
};
|
||||
|
||||
i18n.on('languageChanged', handleLanguageChange);
|
||||
return () => {
|
||||
i18n.off('languageChanged', handleLanguageChange);
|
||||
};
|
||||
}, [i18n, namespaceKey, preload, preloadTranslations]);
|
||||
|
||||
// Retry function
|
||||
const retryLoad = useCallback(async () => {
|
||||
const cacheKey = `${i18n.language}:${namespaceKey}`;
|
||||
preloadCache.delete(cacheKey);
|
||||
loadingStates.delete(cacheKey);
|
||||
await preloadTranslations();
|
||||
}, [namespaceKey, i18n.language, preloadTranslations]);
|
||||
|
||||
// Get performance metrics
|
||||
const performanceMetrics = useMemo(() => getPerformanceMetrics(), [ready]);
|
||||
|
||||
// Enhanced t function with better error handling
|
||||
const enhancedT = useCallback((key: string, defaultValue?: string) => {
|
||||
try {
|
||||
const translation = t(key, { defaultValue });
|
||||
|
||||
// Return the translation if it's not the key itself (indicating it was found)
|
||||
if (translation !== key) {
|
||||
return translation;
|
||||
}
|
||||
|
||||
// If we have a default value, use it
|
||||
if (defaultValue) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
// Fallback to the key
|
||||
return key;
|
||||
} catch (err) {
|
||||
logger.error(`Translation error for key ${key}:`, err);
|
||||
return defaultValue || key;
|
||||
}
|
||||
}, [t]);
|
||||
|
||||
return {
|
||||
t: enhancedT,
|
||||
ready,
|
||||
isLoading,
|
||||
error,
|
||||
retryLoad,
|
||||
performanceMetrics,
|
||||
};
|
||||
};
|
||||
|
||||
// Specialized hooks for commonly used namespaces
|
||||
export const useTaskManagementTranslations = (options?: TranslationHookOptions) => {
|
||||
return useOptimizedTranslation(['task-management', 'task-list-table'], {
|
||||
priority: 8,
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useBulkActionTranslations = (options?: TranslationHookOptions) => {
|
||||
return useOptimizedTranslation(['tasks/task-table-bulk-actions', 'task-management'], {
|
||||
priority: 6,
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useTaskDrawerTranslations = (options?: TranslationHookOptions) => {
|
||||
return useOptimizedTranslation(['task-drawer/task-drawer', 'task-list-table'], {
|
||||
priority: 7,
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useProjectTranslations = (options?: TranslationHookOptions) => {
|
||||
return useOptimizedTranslation(['project-drawer', 'common'], {
|
||||
priority: 7,
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useSettingsTranslations = (options?: TranslationHookOptions) => {
|
||||
return useOptimizedTranslation(['settings', 'common'], {
|
||||
priority: 4,
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
// Utility function to preload multiple namespaces
|
||||
export const preloadMultipleNamespaces = async (
|
||||
namespaces: string[],
|
||||
priority: number = 5
|
||||
): Promise<boolean> => {
|
||||
try {
|
||||
await Promise.all(
|
||||
namespaces.map(ns => preloadPageTranslations([ns]))
|
||||
);
|
||||
return true;
|
||||
} catch (error) {
|
||||
logger.error('Failed to preload multiple namespaces:', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// Hook for pages that need multiple translation namespaces
|
||||
export const usePageTranslations = (
|
||||
namespaces: string[],
|
||||
options?: TranslationHookOptions
|
||||
) => {
|
||||
const { ready, isLoading, error } = useOptimizedTranslation(namespaces, options);
|
||||
|
||||
// Create individual translation functions for each namespace
|
||||
const translations = useMemo(() => {
|
||||
const result: Record<string, any> = {};
|
||||
|
||||
namespaces.forEach(ns => {
|
||||
const { t } = useTranslation(ns);
|
||||
result[ns] = t;
|
||||
});
|
||||
|
||||
return result;
|
||||
}, [namespaces, ready]);
|
||||
|
||||
return {
|
||||
...translations,
|
||||
ready,
|
||||
isLoading,
|
||||
error,
|
||||
};
|
||||
};
|
||||
|
||||
// Language switching utilities
|
||||
export const useLanguageSwitcher = () => {
|
||||
const [switching, setSwitching] = useState(false);
|
||||
|
||||
const switchLanguage = useCallback(async (language: string) => {
|
||||
try {
|
||||
setSwitching(true);
|
||||
await changeLanguageOptimized(language);
|
||||
|
||||
// Clear preload cache for new language
|
||||
preloadCache.clear();
|
||||
loadingStates.clear();
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Failed to switch language:', error);
|
||||
} finally {
|
||||
setSwitching(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return {
|
||||
switchLanguage,
|
||||
switching,
|
||||
};
|
||||
};
|
||||
|
||||
// Performance monitoring hook
|
||||
export const useTranslationPerformance = () => {
|
||||
const [metrics, setMetrics] = useState(getPerformanceMetrics());
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
setMetrics(getPerformanceMetrics());
|
||||
}, 5000); // Update every 5 seconds
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
return metrics;
|
||||
};
|
||||
@@ -1,379 +1,34 @@
|
||||
import i18n from 'i18next';
|
||||
import { initReactI18next } from 'react-i18next';
|
||||
import HttpApi from 'i18next-http-backend';
|
||||
import LanguageDetector from 'i18next-browser-languagedetector';
|
||||
import LocalStorageBackend from 'i18next-localstorage-backend';
|
||||
import logger from './utils/errorLogger';
|
||||
|
||||
// Essential namespaces that should be preloaded to prevent Suspense
|
||||
const ESSENTIAL_NAMESPACES = [
|
||||
'common',
|
||||
'auth/login',
|
||||
'navbar',
|
||||
];
|
||||
|
||||
// Secondary namespaces that can be loaded on demand
|
||||
const SECONDARY_NAMESPACES = [
|
||||
'tasks/task-table-bulk-actions',
|
||||
'task-management',
|
||||
'settings',
|
||||
'home',
|
||||
'project-drawer',
|
||||
];
|
||||
|
||||
// Tertiary namespaces that can be loaded even later
|
||||
const TERTIARY_NAMESPACES = [
|
||||
'task-drawer/task-drawer',
|
||||
'task-list-table',
|
||||
'phases-drawer',
|
||||
'schedule',
|
||||
'reporting',
|
||||
'admin-center/current-bill',
|
||||
];
|
||||
|
||||
// Cache to track loaded translations and prevent duplicate requests
|
||||
const loadedTranslations = new Set<string>();
|
||||
const loadingPromises = new Map<string, Promise<any>>();
|
||||
|
||||
// Background loading queue for non-essential translations
|
||||
let backgroundLoadingQueue: Array<{ lang: string; ns: string; priority: number }> = [];
|
||||
let isBackgroundLoading = false;
|
||||
|
||||
// Performance monitoring
|
||||
const performanceMetrics = {
|
||||
totalLoadTime: 0,
|
||||
translationsLoaded: 0,
|
||||
cacheHits: 0,
|
||||
cacheMisses: 0,
|
||||
};
|
||||
|
||||
// Enhanced caching configuration
|
||||
const CACHE_CONFIG = {
|
||||
EXPIRATION_TIME: 7 * 24 * 60 * 60 * 1000, // 7 days
|
||||
MAX_CACHE_SIZE: 50, // Maximum number of namespaces to cache
|
||||
CLEANUP_INTERVAL: 24 * 60 * 60 * 1000, // Clean cache daily
|
||||
};
|
||||
import HttpApi from 'i18next-http-backend';
|
||||
|
||||
i18n
|
||||
.use(LocalStorageBackend) // Cache translations to localStorage
|
||||
.use(LanguageDetector) // Detect user language
|
||||
.use(HttpApi) // Fetch translations if not in cache
|
||||
.use(HttpApi)
|
||||
.use(LanguageDetector)
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
fallbackLng: 'en',
|
||||
backend: {
|
||||
loadPath: '/locales/{{lng}}/{{ns}}.json',
|
||||
addPath: '/locales/add/{{lng}}/{{ns}}',
|
||||
// Enhanced LocalStorage caching options
|
||||
backendOptions: [{
|
||||
expirationTime: CACHE_CONFIG.EXPIRATION_TIME,
|
||||
// Store translations more efficiently
|
||||
store: {
|
||||
setItem: (key: string, value: string) => {
|
||||
try {
|
||||
// Compress large translation objects
|
||||
const compressedValue = value.length > 1000 ?
|
||||
JSON.stringify(JSON.parse(value)) : value;
|
||||
localStorage.setItem(key, compressedValue);
|
||||
performanceMetrics.cacheHits++;
|
||||
} catch (error) {
|
||||
logger.error('Failed to store translation in cache:', error);
|
||||
}
|
||||
},
|
||||
getItem: (key: string) => {
|
||||
try {
|
||||
const value = localStorage.getItem(key);
|
||||
if (value) {
|
||||
performanceMetrics.cacheHits++;
|
||||
return value;
|
||||
}
|
||||
performanceMetrics.cacheMisses++;
|
||||
return null;
|
||||
} catch (error) {
|
||||
logger.error('Failed to retrieve translation from cache:', error);
|
||||
performanceMetrics.cacheMisses++;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}, {
|
||||
loadPath: '/locales/{{lng}}/{{ns}}.json',
|
||||
// Add request timeout and retry logic
|
||||
requestOptions: {
|
||||
cache: 'force-cache', // Use browser cache when possible
|
||||
},
|
||||
parse: (data: string) => {
|
||||
try {
|
||||
return JSON.parse(data);
|
||||
} catch (error) {
|
||||
logger.error('Failed to parse translation data:', error);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
}],
|
||||
},
|
||||
defaultNS: 'common',
|
||||
ns: ESSENTIAL_NAMESPACES,
|
||||
|
||||
interpolation: {
|
||||
escapeValue: false,
|
||||
},
|
||||
preload: [],
|
||||
load: 'languageOnly',
|
||||
initImmediate: false,
|
||||
|
||||
detection: {
|
||||
order: ['localStorage', 'navigator'], // Check localStorage first, then browser language
|
||||
order: ['localStorage', 'navigator'],
|
||||
caches: ['localStorage'],
|
||||
// Cache the detected language for faster subsequent loads
|
||||
cookieMinutes: 60 * 24 * 7, // 1 week
|
||||
},
|
||||
// Reduce debug output in production
|
||||
|
||||
debug: process.env.NODE_ENV === 'development',
|
||||
// Performance optimizations
|
||||
cleanCode: true, // Remove code characters
|
||||
keySeparator: false, // Disable key separator for better performance
|
||||
nsSeparator: false, // Disable namespace separator for better performance
|
||||
pluralSeparator: '_', // Use underscore for plural separation
|
||||
|
||||
backend: {
|
||||
loadPath: '/locales/{{lng}}/{{ns}}.json',
|
||||
},
|
||||
|
||||
react: {
|
||||
useSuspense: false, // Disable suspense for better control
|
||||
bindI18n: 'languageChanged loaded', // Only bind necessary events
|
||||
bindI18nStore: false, // Disable store binding for better performance
|
||||
useSuspense: false,
|
||||
},
|
||||
});
|
||||
|
||||
// Optimized function to ensure translations are loaded with priority support
|
||||
export const ensureTranslationsLoaded = async (
|
||||
namespaces: string[] = ESSENTIAL_NAMESPACES,
|
||||
languages: string[] = [i18n.language || 'en'],
|
||||
priority: number = 0
|
||||
) => {
|
||||
const startTime = performance.now();
|
||||
|
||||
try {
|
||||
const loadPromises: Promise<any>[] = [];
|
||||
|
||||
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 with enhanced error handling
|
||||
const loadingPromise = new Promise<void>((resolve, reject) => {
|
||||
const currentLang = i18n.language;
|
||||
const shouldSwitchLang = currentLang !== lang;
|
||||
|
||||
const loadForLanguage = async () => {
|
||||
try {
|
||||
if (shouldSwitchLang) {
|
||||
await i18n.changeLanguage(lang);
|
||||
}
|
||||
|
||||
await i18n.loadNamespaces(ns);
|
||||
|
||||
if (shouldSwitchLang && currentLang) {
|
||||
await i18n.changeLanguage(currentLang);
|
||||
}
|
||||
|
||||
loadedTranslations.add(key);
|
||||
performanceMetrics.translationsLoaded++;
|
||||
resolve();
|
||||
} catch (error) {
|
||||
logger.error(`Failed to load namespace: ${ns} for language: ${lang}`, error);
|
||||
// Don't reject completely, just log and continue
|
||||
resolve(); // Still resolve to prevent blocking other translations
|
||||
} finally {
|
||||
loadingPromises.delete(key);
|
||||
}
|
||||
};
|
||||
|
||||
loadForLanguage();
|
||||
});
|
||||
|
||||
loadingPromises.set(key, loadingPromise);
|
||||
loadPromises.push(loadingPromise);
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all(loadPromises);
|
||||
|
||||
const endTime = performance.now();
|
||||
performanceMetrics.totalLoadTime += (endTime - startTime);
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
logger.error('Failed to load translations:', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// Enhanced background loading function with priority queue
|
||||
const processBackgroundQueue = async () => {
|
||||
if (isBackgroundLoading || backgroundLoadingQueue.length === 0) return;
|
||||
|
||||
isBackgroundLoading = true;
|
||||
|
||||
try {
|
||||
// Sort by priority (higher priority first)
|
||||
backgroundLoadingQueue.sort((a, b) => b.priority - a.priority);
|
||||
|
||||
// Process queue in smaller batches to avoid overwhelming the network
|
||||
const batchSize = 2; // Reduced batch size for better performance
|
||||
while (backgroundLoadingQueue.length > 0) {
|
||||
const batch = backgroundLoadingQueue.splice(0, batchSize);
|
||||
const batchPromises = batch.map(({ lang, ns }) =>
|
||||
ensureTranslationsLoaded([ns], [lang], 0).catch(error => {
|
||||
logger.error(`Background loading failed for ${lang}:${ns}`, error);
|
||||
})
|
||||
);
|
||||
|
||||
await Promise.all(batchPromises);
|
||||
|
||||
// Add delay between batches to prevent blocking main thread
|
||||
if (backgroundLoadingQueue.length > 0) {
|
||||
await new Promise(resolve => setTimeout(resolve, 200)); // Increased delay
|
||||
}
|
||||
|
||||
// Break if we've been loading for too long (prevent infinite loops)
|
||||
if (performance.now() - performanceMetrics.totalLoadTime > 30000) { // 30 seconds max
|
||||
logger.error('Background translation loading taking too long, stopping');
|
||||
break;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
isBackgroundLoading = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Enhanced queueing with priority support
|
||||
const queueTranslations = (language: string, namespaces: string[], priority: number = 0) => {
|
||||
namespaces.forEach(ns => {
|
||||
const key = `${language}:${ns}`;
|
||||
if (!loadedTranslations.has(key)) {
|
||||
// Remove existing entry if it exists with lower priority
|
||||
const existingIndex = backgroundLoadingQueue.findIndex(item =>
|
||||
item.lang === language && item.ns === ns);
|
||||
if (existingIndex >= 0) {
|
||||
if (backgroundLoadingQueue[existingIndex].priority < priority) {
|
||||
backgroundLoadingQueue.splice(existingIndex, 1);
|
||||
} else {
|
||||
return; // Don't add duplicate with lower or equal priority
|
||||
}
|
||||
}
|
||||
|
||||
backgroundLoadingQueue.push({ lang: language, ns, priority });
|
||||
}
|
||||
});
|
||||
|
||||
// Start background loading with appropriate delay based on priority
|
||||
const delay = priority > 5 ? 1000 : priority > 2 ? 2000 : 3000;
|
||||
setTimeout(processBackgroundQueue, delay);
|
||||
};
|
||||
|
||||
// Initialize only essential translations for current language
|
||||
const initializeTranslations = async () => {
|
||||
try {
|
||||
const currentLang = i18n.language || 'en';
|
||||
|
||||
// Load only essential namespaces immediately
|
||||
await ensureTranslationsLoaded(ESSENTIAL_NAMESPACES, [currentLang], 10);
|
||||
|
||||
// Queue secondary translations with medium priority
|
||||
queueTranslations(currentLang, SECONDARY_NAMESPACES, 5);
|
||||
|
||||
// Queue tertiary translations with low priority
|
||||
queueTranslations(currentLang, TERTIARY_NAMESPACES, 1);
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
logger.error('Failed to initialize translations:', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// Enhanced language change handler with better prioritization
|
||||
export const changeLanguageOptimized = async (language: string) => {
|
||||
try {
|
||||
// Change language first
|
||||
await i18n.changeLanguage(language);
|
||||
|
||||
// Load essential namespaces immediately with high priority
|
||||
await ensureTranslationsLoaded(ESSENTIAL_NAMESPACES, [language], 10);
|
||||
|
||||
// Queue secondary translations with medium priority
|
||||
queueTranslations(language, SECONDARY_NAMESPACES, 5);
|
||||
|
||||
// Queue tertiary translations with low priority
|
||||
queueTranslations(language, TERTIARY_NAMESPACES, 1);
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
logger.error(`Failed to change language to ${language}:`, error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// Cache cleanup functionality
|
||||
const cleanupCache = () => {
|
||||
try {
|
||||
const keys = Object.keys(localStorage).filter(key =>
|
||||
key.startsWith('i18next_res_')
|
||||
);
|
||||
|
||||
if (keys.length > CACHE_CONFIG.MAX_CACHE_SIZE) {
|
||||
// Remove oldest entries
|
||||
const entriesToRemove = keys.slice(0, keys.length - CACHE_CONFIG.MAX_CACHE_SIZE);
|
||||
entriesToRemove.forEach(key => {
|
||||
try {
|
||||
localStorage.removeItem(key);
|
||||
} catch (error) {
|
||||
logger.error('Failed to remove cache entry:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Failed to cleanup translation cache:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Performance monitoring functions
|
||||
export const getPerformanceMetrics = () => ({
|
||||
...performanceMetrics,
|
||||
cacheEfficiency: performanceMetrics.cacheHits /
|
||||
(performanceMetrics.cacheHits + performanceMetrics.cacheMisses) * 100,
|
||||
averageLoadTime: performanceMetrics.totalLoadTime / performanceMetrics.translationsLoaded,
|
||||
});
|
||||
|
||||
export const resetPerformanceMetrics = () => {
|
||||
performanceMetrics.totalLoadTime = 0;
|
||||
performanceMetrics.translationsLoaded = 0;
|
||||
performanceMetrics.cacheHits = 0;
|
||||
performanceMetrics.cacheMisses = 0;
|
||||
};
|
||||
|
||||
// Utility function to preload translations for a specific page/component
|
||||
export const preloadPageTranslations = async (pageNamespaces: string[]) => {
|
||||
const currentLang = i18n.language || 'en';
|
||||
return ensureTranslationsLoaded(pageNamespaces, [currentLang], 8);
|
||||
};
|
||||
|
||||
// Set up periodic cache cleanup
|
||||
if (typeof window !== 'undefined') {
|
||||
setInterval(cleanupCache, CACHE_CONFIG.CLEANUP_INTERVAL);
|
||||
|
||||
// Cleanup on page unload
|
||||
window.addEventListener('beforeunload', cleanupCache);
|
||||
}
|
||||
|
||||
// Initialize translations on app startup
|
||||
initializeTranslations();
|
||||
|
||||
export default i18n;
|
||||
|
||||
@@ -39,7 +39,7 @@ import { resetState as resetEnhancedKanbanState } from '@/features/enhanced-kanb
|
||||
import { setProjectId as setInsightsProjectId } from '@/features/projects/insights/project-insights.slice';
|
||||
import { SuspenseFallback } from '@/components/suspense-fallback/suspense-fallback';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ensureTranslationsLoaded } from '@/i18n';
|
||||
|
||||
|
||||
// Import critical components synchronously to avoid suspense interruptions
|
||||
import TaskDrawer from '@components/task-drawer/task-drawer';
|
||||
@@ -96,21 +96,10 @@ const ProjectView = React.memo(() => {
|
||||
setTaskId(urlParams.taskId);
|
||||
}, [urlParams]);
|
||||
|
||||
// Ensure translations are loaded for project-view namespace
|
||||
// Remove translation preloading since we're using simple load-as-you-go approach
|
||||
useEffect(() => {
|
||||
const loadTranslations = async () => {
|
||||
try {
|
||||
await ensureTranslationsLoaded(['project-view'], [i18n.language]);
|
||||
updateTabLabels();
|
||||
setTranslationsReady(true);
|
||||
} catch (error) {
|
||||
console.error('Failed to load project-view translations:', error);
|
||||
// Set ready to true anyway to prevent infinite loading
|
||||
setTranslationsReady(true);
|
||||
}
|
||||
};
|
||||
|
||||
loadTranslations();
|
||||
updateTabLabels();
|
||||
setTranslationsReady(true);
|
||||
}, [i18n.language]);
|
||||
|
||||
// Update tab labels when language changes
|
||||
|
||||
@@ -25,6 +25,7 @@ export default defineConfig(({ command, mode }) => {
|
||||
{ find: '@shared', replacement: path.resolve(__dirname, './src/shared') },
|
||||
{ find: '@layouts', replacement: path.resolve(__dirname, './src/layouts') },
|
||||
{ find: '@services', replacement: path.resolve(__dirname, './src/services') },
|
||||
|
||||
],
|
||||
// **Ensure single React instance**
|
||||
dedupe: ['react', 'react-dom'],
|
||||
|
||||
Reference in New Issue
Block a user