File size: 6,611 Bytes
d646dd7
 
bb59211
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d646dd7
bb59211
d646dd7
 
 
 
 
bb59211
 
d646dd7
 
 
 
 
 
bb59211
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d646dd7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bb59211
 
 
 
d646dd7
 
 
 
 
 
 
 
bb59211
 
 
 
 
 
 
 
 
 
 
d646dd7
 
bb59211
 
 
 
d646dd7
bb59211
 
 
 
d646dd7
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
/* i18n — 中英文切换 */

// 词典**不在这里** —— 见同目录的 i18n-data.js。
// 它同时被服务端读取(app/i18n.py),而服务端是 Python;两边读同一份才不会分叉。
// 那个文件在 base.html 里**先于本文件**同步加载,所以这里读得到。
// 真读不到(比如有人单独引这个脚本)时退化成空字典 —— 页面照常起得来,
// t() 会返回 key 本身,一眼看得出是加载顺序出了问题。
const I18N = (typeof window !== 'undefined' && window.__I18N__) || { zh: {}, en: {} };

/**
 * 客户端语言**以服务端渲染时用的那个为准** —— 它就写在 `<html lang>` 上。
 *
 * ★ 不能只读 localStorage。服务端是从 **cookie** 读语言的,两者可能不一致
 *   (比如清了 localStorage 但 cookie 还在)。一旦不一致,下面的
 *   applyTranslations() 会把服务端刚渲染好的中文**又翻回英文** —— 页面反而
 *   变成了"先中文后英文",比原来的闪烁更难看。
 *
 * 以 <html lang> 为准还有个好处:它和 DOM 里已有的文案**必然一致**(服务端就是
 * 按它渲染的),所以首次 applyTranslations() 是个空操作。
 *
 * <head> 里的脚本执行时 <html> 已经解析完了,所以这里读得到。
 */
function _initialLang() {
    const fromHtml = String(document.documentElement.lang || '').toLowerCase();
    if (I18N[fromHtml]) return fromHtml;
    try {
        const saved = localStorage.getItem('lang');
        if (saved && I18N[saved]) return saved;
    } catch (e) { /* 隐私模式禁 storage */ }
    return 'en';
}

let currentLang = _initialLang();

function t(key) {
    return (I18N[currentLang] && I18N[currentLang][key]) || I18N['zh'][key] || key;
}

/** 把静态标注(data-i18n / data-i18n-placeholder)应用到 DOM。 */
function applyTranslations() {
    document.querySelectorAll('[data-i18n]').forEach(el => {
        el.textContent = t(el.dataset.i18n);
    });
    document.querySelectorAll('[data-i18n-placeholder]').forEach(el => {
        el.placeholder = t(el.dataset.i18nPlaceholder);
    });
    // title 属性此前没人处理 —— 只有 data-i18n(写 textContent)和 placeholder 两种,
    // 于是带提示的按钮/图标没法靠标记翻译,只能写死中文。
    document.querySelectorAll('[data-i18n-title]').forEach(el => {
        el.title = t(el.dataset.i18nTitle);
    });
}

function setLang(lang) {
    if (!I18N[lang]) return;                 // 只认字典里有的语言
    currentLang = lang;
    try { localStorage.setItem('lang', lang); } catch (e) { /* 隐私模式禁 storage */ }
    // ★ 同时写 cookie —— **服务端渲染时要读它**,否则每次整页导航都会先渲染
    //   默认语言、再由 JS 纠正,表现为"闪一下另一种语言"。
    //   localStorage 在这里已经不够用了:它是浏览器本地的,服务端看不到。
    //   max-age 一年;SameSite=Lax 免得外站发起的请求也带上这个 cookie。
    try {
        document.cookie = `lang=${encodeURIComponent(lang)}; path=/; max-age=31536000; SameSite=Lax`;
    } catch (e) { /* 禁 cookie:退回旧行为,靠上面的 applyTranslations() 事后翻 */ }
    // 通知**同页面里的其他应用**:工作流画布是独立的 React 应用,不加载本文件,
    // 它靠这个事件跟着切。不能用 storage 事件 —— 那个只在**别的标签页**触发,
    // 而切换按钮就在本页侧边栏上。
    try {
        window.dispatchEvent(new CustomEvent('egodata:lang', { detail: { lang } }));
    } catch (e) { /* 老浏览器不支持 CustomEvent 构造:画布不跟随,其余照常 */ }
    applyTranslations();
    // Update header nav
    const navReview = document.getElementById('nav-review');
    const navTrash = document.getElementById('nav-trash');
    if (navReview) navReview.textContent = t('video_review');
    if (navTrash) navTrash.innerHTML = '<iconify-icon icon="ant-design:delete-outlined" class="icon-sm"></iconify-icon> ' + t('trash');
    // Update trash page
    const trashTitle = document.getElementById('trash-title');
    const trashPurge = document.getElementById('trash-purge-btn');
    if (trashTitle) trashTitle.textContent = t('trash_title');
    if (trashPurge) trashPurge.textContent = t('purge_all');
    // Reload dynamic UI
    if (typeof loadTasks === 'function') loadTasks();
    if (typeof loadProjects === 'function') loadProjects();
    if (typeof loadDevices === 'function') loadDevices();
    if (typeof loadEpisodes === 'function') loadEpisodes();
    if (typeof loadTrashList === 'function') loadTrashList();
    if (typeof renderSourceBar === 'function') renderSourceBar();
    if (typeof updateInfoBar === 'function') updateInfoBar();
    // 首页概览的环形图/柱状图/状态徽标都是 JS 现画的,不重跑就不跟着切语言
    if (typeof refreshDashboard === 'function') refreshDashboard();
    // 用户列表同样是 JS 现画的(表头是静态标注,行内容是动态的)
    if (typeof loadUsers === 'function') loadUsers();
    updateLangToggle();
}

function updateLangToggle() {
    const btn = document.getElementById('lang-toggle');
    if (btn) {
        btn.textContent = currentLang === 'zh' ? 'EN' : '中';
    }
    // 用户菜单里的语言选项:高亮当前项。用的是 class 切换而不是重建 DOM ——
    // 菜单是常驻元素,重建会丢掉挂在它上面的监听。
    document.querySelectorAll('[data-lang-option]').forEach(el => {
        const active = el.dataset.langOption === currentLang;
        el.setAttribute('aria-pressed', active ? 'true' : 'false');
        el.classList.toggle('text-cyan-300', active);
        el.classList.toggle('border-cyan-700', active);
        el.classList.toggle('bg-cyan-900/30', active);
        el.classList.toggle('text-gray-400', !active);
        el.classList.toggle('border-gray-700', !active);
    });
}

// Apply translations on every page at load.
//
// ★ 必须**先读回上次选的语言**:setLang 一直在写 localStorage,但此前没有任何
//   地方读它 —— currentLang 写死 'en',于是每次刷新都退回英文,切换等于没切。
document.addEventListener('DOMContentLoaded', () => {
    // currentLang 在上面就用 _initialLang() 定好了(以 <html lang> 为准),
    // 这里**不再从 localStorage 覆盖** —— 那正是会把服务端渲染好的语言翻回去的操作。
    // 语言由服务端按 cookie 渲染,客户端只负责后续切换。
    applyTranslations();
    updateLangToggle();
});