Files
worklenz/worklenz-frontend/src/i18n.ts
chamiakJ 94977f7255 feat(performance): enhance application performance with optimizations and monitoring
- Updated package dependencies for improved localization support and performance.
- Introduced CSS performance optimizations to prevent layout shifts and enhance rendering efficiency.
- Implemented asset preloading and lazy loading strategies for critical components to improve load times.
- Enhanced translation loading with optimized caching and background loading strategies.
- Added performance monitoring utilities to track key metrics and improve user experience.
- Refactored task management components to utilize new performance features and ensure efficient rendering.
- Introduced new utility functions for asset and CSS optimizations to streamline resource management.
2025-07-10 20:39:15 +05:30

380 lines
12 KiB
TypeScript

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
};
i18n
.use(LocalStorageBackend) // Cache translations to localStorage
.use(LanguageDetector) // Detect user language
.use(HttpApi) // Fetch translations if not in cache
.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
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
react: {
useSuspense: false, // Disable suspense for better control
bindI18n: 'languageChanged loaded', // Only bind necessary events
bindI18nStore: false, // Disable store binding for better performance
},
});
// 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;