Files
Warracker/frontend/js/i18n.js
sassanix 23028fe696 Removed vite
Removed vite, and consolidated files to frontend folder
2025-11-13 09:22:59 -04:00

470 lines
17 KiB
JavaScript

/**
* i18next Configuration and Initialization
* Handles frontend localization with offline support
*/
// Global i18n object to make translation functions available
window.i18n = {};
// Supported languages
const SUPPORTED_LANGUAGES = ['en', 'fr', 'es', 'de', 'it', 'cs', 'nl', 'hi', 'fa', 'ar', 'ru', 'uk', 'zh_CN', 'zh_HK', 'ja', 'pt', 'ko', 'tr', 'pl', 'he'];
const DEFAULT_LANGUAGE = 'en';
const RTL_LANGUAGES = ['ar', 'fa', 'he']; // Arabic, Persian, and Hebrew are RTL languages
/**
* Initialize i18next with configuration
*/
async function initializeI18n() {
console.log('Initializing i18next...');
// Debug: Check if required libraries are loaded
if (typeof i18next === 'undefined') {
console.error('i18next library not loaded');
return;
}
console.log('i18next library loaded successfully');
// Check optional libraries
const hasHttpBackend = typeof i18nextHttpBackend !== 'undefined';
const hasLanguageDetector = typeof i18nextBrowserLanguageDetector !== 'undefined';
console.log('Optional libraries:', {
httpBackend: hasHttpBackend,
languageDetector: hasLanguageDetector
});
try {
// Get current language from various sources
const savedLanguage = getCurrentLanguage();
console.log('Initial language detected:', savedLanguage);
// Load translations manually since HttpBackend might not be available
const translations = await loadTranslations(savedLanguage);
// Configure i18next with or without plugins
let i18nextInstance = i18next;
if (hasLanguageDetector) {
i18nextInstance = i18nextInstance.use(i18nextBrowserLanguageDetector);
}
await i18nextInstance.init({
fallbackLng: DEFAULT_LANGUAGE,
supportedLngs: SUPPORTED_LANGUAGES,
debug: false,
// Set initial language and resources
lng: savedLanguage,
resources: translations,
// Language detection configuration (only if detector is available)
...(hasLanguageDetector && {
detection: {
order: ['cookie', 'localStorage', 'navigator', 'htmlTag'],
lookupCookie: 'lang',
lookupLocalStorage: 'preferred_language',
caches: ['cookie', 'localStorage'],
cookieOptions: {
path: '/',
maxAge: 365 * 24 * 60 * 60 // 1 year
}
}
}),
// Interpolation options
interpolation: {
escapeValue: false
}
});
// Store translation function globally
window.i18n.t = i18next.t.bind(i18next);
window.i18n.changeLanguage = changeLanguage;
window.i18n.getCurrentLanguage = getCurrentLanguage;
window.i18n.getSupportedLanguages = () => SUPPORTED_LANGUAGES;
// Initialize page translations
translatePage();
// Set initial page direction
updatePageDirection(i18next.language);
// Set up language change listener
i18next.on('languageChanged', (lng) => {
console.log('Language changed to:', lng);
translatePage();
updateLanguageAttribute(lng);
updatePageDirection(lng);
});
console.log('i18next initialized successfully with language:', i18next.language);
// Dispatch event to notify other scripts that i18n is ready
window.dispatchEvent(new CustomEvent('i18nReady', {
detail: {
language: i18next.language,
t: window.i18n.t
}
}));
} catch (error) {
console.error('Failed to initialize i18next:', error);
// Fallback to default language without i18next
window.i18n.t = (key) => key; // Return key as fallback
window.i18n.changeLanguage = () => {};
window.i18n.getCurrentLanguage = () => DEFAULT_LANGUAGE;
window.i18n.getSupportedLanguages = () => SUPPORTED_LANGUAGES;
// Still dispatch event so other scripts know i18n is "ready" (even with fallback)
window.dispatchEvent(new CustomEvent('i18nReady', {
detail: {
language: DEFAULT_LANGUAGE,
t: window.i18n.t,
fallback: true
}
}));
}
}
/**
* Load translations for a specific language
*/
async function loadTranslations(language) {
console.log(`loadTranslations called for language: ${language}`);
const translations = {};
try {
// Load primary language
const url = `/locales/${language}/translation.json?v=${Date.now()}`;
console.log(`Fetching translations from: ${url}`);
const response = await fetch(url);
console.log(`Fetch response status: ${response.status} for language: ${language}`);
if (response.ok) {
const data = await response.json();
translations[language] = { translation: data };
const keyCount = Object.keys(data).length;
console.log(`Successfully loaded ${keyCount} translation keys for ${language}:`, Object.keys(data).slice(0, 5));
} else {
console.warn(`Failed to load translations for ${language}, status: ${response.status}`);
console.warn(`Response text:`, await response.text().catch(() => 'Could not read response text'));
}
} catch (error) {
console.error(`Error loading translations for ${language}:`, error);
}
// Always load fallback language if different
if (language !== DEFAULT_LANGUAGE) {
try {
const fallbackUrl = `/locales/${DEFAULT_LANGUAGE}/translation.json?v=${Date.now()}`;
console.log(`Loading fallback language from: ${fallbackUrl}`);
const fallbackResponse = await fetch(fallbackUrl);
console.log(`Fallback fetch response status: ${fallbackResponse.status}`);
if (fallbackResponse.ok) {
const fallbackData = await fallbackResponse.json();
translations[DEFAULT_LANGUAGE] = { translation: fallbackData };
const fallbackKeyCount = Object.keys(fallbackData).length;
console.log(`Loaded ${fallbackKeyCount} fallback translation keys for ${DEFAULT_LANGUAGE}`);
}
} catch (error) {
console.error(`Error loading fallback translations:`, error);
}
}
console.log(`loadTranslations completed. Languages loaded: ${Object.keys(translations)}`);
return translations;
}
/**
* Get current language from storage or user preferences
*/
function getCurrentLanguage() {
// URL override (e.g., ?lang=tr)
try {
const params = new URLSearchParams(window.location.search);
const urlLang = params.get('lang');
if (urlLang && SUPPORTED_LANGUAGES.includes(urlLang)) {
setCookie('lang', urlLang, 365);
localStorage.setItem('preferred_language', urlLang);
return urlLang;
}
} catch (e) {
// Ignore URL parsing errors and continue
}
// Check cookie first
const cookieLang = getCookie('lang');
if (cookieLang && SUPPORTED_LANGUAGES.includes(cookieLang)) {
return cookieLang;
}
// Check localStorage
const storedLang = localStorage.getItem('preferred_language');
if (storedLang && SUPPORTED_LANGUAGES.includes(storedLang)) {
return storedLang;
}
// Check user preferences from API if authenticated
const userInfo = JSON.parse(localStorage.getItem('user_info') || '{}');
if (userInfo.preferred_language && SUPPORTED_LANGUAGES.includes(userInfo.preferred_language)) {
return userInfo.preferred_language;
}
// Fallback to browser language or default
const browserLang = navigator.language?.split('-')[0];
return SUPPORTED_LANGUAGES.includes(browserLang) ? browserLang : DEFAULT_LANGUAGE;
}
/**
* Change language and persist preference
*/
async function changeLanguage(language) {
console.log('changeLanguage called with:', language);
if (!SUPPORTED_LANGUAGES.includes(language)) {
console.warn('Unsupported language:', language);
return;
}
try {
console.log('Loading translations for language:', language);
// Load translations for the new language
const translations = await loadTranslations(language);
console.log('Translations loaded:', Object.keys(translations));
// Add new language resources to i18next
for (const [lng, resources] of Object.entries(translations)) {
console.log('Adding resource bundle for language:', lng);
i18next.addResourceBundle(lng, 'translation', resources.translation, true, true);
}
// Change language in i18next
console.log('Changing i18next language to:', language);
await i18next.changeLanguage(language);
console.log('i18next language changed successfully to:', i18next.language);
// Persist to cookie and localStorage
setCookie('lang', language, 365);
localStorage.setItem('preferred_language', language);
console.log('Language preference saved to cookie and localStorage:', language);
// Update user preference in backend if authenticated
if (localStorage.getItem('auth_token')) {
try {
await saveLanguagePreference(language);
console.log('Language preference saved to backend');
} catch (error) {
console.warn('Failed to save language preference to backend:', error);
}
}
// Dispatch event for other components
window.dispatchEvent(new CustomEvent('languageChanged', { detail: { language } }));
console.log('languageChanged event dispatched for:', language);
} catch (error) {
console.error('Failed to change language:', error);
throw error; // Re-throw to let calling code handle it
}
}
/**
* Save language preference to backend
*/
async function saveLanguagePreference(language) {
const token = localStorage.getItem('auth_token');
if (!token) return;
const response = await fetch('/api/auth/user/language-preference', {
method: 'PUT',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ preferred_language: language })
});
if (!response.ok) {
throw new Error('Failed to save language preference');
}
}
/**
* Translate all elements on the current page
*/
function translatePage() {
if (!window.i18n.t) {
console.warn('Translation function not available, skipping translation');
return;
}
console.log('Starting page translation...');
let translatedCount = 0;
// Update dynamic titles that are set by JavaScript
updateDynamicTitles();
// Translate elements with data-i18n attribute
document.querySelectorAll('[data-i18n]').forEach(element => {
const raw = element.getAttribute('data-i18n');
let handled = false;
// Support syntax like [placeholder]warranties.search_placeholder
const attrMatch = raw && raw.match(/^\s*\[(\w+)\]\s*(.+)$/);
if (attrMatch) {
const attrName = attrMatch[1];
const key = attrMatch[2];
const translation = window.i18n.t(key);
if (translation && translation !== key) {
element.setAttribute(attrName, translation);
translatedCount++;
handled = true;
}
}
if (!handled) {
const key = raw;
const translation = window.i18n.t(key);
if (translation && translation !== key) {
if (element.tagName === 'INPUT' && (element.type === 'text' || element.type === 'email' || element.type === 'password')) {
element.placeholder = translation;
} else if (element.hasAttribute('title')) {
element.title = translation;
} else {
element.textContent = translation;
}
translatedCount++;
} else {
console.warn(`No translation found for key: ${key}`);
}
}
});
// Translate elements with data-i18n-html attribute (allows HTML content)
document.querySelectorAll('[data-i18n-html]').forEach(element => {
const key = element.getAttribute('data-i18n-html');
const translation = window.i18n.t(key);
element.innerHTML = translation;
});
// Translate elements with data-i18n-title attribute
document.querySelectorAll('[data-i18n-title]').forEach(element => {
const key = element.getAttribute('data-i18n-title');
const translation = window.i18n.t(key);
element.title = translation;
});
// Translate elements with data-i18n-placeholder attribute
document.querySelectorAll('[data-i18n-placeholder]').forEach(element => {
const key = element.getAttribute('data-i18n-placeholder');
const translation = window.i18n.t(key);
if (translation && translation !== key) {
element.placeholder = translation;
translatedCount++;
}
});
console.log(`Page translation completed. Translated ${translatedCount} elements.`);
// Re-process warranties to update status text and labels if warranties exist
if (typeof warranties !== 'undefined' && warranties && warranties.length > 0) {
console.log(`[i18n.js] Re-processing ${warranties.length} warranties after language change`);
// Re-process each warranty to update status text with new language
warranties = warranties.map(warranty => {
if (typeof processWarrantyData === 'function') {
return processWarrantyData(warranty);
}
return warranty;
});
// Re-render warranties with new translations
if (typeof applyFilters === 'function') {
applyFilters();
}
}
}
/**
* Update dynamic titles that are set by JavaScript
*/
function updateDynamicTitles() {
// Update warranties panel title on index.html
if (typeof updateWarrantiesPanelTitle === 'function' && typeof isGlobalView !== 'undefined') {
updateWarrantiesPanelTitle(isGlobalView);
}
// Update dashboard title on status.html
if (typeof updateDashboardTitle === 'function') {
updateDashboardTitle();
}
// Re-process warranty cards if they exist (for dynamic status text)
if (typeof processAllWarranties === 'function') {
processAllWarranties();
}
}
/**
* Update HTML lang attribute
*/
function updateLanguageAttribute(language) {
document.documentElement.lang = language;
}
/**
* Update page direction for RTL languages
*/
function updatePageDirection(language) {
const isRTL = RTL_LANGUAGES.includes(language);
const htmlElement = document.documentElement;
const bodyElement = document.body;
if (isRTL) {
htmlElement.dir = 'rtl';
htmlElement.setAttribute('data-rtl', 'true');
bodyElement.classList.add('rtl');
console.log(`Applied RTL direction for language: ${language}`);
} else {
htmlElement.dir = 'ltr';
htmlElement.removeAttribute('data-rtl');
bodyElement.classList.remove('rtl');
console.log(`Applied LTR direction for language: ${language}`);
}
}
/**
* Utility function to get cookie value
*/
function getCookie(name) {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop().split(';').shift();
return null;
}
/**
* Utility function to set cookie
*/
function setCookie(name, value, days) {
const expires = new Date();
expires.setTime(expires.getTime() + (days * 24 * 60 * 60 * 1000));
document.cookie = `${name}=${value};expires=${expires.toUTCString()};path=/`;
}
/**
* Add translation helper for dynamic content
*/
function t(key, options = {}) {
return window.i18n.t ? window.i18n.t(key, options) : key;
}
// Expose translation function globally
window.t = t;
// Initialize when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initializeI18n);
} else {
initializeI18n();
}