# Languages

> Add a language, keep translations side by side, configure menus and interface strings per language, and align heading anchors across languages.

---

LLMS index: [llms.txt](/llms.txt)

---

OINK uses Hugo's multilingual model and adds no directory conventions of its
own: configure a `languages` block, and keep a translation beside its original
in the same directory, distinguished by a filename suffix. What follows covers
what a single-language site has to change to become bilingual, plus the two
things bilingual sites get wrong: resource ownership and heading anchors.

## Enabling a second language {#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日
```

That is this site's configuration. What the four fields do:

- `label` is the name shown in the language picker, written in that language's own script: `简体中文`, not `Chinese`.
- `locale` is the standard language tag, and reaches `<html lang>`, the `hreflang` alternate links and the Open Graph metadata.
- `weight` decides both language order and the picker's cycle order, lowest first.
- `params` is a per-language override: a key not written here inherits the global value of the same name. Date formats usually need one per language.

The default language carries no path prefix (English at `/docs/…`), and each
other language takes one (Chinese at `/zh/docs/…`). To give the default language
a prefix too, add `defaultContentLanguageInSubdir: true`. That changes every URL
on the site, so a live site needs redirects arranged at the same time.

## File naming and resources {#files}

A translation sits beside its original, distinguished by suffix, and Hugo treats
the shared base filename as making them two language versions of one page:

```filetree
- content/docs/
  - install.md          # English
  - install.zh.md       # Chinese
  - _index.md
  - _index.zh.md
```

Page bundles work the same way: `index.md` and `index.zh.md` in one directory.

Resources in a page bundle follow one rule: a resource whose filename has no
language suffix is shared by every language, and one with a suffix belongs only
to that language.

```filetree
- content/docs/install/
  - index.md              # English page
  - index.zh.md           # Chinese page
  - topology.webp         # available to both languages
  - screenshot.zh.webp    # available to the Chinese page only
```

When the body references a suffixed resource, **write the name without the
suffix**: `![Screenshot](screenshot.webp)`, and Hugo resolves it for the current
language.

That rule has a corollary: in a page bundle holding only `index.zh.md` with no
English counterpart, unsuffixed resources are not handed to the Chinese page —
they belong to the default language, which has no page in that bundle. Every
resource then needs the `.zh.` suffix, which is how the Chinese page bundles
under this site's `docs/` are arranged.

What needs translating:

- **Translate**: `title`, `description`, summaries, menu labels, tag names, image alt text, callout bodies, and reader-facing shortcode parameters.
- **Keep identical**: dates, `weight`, aliases, and any metadata affecting routing. A mismatch makes the sidebar order differ between languages.
- **Do not translate**: commands, configuration keys, filenames, URLs, version numbers, product names, shortcode names.

## Per-language configuration {#per-language}

Three things live outside `content/` and need one copy per language.

**Menus** are written under their own language:

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

The `identifier` must match across languages: the command palette's quick links
and the grouping order of search results both match on it. Configuring menus
fully is in [Navigation and menus](/docs/customize/navigation/).

**Home page data** is chosen by language: `data/home/en.yaml`,
`data/home/zh.yaml`. Without a file for the current language it falls back to
`en.yaml`; a single-language site needs only one `data/home.yaml`. See
[Home and landing pages](/docs/customize/home/).

**Interface strings**: the theme ships interface strings for 32 locales.
English, Simplified Chinese (`zh` and `zh-cn`) and Traditional Chinese
(`zh-tw`) are reviewed; the rest keep the translations inherited from Docsy,
with English fallbacks for the labels OINK added. To change one, create a file
of the same name under the site's own `i18n/` and write only the keys you
override:

```yaml {title="i18n/en.yaml"}
ui_search: Search the docs
```

## Untranslated fallback and the language picker {#fallback}

The language picker's icon is itself a link: clicking it moves to the next
language by `weight` (wrapping at the end), while hovering or focusing it
expands a menu of every language. On a touch screen the menu does not expand and
a tap switches directly. A bilingual site therefore toggles back and forth in
one click.

The menu always lists every configured language, whether or not the current page
is translated:

- The target language has a translation → jump to that page;
- The target language has none → jump to that language's **home page**.

Falling back to the home page beats dropping the reader into a 404. The cost is
that the reader may not notice being sent there, so a bilingual site should
check "every page has a counterpart" as a constraint rather than relying on the
fallback.

> [!NOTE] A missing translation is never filled in with the original
> When a Chinese page does not exist, the Chinese site does not have that page
> at all: it is absent from the sidebar, the search index and the paging order.

Search indexes are also per language: searching from a Chinese page matches
Chinese content only. CJK queries use substring matching, detailed in
[Search](/docs/customize/search/).

## Heading anchors must align {#anchors}

Hugo derives an ID from the heading text, so a Chinese heading yields a Chinese
ID: `/docs/install/#prerequisites` and `/zh/docs/install/#前置条件` point at the
same place through two anchors that do not connect, and cross-language deep
links, contents and in-page jumps all break.

The remedy is to write the original's ID explicitly on the translated heading:

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

Two disciplines:

1. Take the ID from the **HTML the English page renders**, not from the heading text. When a heading contains inline code, a badge or a shortcode, the generated ID does not match the heading text.
2. Corresponding pages must have the same number of headings, in the same order, with the same IDs. Where a translation genuinely needs an extra section, give it an independent, stable ID that does not collide with the English side.

This site turns that constraint into a CI check with a script that compares
rendered HTML rather than source:

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

Writing explicit English `{#id}` anchors from the moment a page is created costs
less than retrofitting them.

## Right-to-left languages {#rtl}

Declare the writing direction under the language:

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

`<html dir>` changes with it, and the theme additionally loads Bootstrap's RTL
stylesheet. The theme's own CSS uses logical properties throughout
(`margin-inline-start` rather than `margin-left`), so mirroring happens by
itself. A site's own CSS needs logical properties too, or it will be misplaced
under RTL.

## Verify {#verify}

1. Build, and confirm both languages' output and indexes exist:

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

2. Check `hreflang`: each page's `<head>` should carry one `rel="alternate"` per language plus a `rel="canonical"` pointing at itself.

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

3. On a translated page, expand the language picker and choose the other language; confirm you stay on the same document. Repeat on an untranslated page and confirm you land on that language's home page rather than a 404.

4. Search the same concept once in each language and confirm both return results.

5. Wire the heading alignment check into CI on a bilingual site, using the script above.

## Related {#related}

- [Search](/docs/customize/search/) — per-language indexes and CJK queries
- [Navigation and menus](/docs/customize/navigation/) — per-language menus and where the picker sits
- [Home and landing pages](/docs/customize/home/) — `data/home/<lang>.yaml`
- [Writing pages](/docs/write/pages/) — how to write explicit heading IDs
- [Analytics and SEO](/docs/admin/analytics/) — how `hreflang` and the sitemap are consumed by search engines
