Versions and releases
Between a key in your code and the value your app fetches at runtime, there is a status, a release and a bundle. This page covers all three, and above all what ships and what does not.
The statuses a value moves through
Section titled “The statuses a value moves through”Publishing starts at translated: anything below it never leaves the project.
| Status | What it means |
|---|---|
missing | No value for this language. The default state of an untranslated target. |
draft | A value exists but is not validated. Agent and AI writes land here, below the publish threshold. |
translated | A complete value. This is the threshold: bundles publish from here up. |
reviewed | The value has been reviewed. |
approved | The value is signed off. A human-typed value lands here directly (author = approver). |
rejected | The value was rejected. Never published, a terminal out-of-band state. |
A pure draft cannot be approved in one step: approve_translation returns not_approvable, it must reach translated first. The default export scope (XLIFF, CSV) is likewise translated,reviewed,approved and excludes draft unless you ask for it.
What reaches the CDN
Section titled “What reaches the CDN”A release (publish_cdn) builds one bundle per language and namespace. Only publishable statuses go in, and only non-empty values.
- Included:
translated,reviewed,approved, that is, everything from the threshold up. - Excluded:
draft,missing,rejected. An empty value is skipped too, even if its status would qualify.
The empty-bundle guard
Section titled “The empty-bundle guard”This is the part that prevents the most expensive accident: emptying a live namespace without noticing.
If no key in a bundle is publishable, publish_cdn skips that bundle. Nothing is overwritten, skipped: true, and an entry lands in warnings[] with {language_code, namespace, key_count, included_count, excluded_count, skipped, reason}.
The reason spells it out:
namespace '{ns}' / '{lang}': 0 of {N} keys are publishable: all are below 'translated' status (e.g. draft). The bundle is EMPTY: nothing will be served. Publish was SKIPPED for this bundle; set allow_empty=true to force an empty bundle.
Pass allow_empty: true to publish the empty bundle anyway, which is always reported. In short: you cannot accidentally empty a live namespace by publishing a pile of draft keys.
Before publishing: validate
Section titled “Before publishing: validate”Run validate_translations before publishing, to catch problems while they are still cheap to fix. With no payload it audits the stored values; pass a payload to inspect a submission.
It returns quality_issues[] (and counts.quality_issues), each entry being {key, type, detail}, where type is same_as_source, wrong_language or looks_like_raw_key. The detail says, for instance, "value is identical to the source, likely not translated" or "value '{v}' looks like a key path/identifier, not text".
It is advisory: it flags, it never rewrites your values.
Confirming a language is live
Section titled “Confirming a language is live”Once published there are two distinct questions, with two different checks. Conflating them is the most common source of error.
Is it published?
Section titled “Is it published?”distribution_info, read-only over MCP and free of credits, returns the CDN base and the bundle URL template, the production version, which language/namespace pairs are published, and the gaps. It is exactly what client SDKs can fetch.
Does it resolve?
Section titled “Does it resolve?”sonenta pull --language <code> downloads the resolved translations for that language. That is concrete proof the values fetch, not merely that they exist.
Subscribed SDKs also receive a translations_published event and refresh in place.
Two preflight helpers worth knowing: sonenta doctor checks that MCP is wired, reachable and scoped mcp:*, and sonenta ci assesses the i18n impact of a pull request.
- CI: catch i18n problems on every pull request
- Key nesting: flat or nested keys, and the dotted-key trap
- CLI reference:
sonenta releases