add global config support
This commit is contained in:
parent
cd1e1a42f3
commit
34d553a890
23 changed files with 1682 additions and 343 deletions
109
frontend/js/locale.js
Normal file
109
frontend/js/locale.js
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
// 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(); // 刷新页面以应用所有翻译是最简单可靠的方式
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue