Topics

用EmDash创建多语言博客 〜在SSR中降低翻译API成本的方法〜

  • column

我们的博客「ゆるろぐ」,其实最近悄悄推出了英语版。这次我想分享其中的细节,特别是如何解决「SSR 网站多语言化时 API 成本是否会爆炸」这个问题。

前提:使用 EmDash CMS

这个博客使用 EmDash(一个基于 Astro 的 CMS)驱动。文章通过管理界面编写,前端使用 Astro 进行服务器端渲染(SSR)。

操作感觉很直观,与 WordPress 相似,有人称其为"WordPress 的精神继承者"。

EmDash 本身就提供了"多语言内容"的机制,文章、页面、菜单和分类法(分类、标签)等都可以作为每个语言区域的独立记录。只需通过 translationOf 字段标记"这篇英文文章是这篇日文文章的翻译",就可以将其视为同一篇文章的不同语言版本。

从 microCMS 迁移到 EmDash + Cloudflare

说到多语言化,如果用 SSG 的话就很简单了

通常来说,"通过 API 自动化多语言翻译"这样的需求,如果是 SSG(静态站点生成)就相对容易解决。

  • 在构建时仅调用一次翻译 API
  • 将结果作为静态文件缓存
  • 之后只需要分发该 HTML

因此,即使文章数量增加再多,API 成本也只在「每次构建时」才会产生。频率也很容易控制,缓存也很直接。

但是 EmDash 是 SSR。那么每次请求都要调用 API 吗?

问题就在这里。EmDash 的 CMS 页面按规范来说,全都是 SSR(output: "server")渲染的。不是采用静态构建来创建页面的方式,而是每当收到请求时,就从 CMS 的 API・DB 获取最新内容并渲染的机制。

那么如果「显示英文页面时,就地用 Gemini 翻译并输出」这样简单的实现方式会怎样呢?

  • 每次请求都会产生翻译 API 费用
  • 延迟也会增加(需要等待 LLM 的响应)
  • 同一篇文章每次可能都会返回略有不同的翻译结果(缺乏再现性)

这完全是行不通的。即使是 SSR,如果采用「每次调用 API」的方案,成本和用户体验都会崩溃。

解决方案:将翻译从「请求时」转移到「内容创建时」

答案很简单,不要在请求时进行翻译。

具体来说,我建立了这样的流程。

  1. 在 EmDash 管理面板中撰写和发布日语文章
  2. 只运行一次翻译脚本(scripts/translate.mjs)
    • 从 EmDash API 获取日语文章的 Portable Text(结构化富文本)
    • 提取应该翻译的文本部分,然后发送给 Gemini API(gemini-3.6-flash)
    • 图像和亚马逊产品卡片等块保持原样直接通过,不进行翻译
  3. 将翻译结果作为新的英语文章保存到 EmDash 数据库
    • 在 translationOf 中指定原始日语文章的 ID,将其链接为同一文章组的英语版本
    • 保存为草稿,以便审核后发布

也就是说,翻译结果不是「实时生成的内容」,而是作为「保存在 CMS 中的另一种语言的内容」来处理的。

这样做的话,SSR 的请求处理本身和平时显示文章没有任何区别。只需通过 EmDash 的常规本地化查询(getEmDashEntry(..., { locale: "en" }))从数据库中获取已保存的英文内容。与 Gemini API 的通信只在翻译文章时发生一次,浏览时完全不会产生。

也就是说,这样的结构

翻译运行的时机

请求时的 API 成本

SSG + 按需翻译

构建时

零(静态文件交付)

SSR + 按需翻译(反面例子)

每个请求

文章数 × 访问次数就会产生费用 😱

SSR + 预翻译保存到DB(本次方案)

内容创建时(仅1次)

零

SSG 的「构建时统一缓存」这一思路,如果要应用到 SSR 的世界中,「将翻译后的内容作为永久化数据保存在 DB 中」就是其替代方案。这是本次的重要发现。即使是 SSR,内容本身也不需要动态生成,只要提前完成翻译这种「重型处理」,配信本身就能达到普通 CMS 页面的轻量级性能。

本地和生产环境 DB 不同,多次出现「咦,数据消失了!?」的故事

本次实现中,最令人精疲力尽的就是这个问题。EmDash 在本地开发环境和生产环境中分别使用不同的 DB。说起来理所当然,但正因为如此,我多次遭遇过「刚才还有的功能突然消失了!」的情况。

症状大致如下

  • 页头导航突然变成空的
  • 页脚的"Categories"列表消失了
  • 文章页面的侧边栏小部件消失
  • 辛苦创建的本地菜单项和小工具被还原了

而且这些问题都会在"它刚才还能正常运行啊"这个时刻发生,所以一开始我都是在怀疑"自己是不是弄坏了什么?"的同时去寻找原因。

原因1:将生产数据库拉取到本地的处理(pull.sh)

在开发过程中,我们曾多次运行脚本将生产数据库完整地拉取到本地,原因是"想在本地也能看到生产环境的最新数据"。这样做的话,本地数据库会被完全覆盖成生产环境的状态(比如英文文章还是草稿状态,或是菜单结构还未完成公开前的配置)。

也就是说,即使想在本地确认"生产环境中还未公开的功能",但 pull 之后会立即恢复到与生产环境相同的状态,所以在本地进行的超前工作会暂时消失。这就是发生的问题。

原因2:每次重新进入本地管理界面时运行的"种子"处理

另一个原因是,为了重新登录本地管理界面而建立的机制(开发用旁路登录)同时也会每次重新读取seed.json(初始数据定义文件)。

这很麻烦,

  • 菜单项目·小部件 → 被种子端的内容「完全匹配地」覆盖(=本地手动添加的部分会消失)
  • 分类·标签等分类法 → 「已存在的项目会被跳过」所以不会消失

这样就出现了按项目不同行为也不同的规范。所以才会出现「分类还在,但菜单和小部件每次都消失」这样看似莫名其妙的现象。

采取的措施

从根本上消除这种二重结构(通过pull覆盖/通过种子覆盖)很困难,所以作为务实的折中方案

  • 将seed.json中的演示用虚拟内容(菜单项目·小部件·分类等)全部清空
  • 每次进入本地环境时,通过API重新创建必要的菜单和小部件

通过这样的运维方式来应对。这不是根治,而是缓解方案,但至少能防止「被意外的演示数据覆盖」的事故发生。

总结

  • EmDash 本来就是一个支持多语言的 CMS,能够为每个语言环境持有内容
  • 如果是 SSG,「在构建时翻译→缓存」就可以了,但 SSR 的话「每次都翻译」会变成费用地狱
  • 解决方案是仅在内容创建时执行一次翻译,而不是在请求时,并将结果作为普通CMS内容保存到数据库中
  • 使用的翻译引擎是Gemini API(gemini-3.6-flash)
  • 不过在将CMS多语言化时,不仅要整理翻译流程,还要记住「本地和生产环境中数据库分离本身会产生新的陷阱」
  • 特别是「在生产环境还没有新功能时,预先在本地环境中创建」这类开发方式,容易受到环境同步机制(拉取、数据填充、缓存等)副作用的影响

本次的经验教训是,只要提前掌握「何时进行翻译」和「何时以及覆盖什么的环境同步」这两个时序设计,即使是SSR也能够在不被运维成本和事故困扰的情况下实现多语言支持

您的网站也可以考虑实现多语言化!

本文作者

我主要从事以标记语言为中心,使用JavaScript、React、Next.js进行前端开发的工作。当我参与的网站顺利发布时,我会感到很高兴!我的爱好是弹吉他。喜欢猫和烤红薯🐱🍠

Hiratch

前端工程师 / 2022年入职

查看本员工的文章

安心的团队体制和迅速的反应能力是我们的优势

Liberogic 拥有经验丰富的员工团队,积极推进项目,因此获得了客户的高度评价。
我们会妥善分配项目经理和总监,确保整个项目顺利进行。 通过避免不必要的全职投入导致的成本增加,并采用适当配置人力资源的方式,从把握业务内容到估价的制作和提交速度都赢得了良好的口碑。

* 本公司不积极开展SES驻场工作等业务,敬请谅解。

Slack、Teams、Redmine、Backlog、Asana、Jira、Notion、Google Workspace、Zoom、Webex 等,您可以使用几乎所有主要的项目管理工具和沟通协作工具。

请咨询我们的网站相关问题。

案例分析