Files
liuzhuohua b7ca9c89e8 feat(i18n/layout): 全站中英双语 + 四轮排版修复收官 + 修 boot_id 重启检测
v1.0.10 两件大事:

1. 界面中英双语(英文界面无中文残留)
   - 新增 web/i18n.js(约 4200 条词条),零依赖零构建
   - 后端下发中文数据走 bt4() 查表翻译;独立模块(upstream/netdetail/
     realtime/update)纳入翻译并注册切语言重绘钩子
   - 后端「状态/聚合」接口只回稳定枚举+原始数值,不下发中文文案
   - 顺带修一个真实 XSS:顶层 const 不挂 window,独立脚本 esc 静默退化成零转义

2. 修掉「文字竖着排/错位」(多分辨率反复暴露)
   - 单元格 min-content 只有 1 字宽 → word-break:keep-all + 表头 nowrap
   - 依赖卡片正文被标签/按钮挤成 27px → .dep-body{flex:1 1 200px} + flex-wrap
   - 裸 flex:1(下限为0)给正文 → 一律改 flex:1 1 <px>
   - .dep-item 横向容器直接塞块级子项 → 退回块级
   - 关键陷阱:auto-fill 布局屏越宽列数越多单卡越窄,窄屏反而测不出来,
     真机审计推进到「逐视图点按钮 + 1920/2560 宽视口 + 中英双语」
   - 守卫 t-layout.py 扩到 35 条,12 处反向验证全红

3. 修真缺陷:_link_since() 重启检测被误改成 if False:(注释还写着「已修」),
   uptime 归零后相减得负数 → 恢复比对 boot_id

附带:新增 25 个检查器(t-*.py)、排版审计工具链(shot-*/ocr-*/live-*)、
版本号兜底值同步到 1.0.10、.gitignore 收纳 i18n/排版排查的一次性脚本。
2026-10-10 04:50:32 +08:00

103 lines
3.9 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 端到端验证 i18n:eval 真 i18n.js,把 app.js 里**所有** t('中文') 调用
在两种语言下各跑一遍。
A 段:英文界面下不得出现汉字(漏词会回落中文 —— 用户直接能看见)
B 段:中文界面下返回值必须**逐字等于**代码里的原文
(换行形态别名命中时若返回词条的 zh,会多出/少掉反斜杠)
⛔ 为什么必须真跑:静态判据(正则查 key 是否在字典里)看不出
「换行形态不一致」这类问题 —— 2026-10-05 静态覆盖率显示 100%,
真跑却抓到 24 条英文下仍显示中文。
用法:node _dev/t-i18n-run2.js (在项目根目录下跑) */
'use strict';
const fs = require('fs');
const path = require('path');
const ROOT = path.resolve(__dirname, '..');
const IJ = path.join(ROOT, 'web', 'i18n.js');
const APP = path.join(ROOT, 'web', 'app.js');
/* 最小 DOM 桩:i18n.js 里的 applyDom / setLang 会用到。
⛔ 别用「什么方法都返回自己」的空对象 —— 那样
document.documentElement.lang = x 这种赋值会静默成功,
但 querySelectorAll 返回 undefined 时 forEach 直接抛,
错误信息会指向与被测点无关的地方。 */
const noop = () => 0;
const mkEl = () => ({
classList: { add: noop, remove: noop, contains: () => false },
style: {}, dataset: {},
appendChild: noop, setAttribute: noop, removeAttribute: noop,
querySelectorAll: () => [], querySelector: () => null,
addEventListener: noop, textContent: '', innerHTML: '',
});
global.window = { addEventListener: noop, i18n: null };
global.document = {
documentElement: {},
body: mkEl(), head: mkEl(),
querySelectorAll: () => [],
getElementById: () => null,
createElement: mkEl,
addEventListener: noop,
};
global.localStorage = { getItem: () => null, setItem: noop, removeItem: noop };
// Node 22 的 globalThis.navigator 是只读 getter(赋值会抛 TypeError),
// i18n.js 不用它,这里不设。
const src = fs.readFileSync(IJ, 'utf8');
// eslint-disable-next-line no-eval
eval(src);
const I = global.window.i18n;
if (!I) { console.error('FAIL: i18n.js 没有挂到 window.i18n'); process.exit(1); }
// 抽 app.js 里所有 t('…') 的字面量参数
const app = fs.readFileSync(APP, 'utf8');
const keys = new Set();
for (const m of app.matchAll(/t\('((?:[^'\\]|\\.)*)'/g)) {
if (/[\u4e00-\u9fff]/.test(m[1])) keys.add(m[1]);
}
const arr = [...keys];
let fails = 0;
function ck(name, cond, detail) {
if (cond) {
console.log(' ok ' + name);
} else {
fails++;
console.log(' FAIL ' + name + (detail ? ' ' + detail : ''));
}
}
console.log('[t-i18n-run2] t() 中文 key 共 ' + arr.length + ' 条');
const origWarn = console.warn;
const warnings = [];
console.warn = (m) => { warnings.push(String(m)); };
// A 段:英文界面
I.setLang('en-US', { force: true });
const badEn = [];
for (const k of arr) {
const v = I.t(k);
if (/[\u4e00-\u9fff]/.test(v)) badEn.push([k, v]);
}
// B 段:中文界面
I.setLang('zh-CN', { force: true });
const badZh = [];
for (const k of arr) {
const v = I.t(k);
if (v !== k) badZh.push([k, v]);
}
console.warn = origWarn;
ck('英文界面无中文残留', badEn.length === 0,
badEn.length + ' 条:' + badEn.slice(0, 3)
.map(([k, v]) => JSON.stringify(k.slice(0, 34)) + ' -> ' + JSON.stringify(v.slice(0, 34))).join(' | '));
ck('中文界面逐字等于代码原文', badZh.length === 0,
badZh.length + ' 条:' + badZh.slice(0, 3)
.map(([k, v]) => JSON.stringify(k.slice(0, 34)) + ' -> ' + JSON.stringify(v.slice(0, 34))).join(' | '));
ck('无「缺少词条」告警',
!warnings.some((w) => w.includes('缺少词条')),
warnings.filter((w) => w.includes('缺少词条')).slice(0, 2).join(' | '));
console.log('\n==== t-i18n-run2: 3 条判据, ' + fails + ' 失败 ====');
if (fails) process.exit(1);
console.log('I18N_RUN2_OK');