本站的公司简介、事业内容等主要页面早已支持日语、英语、简体中文三种语言,唯独博客一直只有日语版,英中页面只是直接链接到日语文章。这次我们为全部既有文章都新增了英语和简体中文版本,博客也实现了三语言对应。本文整理这次的设计与实现,以及过程中遇到的限制。

设计 — 用文件名对应译文

本站的博客基于Astro的content collection管理:src/content/blog/*.md 下的每个文件对应一篇文章,文件名直接构成URL。在新增译文时,首先要决定的是如何将日语原文与其译文对应起来。

Astro允许在 src/content.config.ts 中定义多个content collection。这次我们在原有的 blog(日语)之外,新增了 blogEnblogZh。三者都使用相同的 glob() 加载器,仅所指向的目录不同:src/content/blogsrc/content/blog-ensrc/content/blog-zh

官方文档对 glob() 加载器的行为说明如下:

When using the glob() loader with Markdown, MDX, Markdoc, JSON, or TOML files, every content entry id is automatically generated in an URL-friendly format based on the content filename.

(使用glob()加载器处理Markdown、MDX、Markdoc、JSON或TOML文件时,每个内容条目的id都会根据文件名自动生成为适合URL的格式)

也就是说,文件名会直接成为该collection内的id。由此我们确定了设计方案:**只要三个collection使用相同的文件名(YYYY-MM-DD-slug.md),就能仅凭文件名一致来表达“跨语言的同一篇文章”这一对应关系。**由于 /blog/<slug>/en/blog/<slug>/zh/blog/<slug> 指向的都是同一个 <slug>,上下篇文章导航、语言切换、hreflang标签的实现,都可以简化为一个基于slug的简单集合运算——判断某个id是否也存在于另一个collection中。

缩略图方面,我们没有让译文单独持有 thumbnail 字段,而是始终引用同一 <slug> 对应日语版的缩略图。这样既无需复制图片,也不必在缩略图更新时逐一同步。

路由 — 每个页面都需要各自的getStaticPaths

Astro默认采用静态站点输出(SSG)。对于动态路由,官方文档如下说明:

Because all routes must be determined at build time, a dynamic route must export a getStaticPaths() that returns an array of objects with a params property.

(由于所有路由都必须在构建时确定,动态路由必须导出一个getStaticPaths(),返回一个包含params属性的对象数组)

这个 getStaticPaths() 必须写在页面文件本身(src/pages/**/*.astro)中,无法委托给其他Astro组件。本站的博客有一览、文章详情、按标签筛选三种页面,乘以三种语言,就意味着有九个页面文件,每个都需要各自的 getStaticPaths()

如果九个页面各自独立实现,标记结构、CSS、JS都会出现二重甚至三重维护的问题。因此我们让页面文件只负责“获取对应的collection、通过getStaticPaths确定URL、传递数据”,而将实际的界面、文案、脚本集中到共享组件中:BlogPostPage.astroBlogListPage.astroBlogTagPage.astro。这延续了公司简介、事业内容等固定页面早已采用的模式——组件接收locale属性,由各语言的薄页面包装调用——只是这次也应用到了动态路由上。

src/pages/blog/[slug].astro      // getStaticPaths(blog collection)+ <BlogPostPage locale="ja" .../>
src/pages/en/blog/[slug].astro   // getStaticPaths(blogEn collection)+ <BlogPostPage locale="en" .../>
src/pages/zh/blog/[slug].astro   // getStaticPaths(blogZh collection)+ <BlogPostPage locale="zh" .../>

得益于这一结构,语言切换和hreflang标签的实现也变得简单:各页面的getStaticPaths只需检查同一个slug是否也存在于另外两个collection中,并把可用语言列表通过translatedLocales属性传给公共布局即可。

遇到的限制 — 脚注标题标签只能设置一份

本站文章的引用来源,要么使用手动的### 参考标题加列表,要么使用Markdown的脚注语法([^1])。使用脚注语法时,Astro的markdown处理器会在文末自动生成标题,而本站在astro.config.mjs中把这个标题标签固定为了“参考文献”。

markdown: {
  processor: satteri({
    features: {
      gfm: {
        footnotes: {
          label: '参考文献',
          backLabel: '本文の参照箇所{reference}に戻る',
        },
      },
    },
  }),
},

这份markdown处理器配置在全站范围内只有一份实例,没有按文章语言切换的机制。也就是说,即便英文或中文文章使用脚注语法,生成的标题依然会是日语的“参考文献”,而不会变成对应语言。

我们的应对方式是:译文一律不使用脚注语法,即便日语原文使用了脚注,译文也统一转换为手动的标题加列表形式(英文版用### References,中文版用### 参考资料)。由于本站原本就允许“### 参考“或”脚注语法“两种写法,译文只需统一采用前者即可。

小结

  • Astro允许定义多个content collection。由于glob()加载器会根据文件名自动生成每个条目的id,只要三个collection(blogblogEnblogZh)使用相同的文件名,就能仅凭文件名一致来表达跨语言的同一篇文章
  • 在Astro的静态输出模式下,动态路由的getStaticPaths()必须写在该页面文件中,无法委托给其他组件。因此我们让页面文件专注于数据获取,而把界面、文案、脚本集中到接收locale属性的共享组件中
  • markdown处理器的设置(如脚注标题标签)在全站只有一份实例。我们的应对方式是译文不使用脚注语法([^n]),统一转换为手动的标题加列表
  • 缩略图不做复制,译文始终引用日语版的图片

参考资料