caddydash/frontend/js/locale.js
2025-06-30 15:31:14 +08:00

109 lines
No EOL
3.9 KiB
JavaScript

// js/locale.js - 国际化 (i18n) 核心模块
let currentLocale = {};
let currentLang = 'en'; // 默认语言
const supportedLangs = ['en', 'zh-CN']; // 应用支持的语言列表
/**
* 加载指定的语言文件 (JSON)
* @param {string} lang - 语言代码 (e.g., 'en', 'zh-CN')
*/
async function loadLocale(lang) {
try {
const response = await fetch(`/locales/${lang}.json?v=${Date.now()}`);
if (!response.ok) {
throw new Error(`Language file for ${lang} not found (status: ${response.status}).`);
}
currentLocale = await response.json();
currentLang = lang;
document.documentElement.lang = lang;
} catch (error) {
console.error("i18n Error:", error);
// 如果加载目标语言失败, 安全回退到默认的英语
if (lang !== 'en') {
console.warn(`Falling back to default language 'en'.`);
await loadLocale('en');
}
}
}
/**
* 将加载的翻译应用到所有带有 data-i18n 属性的DOM元素上
*/
function applyTranslationsToDOM() {
document.querySelectorAll('[data-i18n]').forEach(el => {
const key = el.dataset.i18n;
const translation = t(key);
if (translation !== key) {
// 优先替换元素的第一个文本节点, 避免覆盖内部的 <i> 等元素
const textNode = Array.from(el.childNodes).find(node => node.nodeType === Node.TEXT_NODE && node.textContent.trim());
if (textNode) {
// 在图标和文本之间保留一个空格
textNode.textContent = el.querySelector('i') ? ` ${translation}` : translation;
} else {
el.textContent = translation;
}
}
});
// 特殊处理 title 属性
document.querySelectorAll('[data-i18n-title]').forEach(el => {
const key = el.dataset.i18nTitle;
const translation = t(key);
if(translation !== key) el.title = translation;
});
}
/**
* 获取翻译文本, 支持点分隔的路径和占位符替换
* @param {string} key - 翻译键 (e.g., 'pages.login.welcome')
* @param {object} [replacements={}] - 用于替换占位符的键值对
* @returns {string} - 翻译后的字符串
*/
export function t(key, replacements = {}) {
// 通过路径 'a.b.c' 在嵌套对象中查找值: currentLocale['a']['b']['c']
const translation = key.split('.').reduce((obj, k) => obj && obj[k], currentLocale);
let result = translation || key; // 如果找不到, 返回原始key作为回退
// 处理占位符替换, e.g., {filename: 'example.com'}
if (typeof result === 'string') {
for (const placeholder in replacements) {
result = result.replace(`{${placeholder}}`, replacements[placeholder]);
}
}
return result;
}
/**
* 初始化 i18n 系统: 检测语言, 加载语言包, 并应用翻译
*/
export async function initI18n() {
const urlParams = new URLSearchParams(window.location.search);
const langFromUrl = urlParams.get('lang');
const langFromStorage = localStorage.getItem('appLanguage');
const browserLang = navigator.language.startsWith('zh') ? 'zh-CN' : 'en';
let langToLoad = 'en';
if (langFromUrl && supportedLangs.includes(langFromUrl)) {
langToLoad = langFromUrl;
} else if (langFromStorage && supportedLangs.includes(langFromStorage)) {
langToLoad = langFromStorage;
} else if (supportedLangs.includes(browserLang)) {
langToLoad = browserLang;
}
await loadLocale(langToLoad);
applyTranslationsToDOM();
}
/**
* 切换应用语言
* @param {string} lang - 目标语言代码
*/
export async function setLanguage(lang) {
if (supportedLangs.includes(lang) && lang !== currentLang) {
localStorage.setItem('appLanguage', lang);
window.location.reload(); // 刷新页面以应用所有翻译是最简单可靠的方式
}
}