Skip to content

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.

Publishing starts at translated: anything below it never leaves the project.

StatusWhat it means
missingNo value for this language. The default state of an untranslated target.
draftA value exists but is not validated. Agent and AI writes land here, below the publish threshold.
translatedA complete value. This is the threshold: bundles publish from here up.
reviewedThe value has been reviewed.
approvedThe value is signed off. A human-typed value lands here directly (author = approver).
rejectedThe 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.

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.

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.

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.

Once published there are two distinct questions, with two different checks. Conflating them is the most common source of error.

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.

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.