英文版上线:一次「类型全过、构建才报错」的 RSC 序列化课
目标
把主站做成中英双语,给海外/英文面试官一个完整的英文版本。需求拍板的三个边界:
- 主站全套英文,构建日志留中文——日志是写给中文读者的施工实录,翻译收益低、维护成本高;英文页诚实标注「build log is in Chinese」。
- AI 分身在英文模式也说英文——核心展品不能只有壳是英文。
- 中文默认在
/,英文在/en,手动切换——不做基于Accept-Language的自动跳转(爬虫/分享不可控),切换按钮常驻导航。
关键决策:选「最不容易出事」的 i18n 形态
Next 有好几种 i18n 玩法(middleware 重写、[lang] 动态段、route group)。这站已经上线、且九屏 scrub 引擎 + 聊天岛对路由结构敏感,改路由结构 = 高风险。所以选了最克制的一套:
- 一套根 layout(
<html lang="zh-CN">),英文页面用/en/*实体路由,页面体外包一层<div lang="en">就近声明语言; getContent(lang)单一数据源:中英两套content/在模块加载时各跑一遍 zod 校验(写错 →next build直接挂),组件全部改成吃contentprop,中英复用同一套组件体;- 界面 chrome 文案(按钮、页脚、导出头)抽到
src/lib/ui.ts的getUi(lang),不混进content/(那是个人内容); - 导航/页脚需要按当前路径决定语言与镜像链接,做成了两个极小的客户端组件(
usePathname判语言)——这违反了「客户端 JS 只许聊天岛 + 导航高亮」的铁律一点点,但在「单 layout + 静态导出」下,server 组件拿不到当前 pathname(headers()会把 layout 打成动态、毁掉 SSG),权衡后接受,并在此如实记录。
结果
- 新增
/en、/en/projects、/en/projects/[slug]、/en/ai-twin、/en/about、/en/privacy+ 独立英文 OG 图,共 45 个静态页全部预渲染通过。 - 架构图 SVG、隐私页、AI 分身原理页这些「长 prose + 内联强调 + 表格」的页面,做成
{ content }驱动、按lang分支的双语视图组件(src/components/pages/*),中英同源。 - AI 分身:
buildSystemPrompt(lang)用对应语言的资料 + 英文版行为边界;演示模式兜底、限流 429、上游报错提示全部按?lang=en出英文;/en/ai-twin页里展示的 system prompt 就是buildSystemPrompt("en")的真实输出(与线上同源)。 - 全站 hreflang 互链(每个中文页 ↔ 英文页)、sitemap 中英成对。
踩坑与纠正
1. 最贵的坑:tsc 全过,next build 才炸。 Ui 类型里有一项 chatExportContact: (origin) => string——导出对话纪要时拼联系人行,origin 只有客户端知道,所以写成了函数。npm run typecheck 一路绿灯。直到 next build 预渲染 /en/projects/noworries 才报:
Functions cannot be passed directly to Client Components unless you explicitly expose it by marking it with "use server".
根因:页脚和聊天岛是客户端组件,会收到整个 Ui 对象,而函数不能跨 RSC → 客户端边界序列化。tsc 只查类型形状,不查「这东西能不能被 React 序列化过边界」——这类错误只有真构建才抓得到。纠正:把函数改成带 {origin} 占位符的纯字符串,客户端 .replace("{origin}", origin);整个 Ui 变成可序列化,顺手把页脚 props 收窄到只传 6 条页脚文案(别把聊天/招聘文案塞进每页页脚 bundle)。教训:跨 RSC 边界的 props 必须当可序列化数据设计,且 typecheck 绿 ≠ build 绿,提交前 build 不能省。
2. 标题模板把中文品牌缀到英文页。 根 layout 的 title.template = "%s · 黄一航",于是英文页标题变成「About · 黄一航」。改用 title: { absolute } 让英文页跳过模板。
3. 英文首页终章里混进中文日志卡。 终章「彩蛋作品」拉最近几篇构建日志做证据,而日志留中文——英文页里就露出中文标题。没有硬翻日志,而是在英文文案里诚实写明「build log is in Chinese」,CTA 也标注 (in Chinese),让人点之前就知道。
用时与备注
约 1.5 小时(含一轮 6 agent 并行翻译全部 content 模块、组件大改、四场景聊天验证)。验证矩阵在 ?lang=en 下全部复跑:① 无 env → 英文演示兜底 ② mock → live 流式 ③ 错误 key → 优雅落英文演示、HTTP 200 不 5xx ④ 连发 → 英文 429。中文侧回归确认未被重构破坏。