# 多语言

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

---

LLMS 索引： [llms.txt](/zh/llms.txt)

---

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

## 启用第二种语言 {#enable}

```yaml {title="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，已上线的站点要同时配好重定向。

## 文件命名与资源 {#files}

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

```filetree
- content/docs/
  - install.md          # 英文
  - install.zh.md       # 中文
  - _index.md
  - _index.zh.md
```

页面包同理：`index.md` 与 `index.zh.md` 放在同一个目录里。

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

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

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

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

哪些内容需要翻译：

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

## 按语言分开的配置 {#per-language}

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

**菜单** 写在各自语言下：

```yaml {title="hugo.yml"}
languages:
  zh:
    menus:
      main:
        - identifier: docs
          name: 文档
          pageRef: /docs
          weight: 20
```

`identifier` 两种语言必须一致：命令面板的快速链接与搜索结果分组顺序都按它匹配。菜单的完整写法见[导航与菜单](/zh/docs/customize/navigation/)。

**首页数据** 按语言取文件：`data/home/en.yaml`、`data/home/zh.yaml`。当前语言没有对应文件时回退到 `en.yaml`；单语言站点用一个 `data/home.yaml` 即可。见[首页与落地页](/zh/docs/customize/home/)。

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

```yaml {title="i18n/zh.yaml"}
ui_search: 搜索文档
```

## 缺译回退与语言选择器 {#fallback}

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

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

- 目标语言有译文 → 跳到那一页；
- 目标语言没有译文 → 跳到那种语言的 **首页**。

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

> [!NOTE] 缺译不会用原文填充
> 中文页面不存在时，中文站里就没有这一页：侧栏、搜索索引、翻页顺序都不包含它。

搜索索引也按语言分开：读者在中文页面搜索只命中中文内容。中文查询采用 CJK 子串匹配，细节见[全文检索](/zh/docs/customize/search/)。

## 标题锚点要对齐 {#anchors}

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

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

```markdown {title="install.zh.md"}
## 前置条件 {#prerequisites}
```

两条纪律：

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

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

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

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

## 从右向左的语言 {#rtl}

在语言下声明书写方向：

```yaml {title="hugo.yml"}
languages:
  ar:
    label: العربية
    locale: ar
    languageDirection: rtl
    weight: 3
```

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

## 验证 {#verify}

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

   ```bash
   hugo --printPathWarnings --panicOnWarning
   ls public/index.html public/zh/index.html
   ls public/offline-search-index.*
   ```

2. 检查 `hreflang`：每个页面的 `<head>` 里，每种语言各一条 `rel="alternate"`，外加一条指向自己的 `rel="canonical"`。

   ```bash
   grep -o 'rel="alternate" hreflang="[^"]*"' public/zh/docs/index.html
   ```

3. 在有译文的页面上展开语言选择器并选择另一种语言，确认停在同一篇文档；在没有译文的页面上重复一次，确认落到目标语言的首页而不是 404。

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

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

## 相关 {#related}

- [全文检索](/zh/docs/customize/search/) — 分语言索引与中文查询
- [导航与菜单](/zh/docs/customize/navigation/) — 按语言配菜单与语言选择器的位置
- [首页与落地页](/zh/docs/customize/home/) — `data/home/<lang>.yaml`
- [编写页面](/zh/docs/write/pages/) — 显式标题 ID 的写法
- [分析与 SEO](/zh/docs/admin/analytics/) — `hreflang` 与站点地图怎么被搜索引擎消费
