跳转到主要内容

多语言

增加一种语言、并排放置译文、按语言配置菜单与界面文案,并对齐中英标题锚点。

OINK 使用 Hugo 的多语言模型,不额外引入目录约定:配置一个 languages 块,译文与原文并排放在同一个目录里,用文件名后缀区分。以下内容覆盖单语言站点扩展为双语站点需要改动的位置,以及双语站点的两处易错点:资源归属与标题锚点。

启用第二种语言

hugo.yml
defaultContentLanguage: en

languages:
  en:
    label: English
    locale: en-US
    weight: 1
    title: OINK
    params:
      description: A Hugo theme for engineering docs
  zh:
    label: 简体中文
    locale: zh-CN
    weight: 2
    title: OINK
    params:
      description: 为工程而设计的 Hugo 文档主题
      time_format_default: 2006年1月2日
      time_format_blog: 2006年1月2日

上面是本站在用的配置。四个字段的作用:

  • label 是语言选择器里显示的名字,用该语言自己的文字书写:写 简体中文,不是 Chinese
  • locale 是标准语言标签,会进 <html lang>hreflang 备用链接和 Open Graph 元数据。
  • weight 同时决定语言排序和选择器的轮换顺序,小的在前。
  • params 是语言级覆盖:这里没写的键继承全局同名值。日期格式通常需要按语言各写一遍。

默认语言不带路径前缀(英文在 /docs/…),其它语言各占一个前缀(中文在 /zh/docs/…)。默认语言也需要前缀时加 defaultContentLanguageInSubdir: true。这会改变全站 URL,已上线的站点要同时配好重定向。

文件命名与资源

译文与原文并排放置,用后缀区分,Hugo 靠相同的基础文件名把它们认成同一页的两个语言版本:

  • content/docs/
    • install.md英文
    • install.zh.md中文
    • _index.md
    • _index.zh.md

页面包同理:index.mdindex.zh.md 放在同一个目录里。

页面包里的资源遵循一条规则:文件名不带语言后缀的资源由所有语言共享,带语言后缀的资源只属于那种语言。

  • content/docs/install/
    • index.md英文页
    • index.zh.md中文页
    • topology.webp两种语言都能用
    • screenshot.zh.webp只有中文页能用

正文里引用带后缀的资源时 写不带后缀的名字![截图](screenshot.webp),Hugo 会按当前语言解析。

这条规则有一个推论:页面包里只有 index.zh.md、没有英文对等页时,不带后缀的资源不会分给中文页,它们归属默认语言,而默认语言在这个包里没有页面。此时所有资源都必须带 .zh. 后缀,本站 docs/ 下的中文页面包即是如此。

哪些内容需要翻译:

  • 翻译titledescription、摘要、菜单标签、标签名、图片 alt、提示块正文、shortcode 里面向读者的参数。
  • 保持一致:日期、weight、别名,以及任何影响路由的元数据。两边不一致会导致侧栏顺序在两种语言下不同。
  • 不翻译:命令、配置键、文件名、URL、版本号、产品名、shortcode 名。

按语言分开的配置

三处内容不在 content/ 里,需要各语言各写一份。

菜单 写在各自语言下:

hugo.yml
languages:
  zh:
    menus:
      main:
        - identifier: docs
          name: 文档
          pageRef: /docs
          weight: 20

identifier 两种语言必须一致:命令面板的快速链接与搜索结果分组顺序都按它匹配。菜单的完整写法见导航与菜单

首页数据 按语言取文件:data/home/en.yamldata/home/zh.yaml。当前语言没有对应文件时回退到 en.yaml;单语言站点用一个 data/home.yaml 即可。见首页与落地页

界面文案:主题自带 32 个 locale 的界面字符串。英文、简体中文(zhzh-cn)和繁体中文(zh-tw)经过审校,其余语言保留继承自 Docsy 的翻译,OINK 新增的标签用英文兜底。要改某一条,在站点自己的 i18n/ 下建同名文件,只写要覆盖的键:

i18n/zh.yaml
ui_search: 搜索文档

缺译回退与语言选择器

语言选择器的图标本身是一个链接:点击它按 weight 顺序切到下一种语言(在末尾回到第一种),悬停或键盘聚焦才展开列出全部语言的菜单,触摸屏上菜单不展开,点按即切换。双语站点因此一次点击即可来回切换。

菜单始终列出全部配置的语言,不论当前页有没有译文:

  • 目标语言有译文 → 跳到那一页;
  • 目标语言没有译文 → 跳到那种语言的 首页

回退到首页优于把读者送进 404。代价是读者不一定察觉自己被送到了首页,双语站点应当把「每个页面都有对等译文」作为约束来检查,而不是依赖回退。

缺译不会用原文填充

中文页面不存在时,中文站里就没有这一页:侧栏、搜索索引、翻页顺序都不包含它。

搜索索引也按语言分开:读者在中文页面搜索只命中中文内容。中文查询采用 CJK 子串匹配,细节见全文检索

标题锚点要对齐

Hugo 从标题文本生成 ID,中文标题生成中文 ID:/docs/install/#prerequisites/zh/docs/install/#前置条件 指向同一个位置,却是两个互不相通的锚点,跨语言的深链、目录与页内跳转都会失效。

做法是在译文标题里显式写出原文的 ID:

install.zh.md
## 前置条件 {#prerequisites}

两条纪律:

  1. ID 从 英文页渲染出来的 HTML 里取,不要凭标题文本推断。标题里含行内代码、徽章或 shortcode 时,生成的 ID 与标题文本不一致。
  2. 中英对应页面的标题数量、顺序、ID 必须一致。确实需要在中文里加一节时,给它一个独立、稳定、不与英文冲突的 ID。

本站用一个脚本把这条约束变成 CI 检查,比对的是渲染后的 HTML 而不是源码:

node scripts/check-doc-translations.mjs --public public

新页面从建立时就写显式英文 {#id},成本低于事后回补。

从右向左的语言

在语言下声明书写方向:

hugo.yml
languages:
  ar:
    label: العربية
    locale: ar
    languageDirection: rtl
    weight: 3

<html dir> 随之改变,主题额外加载 Bootstrap 的 RTL 样式表。主题自身的 CSS 全部使用逻辑属性(margin-inline-start 而不是 margin-left),镜像布局自动完成。站点自己写的 CSS 同样要用逻辑属性,否则 RTL 下会错位。

验证

  1. 构建,确认两种语言的产物和索引都在:

    hugo --printPathWarnings --panicOnWarning
    ls public/index.html public/zh/index.html
    ls public/offline-search-index.*
  2. 检查 hreflang:每个页面的 <head> 里,每种语言各一条 rel="alternate",外加一条指向自己的 rel="canonical"

    grep -o 'rel="alternate" hreflang="[^"]*"' public/zh/docs/index.html
  3. 在有译文的页面上展开语言选择器并选择另一种语言,确认停在同一篇文档;在没有译文的页面上重复一次,确认落到目标语言的首页而不是 404。

  4. 两种语言各搜一次同一个概念,确认都有结果。

  5. 双语站点把标题对齐检查接进 CI,见上一节的脚本。