refactor(i18n): streamline i18n initialization and update translation methods

This commit is contained in:
Will Miao
2025-08-31 09:03:06 +08:00
parent 408a435b71
commit b2c4efab45
11 changed files with 196 additions and 282 deletions

View File

@@ -1,226 +1,118 @@
/**
* DOM utilities for i18n text replacement
* i18n utility functions for safe translation handling
*/
import { i18n } from '../i18n/index.js';
/**
* Replace text content in DOM elements with translations
* Uses data-i18n attribute to specify translation keys
*/
export function translateDOM() {
if (!window.i18n) return;
// Select all elements with data-i18n attributes, including optgroups and options
const elements = document.querySelectorAll('[data-i18n]');
elements.forEach(element => {
const key = element.getAttribute('data-i18n');
const target = element.getAttribute('data-i18n-target') || 'textContent';
if (key) {
const translation = window.i18n.t(key);
// Handle different target attributes
switch (target) {
case 'placeholder':
element.placeholder = translation;
break;
case 'title':
element.title = translation;
break;
case 'label':
// For optgroup elements
element.label = translation;
break;
case 'value':
element.value = translation;
break;
case 'textContent':
default:
element.textContent = translation;
break;
}
}
});
}
/**
* Update placeholder text based on current page
* @param {string} currentPath - Current page path
*/
export function updateSearchPlaceholder(currentPath) {
const searchInput = document.getElementById('searchInput');
if (!searchInput) return;
let placeholderKey = 'header.search.placeholder';
if (currentPath === '/loras') {
placeholderKey = 'header.search.placeholders.loras';
} else if (currentPath === '/loras/recipes') {
placeholderKey = 'header.search.placeholders.recipes';
} else if (currentPath === '/checkpoints') {
placeholderKey = 'header.search.placeholders.checkpoints';
} else if (currentPath === '/embeddings') {
placeholderKey = 'header.search.placeholders.embeddings';
}
searchInput.placeholder = i18n.t(placeholderKey);
}
/**
* Set text content for an element using i18n
* @param {Element|string} element - DOM element or selector
* @param {string} key - Translation key
* @param {Object} params - Translation parameters
*/
export function setTranslatedText(element, key, params = {}) {
const el = typeof element === 'string' ? document.querySelector(element) : element;
if (el) {
el.textContent = i18n.t(key, params);
}
}
/**
* Set attribute value for an element using i18n
* @param {Element|string} element - DOM element or selector
* @param {string} attribute - Attribute name
* @param {string} key - Translation key
* @param {Object} params - Translation parameters
*/
export function setTranslatedAttribute(element, attribute, key, params = {}) {
const el = typeof element === 'string' ? document.querySelector(element) : element;
if (el) {
el.setAttribute(attribute, i18n.t(key, params));
}
}
/**
* Create a translated element
* @param {string} tagName - HTML tag name
* @param {string} key - Translation key
* @param {Object} params - Translation parameters
* @param {Object} attributes - Additional attributes
* @returns {Element} Created element
*/
export function createTranslatedElement(tagName, key, params = {}, attributes = {}) {
const element = document.createElement(tagName);
element.textContent = i18n.t(key, params);
Object.entries(attributes).forEach(([attr, value]) => {
element.setAttribute(attr, value);
});
return element;
}
/**
* Update bulk selection count text
* @param {number} count - Number of selected items
*/
export function updateBulkSelectionCount(count) {
const selectedCountElement = document.getElementById('selectedCount');
if (selectedCountElement) {
const textNode = selectedCountElement.firstChild;
if (textNode && textNode.nodeType === Node.TEXT_NODE) {
textNode.textContent = i18n.t('loras.bulkOperations.selected', { count });
}
}
}
/**
* Format file size with localized units
* @param {number} bytes - Size in bytes
* @param {number} decimals - Number of decimal places
* @returns {string} Formatted size string
*/
export function formatFileSize(bytes, decimals = 2) {
return i18n.formatFileSize(bytes, decimals);
}
/**
* Format date with current locale
* @param {Date|string|number} date - Date to format
* @param {Object} options - Intl.DateTimeFormat options
* @returns {string} Formatted date string
*/
export function formatDate(date, options = {}) {
return i18n.formatDate(date, options);
}
/**
* Format number with current locale
* @param {number} number - Number to format
* @param {Object} options - Intl.NumberFormat options
* @returns {string} Formatted number string
*/
export function formatNumber(number, options = {}) {
return i18n.formatNumber(number, options);
}
/**
* Initialize i18n for the page
* This should be called after DOM content is loaded
*/
export function initializePageI18n() {
// Always use the client-side i18n with user settings
if (window.i18n) {
// Translate DOM elements
translateDOM();
// Update search placeholder based on current page
const currentPath = window.location.pathname;
updateSearchPlaceholder(currentPath);
// Set document direction for RTL languages
if (i18n.isRTL()) {
document.documentElement.setAttribute('dir', 'rtl');
document.body.classList.add('rtl');
} else {
document.documentElement.setAttribute('dir', 'ltr');
document.body.classList.remove('rtl');
}
}
}
/**
* Helper function to get translation directly
* Safe translation function that waits for i18n to be ready
* @param {string} key - Translation key
* @param {Object} params - Parameters for interpolation
* @returns {string} Translated text
* @param {string} fallback - Fallback text if translation fails
* @returns {Promise<string>} Translated text
*/
export function t(key, params = {}) {
return i18n.t(key, params);
export async function safeTranslate(key, params = {}, fallback = null) {
if (!window.i18n) {
console.warn('i18n not available');
return fallback || key;
}
// Wait for i18n to be ready
await window.i18n.waitForReady();
const translation = window.i18n.t(key, params);
// If translation returned the key (meaning not found), use fallback
if (translation === key && fallback) {
return fallback;
}
return translation;
}
/**
* Switch language and retranslate the page
* @param {string} languageCode - The language code to switch to
* @returns {boolean} True if language switch was successful
* Update element text with translation
* @param {HTMLElement|string} element - Element or selector
* @param {string} key - Translation key
* @param {Object} params - Parameters for interpolation
* @param {string} fallback - Fallback text
*/
export function switchLanguage(languageCode) {
if (i18n.setLanguage(languageCode)) {
// Retranslate the entire page
translateDOM();
// Update search placeholder based on current page
const currentPath = window.location.pathname;
updateSearchPlaceholder(currentPath);
// Set document direction for RTL languages
if (i18n.isRTL()) {
document.documentElement.setAttribute('dir', 'rtl');
document.body.classList.add('rtl');
} else {
document.documentElement.setAttribute('dir', 'ltr');
document.body.classList.remove('rtl');
}
// Dispatch a custom event for other components to react to language change
window.dispatchEvent(new CustomEvent('languageChanged', {
detail: { language: languageCode }
}));
return true;
}
return false;
export async function updateElementText(element, key, params = {}, fallback = null) {
const el = typeof element === 'string' ? document.querySelector(element) : element;
if (!el) return;
const text = await safeTranslate(key, params, fallback);
el.textContent = text;
}
/**
* Update element attribute with translation
* @param {HTMLElement|string} element - Element or selector
* @param {string} attribute - Attribute name (e.g., 'title', 'placeholder')
* @param {string} key - Translation key
* @param {Object} params - Parameters for interpolation
* @param {string} fallback - Fallback text
*/
export async function updateElementAttribute(element, attribute, key, params = {}, fallback = null) {
const el = typeof element === 'string' ? document.querySelector(element) : element;
if (!el) return;
const text = await safeTranslate(key, params, fallback);
el.setAttribute(attribute, text);
}
/**
* Create a reactive translation that updates when language changes
* @param {string} key - Translation key
* @param {Object} params - Parameters for interpolation
* @param {Function} callback - Callback function to call with translated text
*/
export function createReactiveTranslation(key, params = {}, callback) {
let currentLanguage = null;
const updateTranslation = async () => {
if (!window.i18n) return;
await window.i18n.waitForReady();
const newLanguage = window.i18n.getCurrentLocale();
// Only update if language changed or first time
if (newLanguage !== currentLanguage) {
currentLanguage = newLanguage;
const translation = window.i18n.t(key, params);
callback(translation);
}
};
// Initial update
updateTranslation();
// Listen for language changes
window.addEventListener('languageChanged', updateTranslation);
window.addEventListener('i18nReady', updateTranslation);
// Return cleanup function
return () => {
window.removeEventListener('languageChanged', updateTranslation);
window.removeEventListener('i18nReady', updateTranslation);
};
}
/**
* Batch update multiple elements with translations
* @param {Array} updates - Array of update configurations
* Each update should have: { element, key, type: 'text'|'attribute', attribute?, params?, fallback? }
*/
export async function batchUpdateTranslations(updates) {
if (!window.i18n) return;
await window.i18n.waitForReady();
for (const update of updates) {
const { element, key, type = 'text', attribute, params = {}, fallback } = update;
if (type === 'text') {
await updateElementText(element, key, params, fallback);
} else if (type === 'attribute' && attribute) {
await updateElementAttribute(element, attribute, key, params, fallback);
}
}
}