多语言静态站实战:源与产物同目录的陷阱
这个站有 180 个页面、5 个语种。某天发现英文页的导航是英文、正文却整段是中文——而且已经上线好几天。查下来根因不是翻译丢了,是构建顺序。
症状:只有导航被翻译了
语言下拉框正确显示 EN,导航也是英文,但正文 hero、卡片、列表全是中文。这种「一半对一半错」的形态最容易让人误判成翻译数据丢了。
真正的诊断方法不是看页面,而是看源文件:统计每个页面正文里还有多少个多语 span。
# 统计 </nav> 之后的多语 span 数量
import re
s = open("src5/tools/chatgpt.html", encoding="utf-8").read()
body = s[s.find("</nav>"):]
print(len(re.findall(r'class="(?:zh|en|id|sl|it)"', body)))
结果是 0。36 个源页面的正文 span 全部为 0,只有 header nav 里的 25 个还在——因为 nav 是另一个脚本后来补回去的。
根因:源和产物写在同一个目录
最初的构建链是这样的:生成器把带 5 语 span 的页面写到根目录,然后 build 脚本折叠 span 生成各语种页面,再把中文版写回根目录同一个路径(因为部署需要中文版在根)。
这一步等于把「源」覆盖成了「已折叠的单语页」。下一轮构建再抓快照时,抓到的就是没有 span 的单语页——折叠时无话可选,5 个语种全部输出中文。
关键教训:只要「源」和「产物」共用目录,且构建过程会回写产物,就必须在折叠之前把多语源快照到独立目录。顺序不是习惯问题,是正确性问题。
修法:把顺序固化进脚本
光记住顺序没用——对话会忘,人会错。正确做法是写一个入口脚本,让顺序变成代码的一部分,漏一步就直接构建失败。
# build_all.py —— 唯一构建入口
STEPS = [
("i18n_lint.py", "翻译数据自检"),
("gen_content.py", "生成内容页(带 5 语 span)"),
# ...
("inject_seo.py", "注入 SEO 头(仅根页)"),
("snapshot_src5.py", "快照多语源 ← 必须在折叠之前"),
("build_i18n.py", "折叠成 180 个单语页"),
("audit_site.py", "全站自检,不过则不部署"),
]
- 快照脚本自带审计:任一页面正文 span 为 0 或页脚重复,立刻退出码 1
- 注入脚本限定只处理根页,排除 src5 与语言子目录
验证:别用肉眼看页面
肉眼检查只能覆盖你刚好打开的那一页,而且看不出「源码已退化、产物正常」这种时序问题。可靠做法是让脚本直接报告数量。
# 构建末尾断言
assert span_count(body) > 0, "正文无多语 span(会退化成单语)"
assert footer_count(page) == 1, "页脚重复"
assert len(re.findall(r'rel="alternate" hreflang', head)) == 6
最后一条来自一次真实误判:统计 hreflang 时把语言下拉菜单里的 <a hreflang> 也算了进去,误报「每个 hreflang 出现两次」。<head> 里其实恰好 6 条,完全正常。看到数量不对时,先确认统计范围,再改产品代码。