Emet’s translation workflow is built around copies: Translate on an entry card starts a translation from a copy of the source (or a blank archetype), and Create translations copies entries in bulk, each in the right place for a human to translate. No AI is involved in either.
The AI side is two separate things. You can translate a selection in place from the inline actions menu, and you can translate a whole entry, frontmatter and body, from the toolbar. Both need at least two languages in site.yaml and a provider that’s ready.
Translating a selection#
Select some text in the visual editor, open the inline actions menu (the AI button on the selection toolbar, or Ctrl+J) and pick Translate, then a language. The list leaves out the language the entry is already in. The selection is replaced with the translation. That’s the whole feature: it doesn’t touch any other file. It’s handy for a quoted paragraph, or for fixing one sentence in a translation you’ve otherwise done yourself.
Translating a whole entry#
Open the toolbar AI menu and pick Translate. The modal has two fields:
- Target Language lists the other configured languages under “Translate into”, and the entry’s own language separately under “This entry” as Translate in place. It preselects the first language this entry has no translation in yet.
- Additional Instructions is optional free text for the prompt: “use the formal register”, “keep product names in English”, that sort of thing.
Nothing in this flow writes to the repository. A machine translation you haven’t read has no business going live, so the result always lands in the editor as an unsaved change, and only your save commits it.
If you pick another language, Emet works out the target path the same way the copy workflow does and checks whether it exists:
- If it doesn’t, the translation opens as a new, unsaved entry at that path, with the source entry in a panel on the left so you can check it against the original. Saving creates the file. Closing it without saving creates nothing.
- If it does, the button reads Translate & Replace (unsaved). Emet loads the saved translation as the baseline, applies the new translation over it as an unsaved edit, and opens it the same way. Discard takes you back to the saved version; Save commits the new one.
If the entry you’re translating from has unsaved changes, you’re asked to save or discard them first, and then the translation opens.
If you pick Translate in place, the button reads Translate & Review. This is the route for an entry that’s in the wrong language, say an English draft sitting in content/fr/. When the model answers, a review dialog shows a line diff of the whole file, frontmatter and body, against what’s in the editor now. Apply translation replaces the editor content (still unsaved); Cancel leaves everything as it was.
When the translation is cut off#
Every model has a ceiling on how much it will write in one reply. Emet asks for about double what the source would take, up to 16k tokens, but a very long entry can still come back incomplete. Rather than open half a page, Emet notices (from a missing end marker or an unfinished reply) and says “The translation was cut off before it finished; try a shorter entry or split it into parts”. Nothing is applied or opened. An entry too long to fit at all is refused before the request goes out. Either way, split the entry, or translate it in place a part at a time.
What gets translated#
The model receives two things: the entry’s body, and the subset of frontmatter that is text. Emet works out that subset itself rather than asking the model to decide:
- Strings and lists of strings are sent. Nested objects are walked, so
image.alt,seo.titleorgallery.0.captionare translated as well astitleanddescription. - Keys that are never prose are left out:
date,lastmod,slug,url,draft,weight,layout,type,aliases,translationKey,author, and any key ending inurl,path,id,color,icon,src,href,image,fileand the like. Hugo’s structural keys (menu,cascade,outputs,resources) are skipped whole. - Values that look like a URL, a path, an email address, a date or a number are left out whatever their key.
The model answers with JSON, and Emet merges it back by key: each translated value goes back where it came from, as long as it still has the same shape (a string for a string, a list of strings for a list). Everything else in the frontmatter, including its order, its types and the keys that weren’t sent, is kept from the original. An absolute url is re-prefixed for the target language afterwards, as in the copy workflow, and translationKey is carried over unchanged.
Because the body travels as a JSON string, code fences inside it don’t confuse anything. Replies in the older YAML-plus-Markdown format are still accepted and parsed properly, nested maps included. Your ai.brandVoice goes into the system prompt as usual.
From the chat#
The chat panel has a create_translation tool that takes a language code. It’s the toolbar modal by another route: it translates the frontmatter text and body and opens the result in the editor as an unsaved entry, either new or applied over an existing translation. It doesn’t write any file, and the model is told to ask you to review and save. With translateContent: false it opens an untranslated copy instead, like the Translate button on an entry card. A cut-off reply fails the tool call with the same message as the modal.
Configuration#
ai:
enabled: true
features:
translation: truetranslation: false removes Translate from both the inline menu and the toolbar menu. The chat tool is unaffected. Languages are configured in site.yaml; see Languages.