不用框架的 i18n:三语站点手写路由的代价

这个站点的手写 i18n 没用框架自带的配置。说说为什么,以及代价具体出现在哪里——两次 404,和一半没翻的文章。

语言这块我没用 Astro 自带的 i18n 配置,自己写了一套。理由不是“框架做得不好”,而是多语言在这个站上不是一个渲染问题,是一个内容管理问题:把文案换成英文是简单的那一半,让三种语言的结构不漂移才是难的那一半。

手写的是什么

三件事:

  1. 一个 [lang] 动态路由。页面文件只有一份,zh / en / ja 共用同一个 .astro
  2. 内容按语言分目录src/content/blog/<lang>/<slug>.md,语言从 entry id 的前缀取。
  3. 一份文案字典src/lib/i18n.ts 里的 LOCALESLOCALE_METAt(locale, key, vars)

路由生成的真相只有一处 —— src/server/paths.ts。页面只 export const getStaticPaths = xxxPaths,不自己拼地址。

与框架 i18n 的差别

框架的 i18n 配置 手写
路由前缀 按配置生成 我在 [lang] 里显式处理
语言检测重定向 框架接管 我自己在 / 上做
缺翻译时的行为 回退到默认语言,静默 那门语言里根本没有这篇
类型 框架的 locale 类型 我自己的 Locale,编译期能收窄
出错时的位置 框架内部 我的代码里,读得到

第三行是决定性的。「回退到默认语言」听着体贴,实际效果是英文读者点开一篇文章,看到的是中文。我宁可空着,也不要读者以为自己点错了。

静默回退会掩盖内容缺失。缺了就该缺得看得见。

代价一:路径必须只有一处生成

手写 i18n 最容易犯的错,是把语言前缀当成「随手拼接的字符串的一部分」。

我踩过:appHref(kind, slug) 返回 /tools/convert —— 没有语言前缀。而真实路由是 /[lang]/[kind]/[slug]/,于是首页和两个列表页上的每一个应用入口都 404。更麻烦的是中文站看起来完全正常,因为那条路径上别的地方正好补了前缀,只有切到英文才暴露。

修法不是「记得加前缀」,是把拼路径这件事关进唯一的函数

// src/server/meta.ts
export function href(locale: string, path = '/'): string {
  const normalized = path.startsWith('/') ? path : '/' + path;
  return '/' + locale + (normalized === '/' ? '/' : normalized);
}

现在没有任何页面或组件自己拼地址。要加一条路由,就在 server/paths.ts 里加一个 xxxPaths

代价二:三语齐全靠纪律,不靠代码

内容一个语言一个文件,所以「翻译没跟上」在构建期完全合法:构建照过,页面照出,只是英文站少几篇。

我实际的状态曾经是中文 3 篇,英文 1 篇,日文 1 篇。列表、归档、标签、RSS 全都从那门语言的文章数组里长出来,所以英文读者看到的博客只有一篇文章,而中文版一切正常。

这件事没有技术解,只能靠规则 + 文档。我把它写进了给 agent 看的撰写规范:

加了 zh/x.md 就同时加 en/x.mdja/x.md。缺一门语言,那门语言的列表、归档、标签、RSS 里就不会有这篇。

配套的还有一张标签词表 —— 标签按语言各写各的,不做跨语言对齐,所以词表得逐语言维护,否则很快会出现 性能 / パフォーマンス / performance 三个标签各指一篇的碎片状态。

现在这套东西的形状

  • 加一种语言:改 LOCALESLOCALE_META、文案字典,往 content/blog/ 加目录。
  • 加一篇文章:加三个文件,别的什么都不用动。
  • 改导航文案:只改字典,三语各一条。

代价是得自己记住这些约定,框架不会替你检查。收益是所有行为都在我读得懂的代码里,没有一层我控制不了的回退。

语言方案的价值不在于“支持多语言”,而在于缺失时它表现得诚实。

← 返回文章列表

评论