/** * Internationalization (i18n) system for LoRA Manager * Uses user-selected language from settings with fallback to English */ import { en } from './locales/en.js'; import { zhCN } from './locales/zh-CN.js'; import { zhTW } from './locales/zh-TW.js'; import { ru } from './locales/ru.js'; import { de } from './locales/de.js'; import { ja } from './locales/ja.js'; import { ko } from './locales/ko.js'; import { fr } from './locales/fr.js'; import { es } from './locales/es.js'; class I18nManager { constructor() { this.locales = { 'en': en, 'zh-CN': zhCN, 'zh-TW': zhTW, 'zh': zhCN, // Fallback for 'zh' to 'zh-CN' 'ru': ru, 'de': de, 'ja': ja, 'ko': ko, 'fr': fr, 'es': es }; this.currentLocale = this.getLanguageFromSettings(); this.translations = this.locales[this.currentLocale] || this.locales['en']; } /** * Get language from user settings with fallback to browser detection * @returns {string} Language code */ getLanguageFromSettings() { // 优先使用后端传递的初始语言 if (window.__INITIAL_LANGUAGE__ && this.locales[window.__INITIAL_LANGUAGE__]) { return window.__INITIAL_LANGUAGE__; } // Check localStorage for user-selected language const STORAGE_PREFIX = 'lora_manager_'; let userLanguage = null; try { const settings = localStorage.getItem(STORAGE_PREFIX + 'settings'); if (settings) { const parsedSettings = JSON.parse(settings); userLanguage = parsedSettings.language; } } catch (e) { console.warn('Failed to parse settings from localStorage:', e); } // If user has selected a language, use it if (userLanguage && this.locales[userLanguage]) { return userLanguage; } // Fallback to browser language detection for first-time users return this.detectLanguage(); } /** * Set the current language and save to settings * @param {string} languageCode - The language code to set * @returns {boolean} True if language was successfully set */ setLanguage(languageCode) { if (!this.locales[languageCode]) { console.warn(`Language '${languageCode}' is not supported`); return false; } this.currentLocale = languageCode; this.translations = this.locales[languageCode]; // Save to localStorage const STORAGE_PREFIX = 'lora_manager_'; try { const currentSettings = localStorage.getItem(STORAGE_PREFIX + 'settings'); let settings = {}; if (currentSettings) { settings = JSON.parse(currentSettings); } settings.language = languageCode; localStorage.setItem(STORAGE_PREFIX + 'settings', JSON.stringify(settings)); console.log(`Language changed to: ${languageCode}`); return true; } catch (e) { console.error('Failed to save language setting:', e); return false; } } /** * Get list of available languages with their native names * @returns {Array} Array of language objects */ getAvailableLanguages() { return [ { code: 'en', name: 'English', nativeName: 'English' }, { code: 'zh-CN', name: 'Chinese (Simplified)', nativeName: '简体中文' }, { code: 'zh-TW', name: 'Chinese (Traditional)', nativeName: '繁體中文' }, { code: 'ru', name: 'Russian', nativeName: 'Русский' }, { code: 'de', name: 'German', nativeName: 'Deutsch' }, { code: 'ja', name: 'Japanese', nativeName: '日本語' }, { code: 'ko', name: 'Korean', nativeName: '한국어' }, { code: 'fr', name: 'French', nativeName: 'Français' }, { code: 'es', name: 'Spanish', nativeName: 'Español' } ]; } /** * Detect browser language with fallback to English (for first-time users) * @returns {string} Language code */ detectLanguage() { // Get browser language const browserLang = navigator.language || navigator.languages[0] || 'en'; // Check if we have exact match if (this.locales[browserLang]) { return browserLang; } // Check for language without region (e.g., 'zh' from 'zh-CN') const langCode = browserLang.split('-')[0]; if (this.locales[langCode]) { return langCode; } // Fallback to English return 'en'; } /** * Get translation for a key with optional parameters * @param {string} key - Translation key (supports dot notation) * @param {Object} params - Parameters for string interpolation * @returns {string} Translated text */ t(key, params = {}) { const keys = key.split('.'); let value = this.translations; // Navigate through nested object for (const k of keys) { if (value && typeof value === 'object' && k in value) { value = value[k]; } else { // Fallback to English if key not found in current locale value = this.locales['en']; for (const fallbackKey of keys) { if (value && typeof value === 'object' && fallbackKey in value) { value = value[fallbackKey]; } else { console.warn(`Translation key not found: ${key}`); return key; // Return key as fallback } } break; } } if (typeof value !== 'string') { console.warn(`Translation key is not a string: ${key}`); return key; } // Replace parameters in the string return this.interpolate(value, params); } /** * Interpolate parameters into a string * Supports both {{param}} and {param} syntax * @param {string} str - String with placeholders * @param {Object} params - Parameters to interpolate * @returns {string} Interpolated string */ interpolate(str, params) { return str.replace(/\{\{?(\w+)\}?\}/g, (match, key) => { return params[key] !== undefined ? params[key] : match; }); } /** * Get current locale * @returns {string} Current locale code */ getCurrentLocale() { return this.currentLocale; } /** * Check if current locale is RTL (Right-to-Left) * @returns {boolean} True if RTL */ isRTL() { const rtlLocales = ['ar', 'he', 'fa', 'ur']; return rtlLocales.includes(this.currentLocale.split('-')[0]); } /** * Format number according to current locale * @param {number} number - Number to format * @param {Object} options - Intl.NumberFormat options * @returns {string} Formatted number */ formatNumber(number, options = {}) { return new Intl.NumberFormat(this.currentLocale, options).format(number); } /** * Format date according to current locale * @param {Date|string|number} date - Date to format * @param {Object} options - Intl.DateTimeFormat options * @returns {string} Formatted date */ formatDate(date, options = {}) { const dateObj = date instanceof Date ? date : new Date(date); return new Intl.DateTimeFormat(this.currentLocale, options).format(dateObj); } /** * Format file size with locale-specific formatting * @param {number} bytes - Size in bytes * @param {number} decimals - Number of decimal places * @returns {string} Formatted size */ formatFileSize(bytes, decimals = 2) { if (bytes === 0) return this.t('common.fileSize.zero'); const k = 1024; const dm = decimals < 0 ? 0 : decimals; const sizes = ['bytes', 'kb', 'mb', 'gb', 'tb']; const i = Math.floor(Math.log(bytes) / Math.log(k)); const size = parseFloat((bytes / Math.pow(k, i)).toFixed(dm)); return `${this.formatNumber(size)} ${this.t(`common.fileSize.${sizes[i]}`)}`; } /** * Initialize i18n from user settings instead of browser detection * This prevents language flashing on page load */ async initializeFromSettings() { const targetLanguage = this.getLanguageFromSettings(); // Set language immediately without animation/transition this.currentLocale = targetLanguage; this.translations = this.locales[targetLanguage] || this.locales['en']; // Dispatch event to notify that language has been initialized window.dispatchEvent(new CustomEvent('languageInitialized', { detail: { language: targetLanguage } })); } } // Create singleton instance export const i18n = new I18nManager(); // Export for global access (will be attached to window) export default i18n;