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

124 lines
4.7 KiB
Python
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.
# -*- coding: utf-8 -*-
"""钉住「模板里的 t() 必须写在 ${…} 内部」。
⛔ 事故(2026-10-05,用户截图):
`t('可用 ') + ${mem.avail_mb} + ' MB 共 '`
浏览器把整个模板当**普通字符串**渲染 —— `${…}` 不再是插值,页面上
原样出现 `t('可用 ') + 3019 + ' MB 共 ' + 3921 + ' MB'`,
用户反馈「没看懂什么意思」。**node --check 抓不到**(语法完全合法),
47 视图的渲染回归也抓不到(它只验「抛不抛异常」)。
正解:t() 写在 ${…} 内部 ——
`${t('可用 ')}${mem.avail_mb}${t(' MB 共 ')}…`
判据:字面量定位**委托 Node 真词法**(_dev/scan-tokens.js,6860/6860
已验证),再对每个 tpl token 判断「t( 前面有没有未闭合的 ${」。
⛔ **绝不要自己写 JS 词法分析** —— 2026-10-05 试了 5 版
(3 版正则 + 2 版手写状态机),全部错:
· 正则版:字面量里含 \\n 转义或 ${ 时切错位,把后续代码卷进模板;
· 状态机版:漏了「${插值内含字符串}」→ 模板一直吞,597 处假红。
Node 那套是唯一可信的。
"""
import io
import json
import os
import re
import subprocess
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.dirname(HERE)
FAILS = []
N = 0
def chk(name, cond, detail=''):
global N
N += 1
if cond:
print(' ok ' + name)
else:
FAILS.append('%s %s' % (name, detail))
print(' FAIL ' + name + ' ' + detail)
def in_interp(seg, pos):
"""seg[pos] 处是 `t(`;它是否在 ${…} 内部。"""
d = 0
i = pos - 1
while i >= 0:
if seg[i] == '{' and i > 0 and seg[i - 1] == '$':
d += 1
i -= 2
continue
i -= 1
return d > 0
_FILES = ('app.js', 'realtime.js', 'upstream.js', 'netdetail.js', 'update.js')
# ⭐ 一次 node 调用扫全部(见 t-i18n-cover.py 的说明:逐个 fork 会拖爆全量回归)
# 用**相对路径**(Node 的输出键就是命令行里给的字符串,# 换绝对路径会导致取不到)
_paths = ['web/' + f for f in _FILES]
_all = subprocess.run(
['node', os.path.join(HERE, 'scan-tokens.js'), '--multi'] + _paths,
capture_output=True, check=True)
TOKS = json.loads(_all.stdout.decode('utf-8'))
for js in _FILES:
path = os.path.join(ROOT, 'web', js)
raw = open(path, 'rb').read()
src = raw.decode('utf-8')
chk('%s 无 CRLF' % js, b'\r\n' not in raw)
chk('%s 无 U+FFFD' % js, chr(0xFFFD) not in src)
toks = TOKS['web/' + js] # Node 的键是命令行里给的路径
# token 与源码必须逐条一致(偏移错位会让判据完全失真)
mism = sum(1 for t in toks
if raw[t['start']:t['end']].decode('utf-8', 'replace') != t['raw'])
chk('%s 字面量定位与源码一致' % js, mism == 0, '%d 条错位' % mism)
bad = []
for t in toks:
if t['kind'] != 'tpl':
continue
r = t['raw']
for m in re.finditer(r"t\('", r):
if not in_interp(r, m.start()):
bad.append('L%d %s' % (t['line'], r[:86].replace('\n', ' ')))
break
chk('%s 的 t() 都在 ${…} 内' % js, not bad, '%d 处:%s' % (len(bad), bad[:2]))
# ---- 变量遮蔽:形参里有 t 的函数,函数体内不得用 t( ----
# 🔴 2026-10-07:tmThemeCard(t, on) 的形参 t 遮蔽了全局翻译函数,
# 渲染时抛 `t is not a function`(check-render 抓到的第一个异常)。
# 那 6 个函数里已改用别名 T —— 这里钉住,别改回去。
import re as _re
_app = io.open(os.path.join(ROOT, 'web', 'app.js'), encoding='utf-8').read()
_lines = _app.split('\n')
_fn = _re.compile(r'^(?:async )?function (\w+)\(([^)]*)\)')
_starts = [(i + 1, m.group(1), m.group(2)) for i, l in enumerate(_lines)
if (m := _fn.match(l))]
_shadow = [(i, n) for i, n, ps in _starts
if 't' in [x.strip() for x in ps.split(',')]]
chk('app.js 有 T 别名(遮蔽时的安全出口)',
_re.search(r'^const T = window\.i18n', _app, _re.M) is not None)
chk('T 定义在使用之前(const 有 TDZ)',
_app.index('const T = window') < _app.index('${T('))
_leak = []
for idx, (start, name, ps) in enumerate(_starts):
if name not in [n for _i, n in _shadow]:
continue
end = _starts[idx + 1][0] - 1 if idx + 1 < len(_starts) else len(_lines)
seg = '\n'.join(_lines[start - 1:end])
if _re.search(r"\$\{t\('", seg):
_leak.append('%s(L%d)' % (name, start))
chk('形参为 t 的函数体内不用 t()(用 T)', not _leak, str(_leak))
print('\n==== t-tpl-t: %d 条判据, %d 失败 ====' % (N, len(FAILS)))
if FAILS:
for f in FAILS:
print(' - ' + f)
sys.exit(1)
print('TPL_T_OK')