- [中] 手动检查不再丢弃「已下载」事实:runCheck('manual') 原来无条件清
抑制记录,导致下载完成后点「检查更新」会把关于页从「重启安装」退回
「下载」(包已在磁盘)。改为保留 downloaded 记录,只清 announcement
记账;测试 §19 补 manual-check 保持 downloaded 的口径(153 断言)
- website 首帧脚本:?lang= 的 decodeURIComponent 包 try/catch(畸形转义
会在 IIFE 内抛 URIError,跳过 html[lang] 与防闪标记,指定语言被静默
忽略);zh-Hant-TW/zh-Hant-HK/zh-Hant 按 BCP-47 语义判为繁体
(normalize 与 i18n-apply.js 双处同步)
- ZmodemOffers 是 App 兄弟节点,语言切换时父级 children bailout 不重渲染:
订阅 language 并以 tFor 显式翻译(事件回调内的 t() 保留)
- ROADMAP 补录对抗性审查推迟项(周期检查可被设置写入推迟、启动开关
即时生效语义、下载无整体超时、--channel-only/--skip-gitea 矛盾)
131 lines
4.5 KiB
JavaScript
131 lines
4.5 KiB
JavaScript
/* OpenTerminal 官网语言层
|
||
选定语言的优先级(与 index.html 头部内联脚本完全一致):
|
||
1. URL 参数 ?lang=xx
|
||
2. localStorage['openterminal.lang']
|
||
3. navigator.language(s) 前缀匹配
|
||
4. 默认 zh-CN
|
||
index.html 头部内联脚本先按同一优先级把 <html lang> 定好(首帧防闪),
|
||
本文件直接读 document.documentElement.lang 作为唯一事实源,不重复判定。
|
||
|
||
应用规则:
|
||
- [data-i18n="k"] → textContent
|
||
- [data-i18n-html="k"] → innerHTML(字典是本地静态文件,非用户输入)
|
||
- [data-i18n-attr="alt:k;title:k2"] → 逐对写属性,分号分隔
|
||
缺键回落到 zh-CN;仍缺则保持 HTML 里的原文,不会把页面写空。
|
||
|
||
语言切换后派发 document 事件 'openterminal:langchange'(detail.lang),
|
||
页面尾部的内联脚本据此重刷主题按钮文案。 */
|
||
(function () {
|
||
'use strict';
|
||
|
||
var DICT = window.OPENTERMINAL_I18N || {};
|
||
var STORAGE_KEY = 'openterminal.lang';
|
||
var FALLBACK = 'zh-CN';
|
||
var SUPPORTED = ['zh-CN', 'zh-TW', 'en', 'ja'];
|
||
|
||
function normalize(tag) {
|
||
if (!tag) return null;
|
||
var t = String(tag).replace(/_/g, '-');
|
||
if (/^zh-?(tw|hk|mo)/i.test(t)) return 'zh-TW';
|
||
// zh-Hant-* is Traditional by definition (kept in sync with the head script).
|
||
if (/^zh-hant/i.test(t)) return 'zh-TW';
|
||
if (/^zh/i.test(t)) return 'zh-CN';
|
||
if (/^ja/i.test(t)) return 'ja';
|
||
if (/^en/i.test(t)) return 'en';
|
||
return null;
|
||
}
|
||
|
||
function tableOf(lang) {
|
||
return DICT[lang] || DICT[FALLBACK] || {};
|
||
}
|
||
|
||
function lookup(lang, key) {
|
||
var table = tableOf(lang);
|
||
if (Object.prototype.hasOwnProperty.call(table, key)) return table[key];
|
||
var fb = DICT[FALLBACK] || {};
|
||
if (Object.prototype.hasOwnProperty.call(fb, key)) return fb[key];
|
||
return null;
|
||
}
|
||
|
||
function applyAttrs(lang) {
|
||
var nodes = document.querySelectorAll('[data-i18n-attr]');
|
||
for (var i = 0; i < nodes.length; i++) {
|
||
var pairs = nodes[i].getAttribute('data-i18n-attr').split(';');
|
||
for (var j = 0; j < pairs.length; j++) {
|
||
var parts = pairs[j].split(':');
|
||
if (parts.length < 2) continue;
|
||
var attr = parts[0].trim();
|
||
var key = parts.slice(1).join(':').trim();
|
||
var val = lookup(lang, key);
|
||
if (attr && val !== null) nodes[i].setAttribute(attr, val);
|
||
}
|
||
}
|
||
}
|
||
|
||
function applyText(lang) {
|
||
var nodes = document.querySelectorAll('[data-i18n]');
|
||
for (var i = 0; i < nodes.length; i++) {
|
||
var val = lookup(lang, nodes[i].getAttribute('data-i18n'));
|
||
if (val !== null) nodes[i].textContent = val;
|
||
}
|
||
}
|
||
|
||
function applyHtml(lang) {
|
||
var nodes = document.querySelectorAll('[data-i18n-html]');
|
||
for (var i = 0; i < nodes.length; i++) {
|
||
var val = lookup(lang, nodes[i].getAttribute('data-i18n-html'));
|
||
if (val !== null) nodes[i].innerHTML = val;
|
||
}
|
||
}
|
||
|
||
function clearPending() {
|
||
document.documentElement.removeAttribute('data-i18n-pending');
|
||
}
|
||
|
||
function apply(lang, announce) {
|
||
var target = normalize(lang) || FALLBACK;
|
||
try {
|
||
document.documentElement.lang = target;
|
||
applyText(target);
|
||
applyHtml(target);
|
||
applyAttrs(target);
|
||
} finally {
|
||
// 任何异常都不能让 body 一直藏在 visibility:hidden 后面
|
||
clearPending();
|
||
}
|
||
var sel = document.getElementById('lang-select');
|
||
if (sel && sel.value !== target) sel.value = target;
|
||
if (announce) {
|
||
try {
|
||
document.dispatchEvent(new CustomEvent('openterminal:langchange', { detail: { lang: target } }));
|
||
} catch (e) {
|
||
var ev = document.createEvent('Event');
|
||
ev.initEvent('openterminal:langchange', true, true);
|
||
ev.detail = { lang: target };
|
||
document.dispatchEvent(ev);
|
||
}
|
||
}
|
||
return target;
|
||
}
|
||
|
||
// 头部内联脚本已经定好语言;这里只是把它落到 DOM 上
|
||
var current = apply(document.documentElement.lang || FALLBACK, false);
|
||
|
||
var select = document.getElementById('lang-select');
|
||
if (select) {
|
||
select.value = current;
|
||
select.addEventListener('change', function () {
|
||
var next = normalize(select.value) || FALLBACK;
|
||
try { localStorage.setItem(STORAGE_KEY, next); } catch (e) {}
|
||
current = apply(next, true);
|
||
});
|
||
}
|
||
|
||
window.OPENTERMINAL_SET_LANG = function (lang) {
|
||
current = apply(lang, true);
|
||
try { localStorage.setItem(STORAGE_KEY, current); } catch (e) {}
|
||
return current;
|
||
};
|
||
window.OPENTERMINAL_I18N_SUPPORTED = SUPPORTED.slice();
|
||
})();
|