mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-03-31 16:20:28 +02:00
231 lines
8.2 KiB
JavaScript
231 lines
8.2 KiB
JavaScript
// 前端国际化初始化(基于 i18next 浏览器版本)
|
||
(function () {
|
||
const DEFAULT_LANG = 'zh-CN';
|
||
const STORAGE_KEY = 'csai_lang';
|
||
const RESOURCES_PREFIX = '/static/i18n';
|
||
|
||
const loadedLangs = {};
|
||
|
||
// 供 bootstrap 等逻辑等待:避免 chat 在 t() 未就绪时用中文硬编码渲染,导致与语言标签不一致
|
||
let i18nReadyResolve;
|
||
window.i18nReady = new Promise(function (resolve) {
|
||
i18nReadyResolve = resolve;
|
||
});
|
||
|
||
function detectInitialLang() {
|
||
try {
|
||
const stored = localStorage.getItem(STORAGE_KEY);
|
||
if (stored) {
|
||
return stored;
|
||
}
|
||
} catch (e) {
|
||
console.warn('无法读取语言设置:', e);
|
||
}
|
||
|
||
const navLang = (navigator.language || navigator.userLanguage || '').toLowerCase();
|
||
if (navLang.startsWith('zh')) {
|
||
return 'zh-CN';
|
||
}
|
||
if (navLang.startsWith('en')) {
|
||
return 'en-US';
|
||
}
|
||
return DEFAULT_LANG;
|
||
}
|
||
|
||
async function loadLanguageResources(lang) {
|
||
if (loadedLangs[lang]) {
|
||
return;
|
||
}
|
||
try {
|
||
const resp = await fetch(RESOURCES_PREFIX + '/' + lang + '.json', {
|
||
cache: 'no-cache'
|
||
});
|
||
if (!resp.ok) {
|
||
console.warn('加载语言包失败:', lang, resp.status);
|
||
return;
|
||
}
|
||
const data = await resp.json();
|
||
if (typeof i18next !== 'undefined') {
|
||
i18next.addResourceBundle(lang, 'translation', data, true, true);
|
||
}
|
||
loadedLangs[lang] = true;
|
||
} catch (e) {
|
||
console.error('加载语言包异常:', lang, e);
|
||
}
|
||
}
|
||
|
||
function applyTranslations(root) {
|
||
if (typeof i18next === 'undefined') return;
|
||
const container = root || document;
|
||
if (!container) return;
|
||
|
||
const elements = container.querySelectorAll('[data-i18n]');
|
||
elements.forEach(function (el) {
|
||
const key = el.getAttribute('data-i18n');
|
||
if (!key) return;
|
||
const skipText = el.getAttribute('data-i18n-skip-text') === 'true';
|
||
const isFormControl = (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA');
|
||
const attrList = el.getAttribute('data-i18n-attr');
|
||
const text = i18next.t(key);
|
||
// 仅当元素无子元素(仅文本或空)时才替换文本,避免覆盖卡片内的数字、子节点等;input/textarea 永不设置 textContent
|
||
const hasNoElementChildren = !el.querySelector('*');
|
||
if (!skipText && !isFormControl && hasNoElementChildren && text && typeof text === 'string') {
|
||
el.textContent = text;
|
||
}
|
||
|
||
if (attrList) {
|
||
const titleKey = el.getAttribute('data-i18n-title');
|
||
attrList.split(',').map(function (s) { return s.trim(); }).forEach(function (attr) {
|
||
if (!attr) return;
|
||
var val = text;
|
||
if (attr === 'title' && titleKey) {
|
||
var titleText = i18next.t(titleKey);
|
||
if (titleText && typeof titleText === 'string') val = titleText;
|
||
}
|
||
if (val && typeof val === 'string') {
|
||
el.setAttribute(attr, val);
|
||
}
|
||
});
|
||
}
|
||
});
|
||
|
||
// 对话输入框:若 value 与 placeholder 相同,清空 value 以便正确显示占位提示
|
||
try {
|
||
const chatInput = document.getElementById('chat-input');
|
||
if (chatInput && chatInput.tagName === 'TEXTAREA') {
|
||
const ph = (chatInput.getAttribute('placeholder') || '').trim();
|
||
if (ph && chatInput.value.trim() === ph) {
|
||
chatInput.value = '';
|
||
}
|
||
}
|
||
} catch (e) { /* ignore */ }
|
||
|
||
// 更新 html lang 属性
|
||
try {
|
||
if (document && document.documentElement) {
|
||
document.documentElement.lang = i18next.language || DEFAULT_LANG;
|
||
}
|
||
} catch (e) {
|
||
// ignore
|
||
}
|
||
}
|
||
|
||
function updateLangLabel() {
|
||
const label = document.getElementById('current-lang-label');
|
||
if (!label || typeof i18next === 'undefined') return;
|
||
const lang = (i18next.language || DEFAULT_LANG).toLowerCase();
|
||
if (lang.indexOf('zh') === 0) {
|
||
label.textContent = i18next.t('lang.zhCN');
|
||
} else {
|
||
label.textContent = i18next.t('lang.enUS');
|
||
}
|
||
}
|
||
|
||
function closeLangDropdown() {
|
||
const dropdown = document.getElementById('lang-dropdown');
|
||
if (dropdown) {
|
||
dropdown.style.display = 'none';
|
||
}
|
||
}
|
||
|
||
function handleGlobalClickForLangDropdown(ev) {
|
||
const dropdown = document.getElementById('lang-dropdown');
|
||
const btn = document.querySelector('.lang-switcher-btn');
|
||
if (!dropdown || dropdown.style.display !== 'block') return;
|
||
const target = ev.target;
|
||
if (btn && btn.contains(target)) {
|
||
return;
|
||
}
|
||
if (!dropdown.contains(target)) {
|
||
closeLangDropdown();
|
||
}
|
||
}
|
||
|
||
async function changeLanguage(lang) {
|
||
if (typeof i18next === 'undefined') return;
|
||
const current = i18next.language || DEFAULT_LANG;
|
||
if (lang === current) return;
|
||
await loadLanguageResources(lang);
|
||
await i18next.changeLanguage(lang);
|
||
try {
|
||
localStorage.setItem(STORAGE_KEY, lang);
|
||
} catch (e) {
|
||
console.warn('无法保存语言设置:', e);
|
||
}
|
||
applyTranslations(document);
|
||
updateLangLabel();
|
||
try {
|
||
window.__locale = lang;
|
||
} catch (e) { /* ignore */ }
|
||
try {
|
||
document.dispatchEvent(new CustomEvent('languagechange', { detail: { lang: lang } }));
|
||
} catch (e) { /* ignore */ }
|
||
}
|
||
|
||
async function initI18n() {
|
||
if (typeof i18next === 'undefined') {
|
||
console.warn('i18next 未加载,跳过前端国际化初始化');
|
||
if (typeof i18nReadyResolve === 'function') i18nReadyResolve();
|
||
return;
|
||
}
|
||
|
||
const initialLang = detectInitialLang();
|
||
await i18next.init({
|
||
lng: initialLang,
|
||
fallbackLng: DEFAULT_LANG,
|
||
debug: false,
|
||
resources: {}
|
||
});
|
||
|
||
await loadLanguageResources(initialLang);
|
||
applyTranslations(document);
|
||
updateLangLabel();
|
||
try {
|
||
window.__locale = i18next.language || initialLang;
|
||
} catch (e) { /* ignore */ }
|
||
|
||
// 导出全局函数供其他脚本调用(支持插值参数,如 _t('key', { count: 2 }))
|
||
window.t = function (key, opts) {
|
||
if (typeof i18next === 'undefined') return key;
|
||
return i18next.t(key, opts);
|
||
};
|
||
window.changeLanguage = changeLanguage;
|
||
window.applyTranslations = applyTranslations;
|
||
|
||
// 语言切换下拉支持
|
||
window.toggleLangDropdown = function () {
|
||
const dropdown = document.getElementById('lang-dropdown');
|
||
if (!dropdown) return;
|
||
if (dropdown.style.display === 'block') {
|
||
dropdown.style.display = 'none';
|
||
} else {
|
||
dropdown.style.display = 'block';
|
||
}
|
||
};
|
||
window.onLanguageSelect = function (lang) {
|
||
changeLanguage(lang);
|
||
closeLangDropdown();
|
||
};
|
||
|
||
document.addEventListener('click', handleGlobalClickForLangDropdown);
|
||
|
||
// 若 chat 已在 i18n 完成前用后备中文渲染了系统就绪消息,这里按当前语言纠正一次
|
||
try {
|
||
if (typeof refreshSystemReadyMessageBubbles === 'function') {
|
||
refreshSystemReadyMessageBubbles();
|
||
}
|
||
} catch (e) { /* ignore */ }
|
||
|
||
if (typeof i18nReadyResolve === 'function') i18nReadyResolve();
|
||
}
|
||
|
||
document.addEventListener('DOMContentLoaded', function () {
|
||
// i18n 初始化在 DOM Ready 后执行
|
||
initI18n().catch(function (e) {
|
||
console.error('初始化国际化失败:', e);
|
||
if (typeof i18nReadyResolve === 'function') i18nReadyResolve();
|
||
});
|
||
});
|
||
})();
|
||
|