Hugo has had multilingual support for a long time, but the editing side of it has always been thin: you keep parallel content directories in sync by hand and hope nobody forgets the French version of last week’s post. Emet understands Hugo’s language layout, so it can show you which translations exist, swap the entry list between languages, and create a translation with a click.
What Emet supports#
Hugo lets you organise translations in two ways: one content directory per language (content/en/posts/hello.md, content/fr/posts/hello.md) or language suffixes on filenames in one directory (posts/hello.md, posts/hello.fr.md). Emet supports both. The directory layout is the default; the filename layout is switched on with languages_layout: filename in site.yaml, which the setup wizard writes for you when it finds suffixed files.
Hugo pairs translations by their path relative to the language directory (content/en/posts/hello.md and content/fr/posts/hello.md are the same page) or, in the filename layout, by directory and filename with the suffix removed (posts/hello.md and posts/hello.fr.md). Emet uses the same rules. Keep filenames identical across languages; if you want a language-appropriate URL, set slug in the frontmatter rather than renaming the file. Where a different filename really is wanted, Hugo’s third mechanism works too: give the pages the same translationKey in their frontmatter and Emet links them by that key regardless of their names.
A matching hugo.toml looks like this:
defaultContentLanguage = 'en'
[languages.en]
contentDir = 'content/en'
languageName = 'English'
weight = 1
[languages.fr]
contentDir = 'content/fr'
languageName = 'Français'
weight = 2Telling Emet about your languages#
At run time Emet doesn’t read the languages block from your Hugo config; it uses its own copy in admin/site.yaml, which is a little redundant but keeps Emet’s config self-contained. The setup wizard and the local dev server derive that copy from your Hugo config when they generate the file:
languages:
- code: en
name: English
contentDir: content/en
default: true
- code: fr
name: Français
contentDir: content/frcode and contentDir should match your Hugo config; name is what the language switcher shows. Mark exactly one language default. Without a languages block, Emet assumes a single English site rooted at content, which is what most sites are. The setup wizard writes this block for a multilingual site, using languageName, contentDir and defaultContentLanguage from your Hugo config; if you add a language later, add it here too. The full key reference, and the detection rules, are on the Languages configuration page.
If your site lives in a subdirectory via base_path, that’s prefixed onto every contentDir for you.
Sections and languages#
You define each section once, pointing at the default language’s directory:
sections:
posts:
label: Posts
folder: content/en/posts
fields:
- name: title
type: stringWhen you switch to French, Emet swaps the content/en prefix for content/fr and reads content/fr/posts instead. The same swap applies when creating or duplicating an entry, so new French posts land in the French folder. You can think of a section as a path template with the language directory as a variable.
In the filename layout there’s nothing to swap: the folder is content/posts for every language, and switching to French shows the *.fr.md files in it instead of the unsuffixed ones. A new French post is created as content/posts/<name>.fr.md.
Two edge cases. A section whose folder doesn’t start with any language’s contentDir (say content/pages on a site using content/en and content/fr) is left alone and shows the same files in every language; Emet treats those entries as belonging to the default language. And if you really do need different fields per language, you can declare separate sections with explicit folders for each, at the cost of maintaining them twice.
What you see in the CMS#
With more than one language configured (and only then), a globe icon and a language dropdown appear in the header. Changing it switches every section’s entry list to that language and resets the list’s status filter.
In the entry list, cards for entries that exist in other languages show small uppercase badges, FR, DE and so on, one per existing translation. Click one to open that translation. The badge for the language you currently have open is highlighted. Emet works this out by listing each language’s folder once per section (or, in the filename layout, the section’s one folder) and caching the result, so it’s a handful of requests rather than one per entry. Entries with a translationKey additionally have the other languages’ folders read once so a differently named translation with the same key is found; that index is cached with the listing.
In the editor, the Fields header shows the language of the open entry, and the card menu on a multilingual site grows a Translate action. Creating translations, working side by side with the source and doing it in bulk are covered in Translation workflow.
Limitations#
- In the filename layout the default language’s files must have no suffix (
post.md, notpost.en.md). Hugo’s own default is the same. translationKeymatches are looked for only in the corresponding directory of each other language, not across the whole site.- Media is shared. There’s one
mediafolder, so an image uploaded while editing French is available to English too. This is usually what you want. - Language-specific site parameters (
[languages.fr.params]) are Hugo’s business, not Emet’s. Edit them in the Hugo config editor or by hand. - Renaming an entry’s file renames only that language’s copy and so detaches it from its translations, unless the entry has a
translationKey. Emet warns about this in the frontmatter panel when it applies. - An absolute
urlin frontmatter is re-prefixed with the target language code when you create a translation (/en/about/becomes/fr/about/), since Hugo doesn’t do that for you. Relative URLs, slugs and aliases are left alone because Hugo handles those.