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

154 lines
5.0 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.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""钉住「翻译调用统一用别名 T()」这件事,防止回退成裸 t()。
背景(2026-10-07):
web/app.js 里 `t` 在几十处被当作**局部变量**用:
const t = tmCur() // 主题
const t = $('#toast') // 提示条
rows.map(t => ...) // 表格行
function tmThemeCard(t, on)
const t = await api(...)
在这些作用域里写 t('中文') 会直接抛 "t is not a function",
页面表现为「切到英文后整个视图白屏 / 只剩半个页面」。
根治办法:app.js 里所有翻译调用改写成别名 T('…'),
并且把仅有的两个局部 T 绑定(tmRender / tmRenderPane)改名成 TH,
让 T 在 app.js 里**只有一个含义**。
本判据就是守住这个不变量 —— 一旦有人新写了 t('…') 或新增了局部 T,
这里立刻变红。
"""
import io
import os
import re
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.dirname(HERE)
fails = []
n = 0
def ck(desc, cond, extra=''):
global n
n += 1
if cond:
print('ok %s' % desc)
else:
print('FAIL %s %s' % (desc, extra))
fails.append(desc)
def read(*parts):
with io.open(os.path.join(ROOT, *parts), encoding='utf-8') as f:
return f.read()
def strip_comments(src):
"""把注释换成等长空白 —— 行号不变,注释里的例子不会误判。"""
out = list(src)
i, L = 0, len(src)
st = None # None / 'line' / 'block' / 'sq' / 'dq' / 'tpl'
while i < L:
c = src[i]
if st is None:
if c == '/' and i + 1 < L and src[i + 1] == '/':
st = 'line'
elif c == '/' and i + 1 < L and src[i + 1] == '*':
st = 'block'
elif c in "'\"":
st = 'sq' if c == "'" else 'dq'
i += 1
continue
elif c == '`':
st = 'tpl'
i += 1
continue
elif st == 'line':
if c == '\n':
st = None
else:
out[i] = ' '
elif st == 'block':
if c == '*' and i + 1 < L and src[i + 1] == '/':
out[i] = out[i + 1] = ' '
i += 2
st = None
continue
if c != '\n':
out[i] = ' '
elif st in ('sq', 'dq'):
q = "'" if st == 'sq' else '"'
if c == '\\':
i += 2
continue
if c == q:
st = None
elif st == 'tpl':
if c == '\\':
i += 2
continue
if c == '`':
st = None
i += 1
return ''.join(out)
print('=== 一、T 别名本身 ===')
aj = read('web', 'app.js')
code = strip_comments(aj)
ck('app.js 顶部定义了 T 别名',
re.search(r"const\s+T\s*=\s*window\.i18n\s*\?\s*window\.i18n\.t\s*:\s*window\.t\s*;",
aj) is not None)
# 裸 t(':排除 window.t( / .t( / 标识符前缀(如 fmt.t(、ajax.t()
bare = re.findall(r"(?<![A-Za-z0-9_$.])t\s*\(\s*'", code)
ck('app.js 已无裸 t(\' 翻译调用', not bare,
'残留 %d 处,例:%s' % (len(bare), bare[:5]))
print()
print('=== 二、T 不被局部遮蔽 ===')
# 允许顶部那一处别名定义;其余 const/let/var T = 都是遮蔽
alias_line = re.search(r"^const\s+T\s*=\s*window\.i18n", aj, re.M)
shadow = [m.start() for m in re.finditer(r"\b(?:const|let|var)\s+T\b\s*=", code)]
if alias_line:
shadow = [p for p in shadow
if abs(code[:p].count('\n') - code[:alias_line.start()].count('\n')) > 0]
ck('没有第二个 const/let/var T =', not shadow,
'位置行号 %s' % [code[:p].count('\n') + 1 for p in shadow[:5]])
ck('没有 .map(T => / .forEach(T => 这类箭头遮蔽',
re.search(r"\.\s*(?:map|forEach|filter|find|some|every)\s*\(\s*T\s*(?:=>|,)",
code) is None)
ck('没有 function T( 定义', re.search(r"function\s+T\s*\(", code) is None)
print()
print('=== 三、其余 web 文件走 window.t,也不许遮蔽 ===')
ij = read('web', 'i18n.js')
ck('i18n.js 导出 window.t(其它文件靠它拿到 t)',
re.search(r"window\.t\s*=\s*t\s*;", ij) is not None)
for fn in ('netdetail.js', 'realtime.js', 'update.js', 'upstream.js'):
p = os.path.join(ROOT, 'web', fn)
if not os.path.isfile(p):
continue
src = read('web', fn)
c = strip_comments(src)
bad = (re.findall(r"\b(?:const|let|var)\s+t\b\s*=", c)
+ re.findall(r"\.\s*(?:map|forEach|filter|find|some|every)\s*\(\s*t\s*(?:=>|,)", c))
ck('%s 不遮蔽 t' % fn, not bad, '例:%s' % bad[:5])
# 这些文件用 t() 是可以的,但必须没有 T( 混用(混用说明改了一半)
ck('%s 不混用 T(' % fn,
re.search(r"(?<![A-Za-z0-9_$.])T\s*\(\s*'", c) is None)
print()
print('=' * 56)
print('T 别名不变量 %d 条,失败 %d 条' % (n, len(fails)))
for f in fails:
print(' FAIL: %s' % f)
sys.exit(1 if fails else 0)