mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-25 07:05:43 -03:00
feat(i18n): Implement server-side internationalization support
- Added ServerI18nManager to handle translations and locale settings on the server. - Integrated server-side translations into templates, reducing language flashing on initial load. - Created API endpoints for setting and getting user language preferences. - Enhanced client-side i18n handling to work seamlessly with server-rendered content. - Updated various templates to utilize the new translation system. - Added mixed i18n handler to coordinate server and client translations, improving user experience. - Expanded translation files to include initialization messages for various components.
This commit is contained in:
@@ -42,6 +42,11 @@ class I18nManager {
|
||||
return window.__INITIAL_LANGUAGE__;
|
||||
}
|
||||
|
||||
// 检查服务端传递的翻译数据
|
||||
if (window.__SERVER_TRANSLATIONS__ && window.__SERVER_TRANSLATIONS__.language && this.locales[window.__SERVER_TRANSLATIONS__.language]) {
|
||||
return window.__SERVER_TRANSLATIONS__.language;
|
||||
}
|
||||
|
||||
// Check localStorage for user-selected language
|
||||
const STORAGE_PREFIX = 'lora_manager_';
|
||||
let userLanguage = null;
|
||||
|
||||
@@ -373,6 +373,26 @@ export const en = {
|
||||
initialization: {
|
||||
title: 'Initializing LoRA Manager',
|
||||
message: 'Scanning and building LoRA cache. This may take a few minutes...',
|
||||
loras: {
|
||||
title: 'Initializing LoRA Manager',
|
||||
message: 'Scanning and building LoRA cache. This may take a few minutes...'
|
||||
},
|
||||
checkpoints: {
|
||||
title: 'Initializing Checkpoint Manager',
|
||||
message: 'Scanning and building checkpoint cache. This may take a few minutes...'
|
||||
},
|
||||
embeddings: {
|
||||
title: 'Initializing Embedding Manager',
|
||||
message: 'Scanning and building embedding cache. This may take a few minutes...'
|
||||
},
|
||||
recipes: {
|
||||
title: 'Initializing Recipe Manager',
|
||||
message: 'Loading and processing recipes. This may take a few minutes...'
|
||||
},
|
||||
statistics: {
|
||||
title: 'Initializing Statistics',
|
||||
message: 'Processing model data for statistics. This may take a few minutes...'
|
||||
},
|
||||
steps: {
|
||||
scanning: 'Scanning model files...',
|
||||
processing: 'Processing metadata...',
|
||||
|
||||
@@ -373,6 +373,26 @@ export const zhCN = {
|
||||
initialization: {
|
||||
title: '初始化 LoRA 管理器',
|
||||
message: '正在扫描并构建 LoRA 缓存,这可能需要几分钟时间...',
|
||||
loras: {
|
||||
title: '初始化 LoRA 管理器',
|
||||
message: '正在扫描并构建 LoRA 缓存,这可能需要几分钟时间...'
|
||||
},
|
||||
checkpoints: {
|
||||
title: '初始化大模型管理器',
|
||||
message: '正在扫描并构建大模型缓存,这可能需要几分钟时间...'
|
||||
},
|
||||
embeddings: {
|
||||
title: '初始化 Embedding 管理器',
|
||||
message: '正在扫描并构建 Embedding 缓存,这可能需要几分钟时间...'
|
||||
},
|
||||
recipes: {
|
||||
title: '初始化配方管理器',
|
||||
message: '正在加载和处理配方,这可能需要几分钟时间...'
|
||||
},
|
||||
statistics: {
|
||||
title: '初始化统计信息',
|
||||
message: '正在处理模型数据以生成统计信息,这可能需要几分钟时间...'
|
||||
},
|
||||
steps: {
|
||||
scanning: '扫描模型文件...',
|
||||
processing: '处理元数据...',
|
||||
|
||||
@@ -169,8 +169,19 @@ export function formatNumber(number, options = {}) {
|
||||
* This should be called after DOM content is loaded
|
||||
*/
|
||||
export function initializePageI18n() {
|
||||
// Translate all elements with data-i18n attributes
|
||||
translateDOM();
|
||||
// 优先使用服务端传递的翻译数据,避免闪烁
|
||||
if (window.__SERVER_TRANSLATIONS__ && window.__SERVER_TRANSLATIONS__.language) {
|
||||
// 设置客户端i18n的语言为服务端传递的语言
|
||||
if (window.i18n && window.i18n.setLanguage) {
|
||||
window.i18n.setLanguage(window.__SERVER_TRANSLATIONS__.language);
|
||||
}
|
||||
|
||||
// 对于剩余的需要动态翻译的元素,仍使用客户端翻译
|
||||
translateDOM();
|
||||
} else {
|
||||
// 回退到完整的客户端翻译
|
||||
translateDOM();
|
||||
}
|
||||
|
||||
// Update search placeholder based on current page
|
||||
const currentPath = window.location.pathname;
|
||||
|
||||
212
static/js/utils/mixedI18n.js
Normal file
212
static/js/utils/mixedI18n.js
Normal file
@@ -0,0 +1,212 @@
|
||||
/**
|
||||
* Mixed i18n handler - coordinates server-side and client-side translations
|
||||
* Reduces language flashing by using server-rendered content initially
|
||||
*/
|
||||
|
||||
class MixedI18nHandler {
|
||||
constructor() {
|
||||
this.serverTranslations = window.__SERVER_TRANSLATIONS__ || {};
|
||||
this.currentLanguage = this.serverTranslations.language || 'en';
|
||||
this.initialized = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize mixed i18n system
|
||||
*/
|
||||
async initialize() {
|
||||
if (this.initialized) return;
|
||||
|
||||
// Import the main i18n module
|
||||
const { i18n } = await import('/loras_static/js/i18n/index.js');
|
||||
this.clientI18n = i18n;
|
||||
|
||||
// Ensure client i18n uses the same language as server
|
||||
if (this.currentLanguage && this.clientI18n.getCurrentLocale() !== this.currentLanguage) {
|
||||
this.clientI18n.setLanguage(this.currentLanguage);
|
||||
}
|
||||
|
||||
// Translate any remaining elements that need client-side translation
|
||||
this.translateRemainingElements();
|
||||
|
||||
this.initialized = true;
|
||||
|
||||
// Dispatch event to notify that mixed i18n is ready
|
||||
window.dispatchEvent(new CustomEvent('mixedI18nReady', {
|
||||
detail: { language: this.currentLanguage }
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Translate elements that still need client-side translation
|
||||
* (primarily dynamic content and complex components)
|
||||
*/
|
||||
translateRemainingElements() {
|
||||
if (!this.clientI18n) return;
|
||||
|
||||
// Find all elements with data-i18n attribute that haven't been server-rendered
|
||||
const elements = document.querySelectorAll('[data-i18n]');
|
||||
|
||||
elements.forEach(element => {
|
||||
// Skip if already translated by server (check if content matches key pattern)
|
||||
const key = element.getAttribute('data-i18n');
|
||||
const currentContent = element.textContent || element.value || element.placeholder;
|
||||
|
||||
// If the current content looks like a translation key, translate it
|
||||
if (currentContent === key || currentContent.includes('.') || currentContent === '') {
|
||||
this.translateElement(element, key);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Translate a single element using client-side i18n
|
||||
*/
|
||||
translateElement(element, key) {
|
||||
if (!this.clientI18n) return;
|
||||
|
||||
const params = element.getAttribute('data-i18n-params');
|
||||
let parsedParams = {};
|
||||
|
||||
if (params) {
|
||||
try {
|
||||
parsedParams = JSON.parse(params);
|
||||
} catch (e) {
|
||||
console.warn(`Invalid JSON in data-i18n-params for key ${key}:`, params);
|
||||
}
|
||||
}
|
||||
|
||||
// Get translated text
|
||||
const translatedText = this.clientI18n.t(key, parsedParams);
|
||||
|
||||
// Handle different translation targets
|
||||
const target = element.getAttribute('data-i18n-target') || 'textContent';
|
||||
|
||||
switch (target) {
|
||||
case 'placeholder':
|
||||
element.placeholder = translatedText;
|
||||
break;
|
||||
case 'title':
|
||||
element.title = translatedText;
|
||||
break;
|
||||
case 'alt':
|
||||
element.alt = translatedText;
|
||||
break;
|
||||
case 'innerHTML':
|
||||
element.innerHTML = translatedText;
|
||||
break;
|
||||
case 'textContent':
|
||||
default:
|
||||
element.textContent = translatedText;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch language (triggers page reload for server-side re-rendering)
|
||||
*/
|
||||
async switchLanguage(languageCode) {
|
||||
try {
|
||||
// Update server-side setting
|
||||
const response = await fetch('/api/set-language', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ language: languageCode })
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
// Reload page to get server-rendered content in new language
|
||||
window.location.reload();
|
||||
} else {
|
||||
const error = await response.json();
|
||||
console.error('Failed to set language:', error.error);
|
||||
|
||||
// Fallback to client-side only language change
|
||||
if (this.clientI18n) {
|
||||
this.clientI18n.setLanguage(languageCode);
|
||||
this.currentLanguage = languageCode;
|
||||
this.translateRemainingElements();
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error switching language:', error);
|
||||
|
||||
// Fallback to client-side only language change
|
||||
if (this.clientI18n) {
|
||||
this.clientI18n.setLanguage(languageCode);
|
||||
this.currentLanguage = languageCode;
|
||||
this.translateRemainingElements();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current language
|
||||
*/
|
||||
getCurrentLanguage() {
|
||||
return this.currentLanguage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get translation using client-side i18n (for dynamic content)
|
||||
*/
|
||||
t(key, params = {}) {
|
||||
if (this.clientI18n) {
|
||||
return this.clientI18n.t(key, params);
|
||||
}
|
||||
|
||||
// Fallback: check server translations
|
||||
if (this.serverTranslations.common && key.startsWith('common.')) {
|
||||
const subKey = key.substring(7); // Remove 'common.' prefix
|
||||
return this.serverTranslations.common[subKey] || key;
|
||||
}
|
||||
|
||||
return key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format file size using client-side i18n
|
||||
*/
|
||||
formatFileSize(bytes, decimals = 2) {
|
||||
if (this.clientI18n) {
|
||||
return this.clientI18n.formatFileSize(bytes, decimals);
|
||||
}
|
||||
|
||||
// Simple fallback
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
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));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
/**
|
||||
* Format date using client-side i18n
|
||||
*/
|
||||
formatDate(date, options = {}) {
|
||||
if (this.clientI18n) {
|
||||
return this.clientI18n.formatDate(date, options);
|
||||
}
|
||||
|
||||
// Simple fallback
|
||||
const dateObj = date instanceof Date ? date : new Date(date);
|
||||
return dateObj.toLocaleDateString();
|
||||
}
|
||||
}
|
||||
|
||||
// Create global instance
|
||||
window.mixedI18n = new MixedI18nHandler();
|
||||
|
||||
// Auto-initialize when DOM is ready
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
window.mixedI18n.initialize();
|
||||
});
|
||||
} else {
|
||||
window.mixedI18n.initialize();
|
||||
}
|
||||
|
||||
// Export for module usage
|
||||
export default window.mixedI18n;
|
||||
Reference in New Issue
Block a user