Translations
One page per language, linked together, with a clear fallback when a language is missing.
4 min read
Languages of a brand
A brand serves a list of locales, up to 20, and one of them is the default. A locale looks like en or pt-BR. The default locale is served without a prefix (/pricing) and every other locale is prefixed (/en/pricing). Platform admins set the list when they create the brand.
A translation is an ordinary entry in another language. Paths are per language, so /tentang and /en/about can be the same page.
Translation groups
Entries that are the same content in different languages share a translation_group_id. A new entry starts its own group. Creating a translation adds an entry to the group of the entry you translate from.
/v1/manage/entries/{entry}/translationsAny editor can do this. The body is:
{ "locale": "en", "path": "/about", "title": "About us" }localeis required, must be a locale of the brand and must not exist in the group yet.pathis required for a page. For a post it is optional, and the server builds one from the permalink structure.titleis optional and defaults to the source title.
The API copies the newest revision of the source into a new draft entry and remembers which revision it came from (source_revision_id). The answer is 201 with the new entry. Nothing goes live until a publisher publishes it.
/v1/manage/entries/{entry}/translationsThe answer has one row per brand locale, existing or not:
{
"data": [
{ "locale": "id", "entry_id": "01m...", "path": "/tentang", "status": "published", "outdated": false },
{ "locale": "en", "entry_id": "01n...", "path": "/about", "status": "draft", "outdated": true }
]
}A locale with no entry has entry_id, path and status set to null.
To list content by group, use GET /v1/manage/entries?group=translations. You get one item per group, and missing_locale=en narrows it to groups that have no English entry yet. Each item carries a translations array.
Outdated translations
A translation is outdated when the default language entry has a newer revision than the one the translation was made from. The default language entry itself is never outdated. The flag shows in the translation list and in the dashboard health report, so a team can see which pages need another look.
When you publish a group in bulk with include_translations: true, every member needs a reviewed latest_revision_id in revisions. A member that changed since it was reviewed is skipped instead of published.
What a visitor gets
Public delivery never mixes languages by accident.
/v1/public/page?path=/about&locale=enIf no live entry exists in the requested locale, the API serves the default language entry and says so:
{ "data": { "locale": "id", "path": "/tentang", "fallback": true } }Your site decides whether to show it, redirect, or add a banner. The fallback applies to template overrides as well. A brand can turn it off with the locale_fallback setting set to false (admin). Then a missing translation is a plain 404.
hreflang links
The page payload lists the other live languages of the same page, which is what a site needs for hreflang:
{ "data": { "translations": [ { "locale": "id", "path": "/tentang" } ] } }The list leaves out the current entry and any language that is not published. The front page has the path / in every language. GET /v1/public/sitemap-urls carries the same alternates for the sitemap.
UI strings
Short texts that belong to the site and not to a page, such as button labels and error messages, live in a separate store. Each string has a namespace, a key and a locale.
/v1/manage/translationsAny member can read. Filter with namespace and locale. The list is paginated by 100.
/v1/manage/translationsA publisher saves strings in a batch of 1 to 500:
{
"translations": [
{ "namespace": "common", "key": "readMore", "locale": "id", "value": "Selengkapnya" },
{ "namespace": "common", "key": "readMore", "locale": "en", "value": "Read more" }
]
}A value is up to 5000 characters, namespace up to 64 and key up to 255, and the locale must belong to the brand. A save audits the keys (not the values) and queues the purge webhook as strings.updated. DELETE /v1/manage/translations/{translation} removes one string.
Your site reads them from the public API:
/v1/public/stringscurl -s "https://api.atlascms.id/v1/public/strings?locale=en&namespace=common" \
-H "Authorization: Bearer $ATLAS_TOKEN"{ "data": { "locale": "en", "strings": { "common": { "readMore": "Read more" } } } }Keys missing in the requested locale are filled from the default locale. Leave namespace out to get every namespace.
AI translation is a draft
/v1/manage/ai/entries/{entry}/translateSend { "locale": "en", "path": "/about" } (the path is optional). The assistant writes the translation as a draft revision, in the same way a person would. A manual AI translation is never published. A member who can edit the entry reviews the draft, fixes it, and a publisher publishes it through the normal route. See Core concepts for how drafts and publishing differ.
Link existing content
If you imported pages in several languages, link them into groups by path (pages) or slug (posts) from the server:
php artisan atlas:link-translations acme --type=page,post
php artisan atlas:link-translations acme --type=page,post --writeThe first command is a dry run. It reports how many entries it would link, how many are already linked, ambiguous or in conflict. Add --write to apply.
Keep the default language as the source
Translate from the default language entry. Then the outdated flag tells you exactly which translations to refresh after an edit.
Was this page helpful?