Translation workflow

A translation in Emet is an ordinary entry that happens to live in another language’s directory. There’s no special translation state and nothing to approve; the workflow is just a quicker way of getting a correctly placed copy to start from, with the original beside you while you work.

Creating a translation#

  1. In the entry list, open the card’s More actions menu (the three dots) and click Translate. The action only appears on sites with more than one language.
  2. In the Create Translation dialog, pick the Target Language. The dropdown lists only languages this entry doesn’t have a translation in yet; if it has them all, the dialog says so and there’s nothing to do.
  3. Choose what to Start from:
    • Copy of source (the default) duplicates the whole source file, frontmatter and body, so you translate in place with the original text in front of you.
    • Blank from archetype gives you a fresh entry from the section’s archetype (the same template a new entry gets), keeping only the source’s title and its translationKey if it has one. Pick this when the body is going to be rewritten rather than translated line by line.
  4. Leave Open side-by-side with source ticked if you want the original visible while you translate. It’s on by default.
  5. Click Start Translation.

The translation goes to the same relative path under the target language’s directory, so content/en/posts/hello.md becomes content/fr/posts/hello.md. The whole path below the language directory is kept, subfolders and page bundles included: content/en/posts/2024/hello.md becomes content/fr/posts/2024/hello.md, and a bundle’s content/en/posts/hello/index.md becomes content/fr/posts/hello/index.md. On a site using translation by filename the copy stays in the same directory and gets the target language’s suffix instead: content/posts/hello.md becomes content/posts/hello.fr.md, and content/posts/hello/index.de.md translated into the default language becomes content/posts/hello/index.md.

Nothing is committed at this point. The new entry opens in the editor as a pending file: it shows in the target language’s list with the unsaved-changes marker, but it doesn’t exist in the repository until you save it. That matters on a site that deploys on every push, because an untranslated copy committed straight away would go live at the French URL showing the English text. Save when the page is ready (or at least a draft you’re happy to have in the repository) and the first save creates the file, with a “Create hello” commit. Autosave never makes that first commit for you: a pending entry is only written by an explicit save, so you can leave the tab open as long as you like. Closing the entry without saving discards it, and the usual unsaved-changes prompt asks first.

Three details worth knowing. The copy is a copy: no machine translation happens here. You can translate from any language into any other, not only from the default one, so a German post written first can be the source for its English version. And if the source has a translationKey in its frontmatter, the copy keeps it, so the two stay linked even if you later rename one of them; Hugo links pages by that key regardless of their filenames, and so does Emet (it looks for a matching key in the corresponding directory of each other language before falling back to the matching path).

If the target file already exists (someone else created it a moment ago, say) you get “Translation already exists” rather than an overwritten file. If the file appears in the repository between starting the translation and saving it, the save is refused and the editor shows the remote version so you can decide what to keep.

Working side by side#

With the side-by-side option on, the source entry appears in an amber panel on the left of the editor. Its header reads Source followed by the source language’s name and code. Below that are the source’s Fields, in the same order and with the same labels as the form on the right (every visible field that has a value, plus any extra frontmatter keys), and its Body. The body is shown rendered by default; the Rendered / Raw switch in the Body header flips it to the raw Markdown when you need to see exactly what was written. Both sections collapse, and the refresh icon reloads the source if someone’s edited it meanwhile. Drag the panel’s right edge to make it wider or narrower; the width is remembered. It’s read-only; edits go in the main editor on the right.

The panel is a preference, not a one-off: once it’s on, it stays on as you move between entries, and it always shows the source for the entry you’re currently editing. Open a German post and it shows the English original; open an unrelated English post and it shows that post’s German translation if there is one, or disappears until you open an entry that has one. Which language counts as “the source” is the language you translated from, falling back to the site’s default language, and you can change it from the translation menu (see below).

The close button in the panel’s header turns it off. To bring it back, open the translation menu in the fields bar and choose Show source.

Switching between translations#

In the editor, the fields bar has a translation menu: a globe icon with the current entry’s language code. Open it to see every language this entry exists in, with the one you’re editing marked Current. Click another language to open that translation. Below the list, Create new translation… opens the same Create Translation dialog as the entry list, pre-filled with the entry you have open; the dropdown only offers languages the entry doesn’t have yet, and the menu’s language list updates as soon as the copy is created.

The menu’s Source panel section has the Show source toggle described above. When the entry exists in more than two languages, the languages are listed underneath it so you can pick which one the panel shows; picking one also turns the panel on.

Entry cards show a badge for each language the entry exists in too; click a badge to open that translation. Languages the entry is missing appear as dashed, greyed-out badges, and clicking one of those opens the Create Translation dialog with that language already selected. Alternatively, pick the language in the header switcher and open the entry from the list like any other.

Opening a translation is subject to the same rules as opening any entry. If the one you have open has unsaved changes, you’re asked to save or discard them first; and if the translation is already open with edits of its own, clicking its badge switches to it rather than reloading it over your work.

The translation you have open is highlighted in the list even when the list is showing a different language, so you can always find your way back.

Translating several entries at once#

Turn on selection mode in the entry list, select the entries, and choose Create translations from the bulk actions. The dialog asks for a target language and tells you what will happen before you commit: “Will create 4 translations · 2 already exist (skipped)”. Unlike the single-entry workflow above, this one does commit the copies straight away, all in a single commit (“Create 4 fr translations”). To keep those untranslated copies off the live site, leave Mark copies as drafts ticked: each copy gets draft: true and the commit carries the site’s skip-CI marker, so the deploy isn’t rebuilt. Entries with unsaved changes are skipped too, and nothing is opened in the editor afterwards, so this is the tool for bootstrapping a new language across a whole section rather than for translating anything. See Bulk actions for the mechanics.

Keeping translations up to date#

Two questions come up once a site has been multilingual for a while: which entries still lack a translation, and which translations have fallen behind their source. The entry list answers both.

What’s missing#

The status filter on a multilingual site has a Missing in option per language, and the line under the list header (DE 9/12 · FR 12/12) shows at a glance how complete each language is for the entries you’re looking at; click a language there to apply its filter. Untranslated anywhere finds the entries that exist in no other language at all. Combine a Missing in filter with selection mode and Create translations to bootstrap the gaps in one commit: the bulk dialog preselects the filtered language. See The entry list for the filters and Bulk actions for the bulk step.

What’s outdated#

After the translation statuses have loaded, Emet checks, in the background, whether each entry’s default-language source has changed since the translation was last edited. The result shows up in two places. In the default language’s list, a translation badge turns amber with a small history icon when that translation is older than the source; hover it to see “Source changed on after this translation was last edited”. In another language’s list, the entry itself gets an amber Outdated chip. The Translation outdated filter shows just those entries.

The automatic check reads only frontmatter, so it costs at most one file read per translation and nothing per entry on a cached list:

  1. lastmod in frontmatter. When both files have one, that decides it. If you keep lastmod up to date in your frontmatter, this is all the check ever needs.
  2. date in frontmatter. Otherwise date is compared when both files have one. It’s a weak signal, since a translation created as a copy keeps the source’s date, so only a source whose date was bumped counts as newer.

On GitHub-hosted sites there is a deeper, on-demand check through commit history: the date of the latest commit touching each file is compared for every pair where either file lacks a lastmod. It costs one request per such file, so it never runs by itself. Start it with Check freshness in the line under the list header, or just pick the Translation outdated filter, which runs it for you; while it runs the summary line shows how many files have been looked up. The lookups are limited to a few at a time and cached per file version, so revisiting a list, or refreshing it, does not repeat them, and once every pair that needed it has been looked up the line reads Freshness checked. Local and desktop sites have no commit history, so they stop at the frontmatter signals.

Equal dates never count as outdated, and if neither file has a comparable date the entry is simply not flagged. The check only runs on sites with more than one language, and only after the list and its translation statuses have finished loading; the Refresh button in the list header clears its caches, including the cached commit dates, and runs the automatic pass again.

Translating with AI#

If you’ve configured an AI provider, the editor’s AI menu has a Translate action that translates the frontmatter and body of the open entry into another language and can write the result straight into the target file. That’s a different workflow from the copy-and-edit one above, and it’s described in AI translation.