Languages

Hugo does multilingual sites either by giving each language its own content directory (content/en and content/de, or content for the default language and content.de for the other) or by keeping every language in one directory and putting the language code in the filename (post.md, post.de.md). Emet needs the same mapping so it can switch between them, find an entry’s translations and work out URLs.

languages:
  - code: en
    name: English
    contentDir: content/en
    default: true
  - code: de
    name: Deutsch
    contentDir: content/de
Key What it does
code The language code, as in your Hugo config.
name What the language switcher calls it.
contentDir That language’s content directory, relative to the Hugo root.
default true on exactly one language. If none is marked, the first in the list is the default.

Leave the block out and Emet assumes a single language, en, in content. The language switcher only appears once there are two or more.

Next to the block, one top-level key picks the layout:

Key What it does
languages_layout directory (the default) for one content directory per language, or filename for language suffixes on filenames. See Translation by filename.

How sections map across languages#

You define each section once, pointing at the default language’s folder:

sections:
  blog:
    folder: content/en/blog

When you switch to German, Emet swaps the content/en prefix for content/de and looks in content/de/blog. The same swap is how it finds an entry’s translations and decides where to put a new one: everything after the language directory stays the same, so content/en/blog/2024/post.md pairs with content/de/blog/2024/post.md and the bundle content/en/blog/post/index.md with content/de/blog/post/index.md. The prefix is matched on whole path segments, so languages whose codes overlap (content/en and content/en-us, say) don’t get confused. For this to work the section’s folder has to start with a language’s contentDir; a folder like content/blog on a site whose languages live in content/en and content/de won’t be found in any language but the default.

Entries whose path doesn’t start with any contentDir are treated as belonging to the default language. Page URLs for a non-default language get a /<code>/ prefix, which matches Hugo’s default of putting only the non-default languages in a subdirectory.

If you’ve set base_path, each contentDir is prefixed with it, like every other path in the config.

Translation by filename#

Hugo’s other layout keeps every language in the same directory and marks the language in the filename, just before the extension: post.md and post.de.md, about/index.md and about/index.de.md, _index.md and _index.de.md. Files without a suffix belong to the default language. To use it, set languages_layout: filename and give every language the same contentDir:

languages_layout: filename
languages:
  - code: en
    name: English
    contentDir: content
    default: true
  - code: de
    name: Deutsch
    contentDir: content

With this layout a section’s folder is the same for every language, so switching the language in the header keeps you in the same directory and only changes which files you see: de shows the *.de.md files, the default language shows the files with no suffix. Translations are paired by directory and filename with the suffix removed, so content/posts/hello.md, content/posts/hello.de.md and content/posts/hello.fr.md are one page, and a new translation is created next to its source with the target language’s suffix (or no suffix, for the default language). New entries created while a non-default language is selected get its suffix too.

The suffix has to be one of the configured codes; anything else is just part of the filename, so release.v2.md is a default-language page called release.v2, and overlapping codes (en and en-us) are told apart because the whole segment has to match. Only the segment right before the extension counts.

The default language never carries a suffix in Emet, which is Hugo’s default too. If your Hugo config sets disableDefaultLanguageInSubdir or similar, that affects URLs, not filenames, and needs no change here; but a site whose default-language files do carry a suffix (post.en.md with en as the default) is not supported.

Linking translations with translationKey#

In both layouts Hugo also links pages whose frontmatter has the same translationKey, however they’re named. Emet does the same: when an entry has a translationKey, its translations are the entries in each other language’s corresponding directory (the same relative path in the directory layout, the same directory in the filename layout) with a matching key, and the usual path-based match is used only when no keyed match is found. Since the key, not the filename, does the linking, renaming a keyed entry doesn’t detach it, and the frontmatter panel doesn’t warn that it will.

Creating a translation of a keyed entry copies the key along with the rest of the frontmatter, and the copy still goes to the path-based location (content/de/posts/hello.md or posts/hello.de.md); you can rename it afterwards and the badge will keep following the key. Working this out means reading the frontmatter of the files in those directories once, so it’s done only for entries that actually have a key, and the result is cached alongside the directory listing.

What the setup wizard generates#

The setup wizard and the local dev server write this block for you when they detect a multilingual site, so the translation features appear without hand-editing. Your Hugo config is the source of truth:

  • If hugo.toml (or config/_default/languages.toml) declares two or more languages, each one becomes an entry. name comes from languageName, or from a built-in table of common codes when that’s missing (pt-BR becomes “Portuguese (BR)”). contentDir comes from the language’s contentDir, or content/<code> when that folder exists; a language with neither is left out. The one matching defaultContentLanguage is marked default. Languages with disabled = true are left out too, and the rest are ordered by weight, default first.

  • Without a languages map, a content/ directory made up entirely of language-code folders (content/en, content/de, …) is treated as one folder per language. Names come from a built-in table of common ISO 639-1 codes (de → German), falling back to the code. The default is defaultContentLanguage if set, otherwise en if present, otherwise the first alphabetically.

  • A Hugo config that declares two or more languages without per-language content directories, on a content/ tree that contains files with a language suffix (post.de.md) for at least one of those codes, is translation by filename. Every language gets contentDir: content and languages_layout: filename is written. A site that declares languages but has neither per-language directories nor suffixed files yet is treated as single-language; add the block and the switch by hand once you start adding translations.

Section folders are generated against the default language’s contentDir, so the swap described above works out of the box. A site that resolves to a single language, including one with only content/en, gets no languages block at all.

The wizard shows the detected languages on its final step before committing anything, and the block is plain YAML you can edit afterwards. See Internationalisation for the translation workflow once it’s configured.