wifi-densepose/ui/i18n/index.js

239 lines
6.1 KiB
JavaScript

/**
* @file i18n 核心模块 - 提供国际化语言支持
* @description 轻量级国际化解决方案,支持多语言切换、参数插值、嵌套翻译
*/
const DEFAULT_LOCALE = 'en-US';
const STORAGE_KEY = 'wifi-densepose-locale';
class I18n {
constructor() {
this.currentLocale = DEFAULT_LOCALE;
this.translations = new Map();
this.fallbackLocale = DEFAULT_LOCALE;
this.observers = new Set();
}
/**
* 注册语言包
* @param {string} locale - 语言代码 (如 'zh-CN', 'en-US')
* @param {Object} messages - 翻译消息对象
*/
register(locale, messages) {
if (!locale || typeof locale !== 'string') {
console.error('[I18n] Invalid locale:', locale);
return;
}
this.translations.set(locale, messages);
console.log(`[I18n] Registered locale: ${locale}`);
}
/**
* 设置当前语言
* @param {string} locale - 语言代码
*/
setLocale(locale) {
if (!this.translations.has(locale)) {
console.warn(`[I18n] Locale "${locale}" not registered, using fallback`);
locale = this.fallbackLocale;
}
const oldLocale = this.currentLocale;
this.currentLocale = locale;
try {
localStorage.setItem(STORAGE_KEY, locale);
} catch (e) {
console.warn('[I18n] Failed to save locale to localStorage');
}
document.documentElement.lang = locale.split('-')[0];
this.observers.forEach(callback => {
try {
callback(locale, oldLocale);
} catch (e) {
console.error('[I18n] Observer callback error:', e);
}
});
console.log(`[I18n] Locale changed: ${oldLocale} -> ${locale}`);
}
/**
* 获取当前语言
* @returns {string} 当前语言代码
*/
getLocale() {
return this.currentLocale;
}
/**
* 获取所有已注册的语言列表
* @returns {string[]} 语言代码数组
*/
getAvailableLocales() {
return Array.from(this.translations.keys());
}
/**
* 初始化语言设置
* @returns {string} 检测到的语言代码
*/
init() {
let savedLocale = null;
try {
savedLocale = localStorage.getItem(STORAGE_KEY);
} catch (e) {
console.warn('[I18n] Failed to read locale from localStorage');
}
if (savedLocale && this.translations.has(savedLocale)) {
this.currentLocale = savedLocale;
} else {
const browserLocale = this.detectBrowserLocale();
this.currentLocale = browserLocale;
}
document.documentElement.lang = this.currentLocale.split('-')[0];
console.log(`[I18n] Initialized with locale: ${this.currentLocale}`);
return this.currentLocale;
}
/**
* 检测浏览器语言
* @returns {string} 检测到的语言代码
*/
detectBrowserLocale() {
const browserLang = navigator.language || navigator.userLanguage;
if (this.translations.has(browserLang)) {
return browserLang;
}
const baseLang = browserLang.split('-')[0];
for (const locale of this.translations.keys()) {
if (locale.startsWith(baseLang)) {
return locale;
}
}
return this.fallbackLocale;
}
/**
* 翻译文本
* @param {string} key - 翻译键,支持点分隔符嵌套 (如 'nav.dashboard')
* @param {Object} params - 插值参数
* @returns {string} 翻译后的文本
*/
t(key, params = {}) {
const messages = this.translations.get(this.currentLocale);
let text = this.getNestedValue(messages, key);
if (text === undefined) {
const fallbackMessages = this.translations.get(this.fallbackLocale);
text = this.getNestedValue(fallbackMessages, key);
}
if (text === undefined) {
console.warn(`[I18n] Missing translation for key: "${key}"`);
return key;
}
if (typeof text === 'string' && Object.keys(params).length > 0) {
text = this.interpolate(text, params);
}
return text;
}
/**
* 获取嵌套对象的值
* @param {Object} obj - 源对象
* @param {string} key - 点分隔的键路径
* @returns {*} 找到的值或 undefined
*/
getNestedValue(obj, key) {
if (!obj || typeof key !== 'string') return undefined;
const keys = key.split('.');
let value = obj;
for (const k of keys) {
if (value && typeof value === 'object' && k in value) {
value = value[k];
} else {
return undefined;
}
}
return value;
}
/**
* 插值替换参数
* @param {string} text - 包含 {param} 占位符的文本
* @param {Object} params - 参数对象
* @returns {string} 替换后的文本
*/
interpolate(text, params) {
return text.replace(/\{(\w+)\}/g, (match, key) => {
return params.hasOwnProperty(key) ? String(params[key]) : match;
});
}
/**
* 监听语言变化
* @param {Function} callback - 回调函数 (newLocale, oldLocale) => void
* @returns {Function} 取消监听函数
*/
onLocaleChange(callback) {
this.observers.add(callback);
return () => this.observers.delete(callback);
}
/**
* 更新页面中所有带有 data-i18n 属性的元素
*/
updateDOM() {
const elements = document.querySelectorAll('[data-i18n]');
elements.forEach(element => {
const key = element.getAttribute('data-i18n');
const attr = element.getAttribute('data-i18n-attr') || 'textContent';
const params = element.getAttribute('data-i18n-params');
let parsedParams = {};
if (params) {
try {
parsedParams = JSON.parse(params);
} catch (e) {
console.warn('[I18n] Failed to parse params:', params);
}
}
const text = this.t(key, parsedParams);
if (attr === 'textContent') {
element.textContent = text;
} else if (attr === 'innerHTML') {
element.innerHTML = text;
} else if (attr === 'placeholder') {
element.placeholder = text;
} else if (attr === 'title') {
element.title = text;
} else {
element.setAttribute(attr, text);
}
});
}
}
const i18n = new I18n();
export { i18n, I18n };
export default i18n;