Languages
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
That is this site’s configuration. What the four fields do:
labelis the name shown in the language picker, written in that language’s own script:简体中文, notChinese.localeis the standard language tag, and reaches<html lang>, thehreflangalternate links and the Open Graph metadata.weightdecides both language order and the picker’s cycle order, lowest first.paramsis 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
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:
content/docs/
- install.mdEnglish
- install.zh.mdChinese
- _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.
content/docs/install/
- index.mdEnglish page
- index.zh.mdChinese page
- topology.webpavailable to both languages
- screenshot.zh.webpavailable to the Chinese page only
When the body references a suffixed resource, write the name without the
suffix: , 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
Three things live outside content/ and need one copy per language.
Menus are written under their own language:
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.
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.
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:
Untranslated fallback and the language picker
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.
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.
Heading anchors must align
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:
Two disciplines:
- 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.
- 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:
Writing explicit English {#id} anchors from the moment a page is created costs
less than retrofitting them.
Right-to-left languages
Declare the writing direction under the language:
<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
-
Build, and confirm both languages’ output and indexes exist:
-
Check
hreflang: each page’s<head>should carry onerel="alternate"per language plus arel="canonical"pointing at itself. -
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.
-
Search the same concept once in each language and confirm both return results.
-
Wire the heading alignment check into CI on a bilingual site, using the script above.
Related
- Search — per-language indexes and CJK queries
- Navigation and menus — per-language menus and where the picker sits
- Home and landing pages —
data/home/<lang>.yaml - Writing pages — how to write explicit heading IDs
- Analytics and SEO — how
hreflangand the sitemap are consumed by search engines