Looking for the editor’s own language — translating “Publish” into German,
or getting AI responses back in the user’s language? That is
Internationalization. This page is about content that exists in more
than one language.
The mismatch
Avocado’s content model has no locale axis:title, one slug, and props are flat values. A CMS with field-level
i18n stores the opposite shape — one document per page, and every translatable
field is an object:
The shape that works: one page per language
An Avocado page is one language of one page. Fifteen tri-lingual documents become forty-fivePageDocs. /de/prices and /fr/prix are two Avocado pages
backed by the same CMS document.
- Read projects each localised field down to the plain string that language renders. The editor then sees ordinary monolingual pages and the planner never meets a locale object.
- Write merges the edited value back into that language’s slot only.
- The language rides along in the slug, and in whatever context your publisher
carries (
diffPage’sctxis generic — put alangin it).
/de/prices is a page identity even on a
site that serves German at the root and answers 404 for that path. Put the URL a
visitor actually uses in meta.path;
absent means “the slug is the path”. Without it, an agent handed the slug and
told to open it lands on a 404 and cannot tell it was given an id.
Keep the source block. Carry the untouched CMS block along in the props (a
__source or _src key) so publish can diff against what it was projected
from, rather than against a projection of a projection. Edits keep props the
schema does not declare, and a leading underscore marks the key internal, so
the panel and the planner never show it. A duplicated block is the exception:
duplicate_block drops top-level _ props from the copy, because the copy has
no source document of its own.
Four rules, or the round trip corrupts content
All four were found by round-trip checks on real tri-lingual sites, not by reasoning about the model. A projection without them looks correct and silently rewrites the dataset. Three of the four were written into an integration before being caught, by someone who had already read this page.Build the round-trip check first
Before wiring the editor to anything, write a script that projects every document in every language, rehydrates it with no edits, and asserts the result is byte-identical to the source:Declare the site’s languages
Return them fromgetSiteConfig in the editor API handler (or the Astro content
module), as the CMS reports them — even when there is only one:
locales and defaultLocale to the orchestrator when it
loads the site, and they change what a translation request does. A page’s
language is a property of which page is open, so “translate this to Russian” on
an en-US page cannot mean “add a Russian translation” — it can only mean
“replace the English text with Russian”, which a publish then writes into the
en-US field. With the site’s languages known, the chat answers that request
with a question instead:
A site that declares nothing still gets the check when its slugs carry language
prefixes (/post beside /de-DE/post); a site with neither translates in place,
as it always has. Declaring is still the reliable form, and the only one that
covers a single-language site — which is exactly where the overwrite happened:
a Contentful space with one locale, whose English description came back in
Russian.
Tell the user what a publish skipped
Some edits cannot land. A field the CMS does not localize has one value, owned by the default locale, so an edit to it on thede-DE page has nowhere to go.
Skip it — and say so. Return it from onPublish in unsupported:
describeUnsupported() from @avocadostudio-ai/site-sdk/publish
formats a field diff’s unsupported entries this way.
What is not solved
- The editor pairs sibling pages by their slugs:
/postand/de-DE/postare one entry in the page picker, with a language switch beside it that moves between them. A page whose translation has a translated slug (/de/veranstaltungenfor/events) has nothing to pair it by, so switching language on it lands on that language’s home page. duplicate_pageacross languages does what it says — it copies a page, not a translation.
Rich text
Rich text can round-trip if the CMS content was authored through a parser you can invert. Write the inverse serialiser and keep the original tree whenever re-serialising returns it unchanged; that makes rich text lossless unless it was actually edited. Where no such parser exists, the usual lossy flatten applies — see rich text for the converters in@avocadostudio-ai/richtext that pivot through a ProseMirror document, and
prefer a document-valued prop over a markdown string when the CMS has real rich
text.