chore: moved locale files

This commit is contained in:
chamikaJ
2025-07-11 09:37:50 +05:30
parent 94977f7255
commit 0e083868cb
414 changed files with 11185 additions and 674 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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'],