Skip to content

Releases and OTA

An export is a file you commit. A release is a snapshot your app downloads. Releases are how you change a translation without shipping a new build.

From the project’s Releases tab, or the GraphQL createRelease mutation. A release takes the approved translations of the project as they stand right now and freezes them.

FieldMeaning
versionAssigned automatically, incrementing per project
name, descriptionYours, to say what changed
splitByFilesWhether the bundle is grouped by file — see below
languagesWhich languages ended up in the bundle
segmentCountHow many keys the fullest language carries

Only approved translations go in. A language nobody has finished appears with the keys that are done and nothing else, so a release is never blocked by an incomplete language — but it is worth checking progress before you cut one.

By default a bundle is a flat map of key to value, one map per language, with the source language carrying the source texts:

json
{
"en": { "home.title": "Hello" },
"de": { "home.title": "Hallo" }
}

With splitByFiles, the keys are grouped by the file they came from:

json
{
"en": { "5f1c…": { "home.title": "Hello" } },
"de": { "5f1c…": { "home.title": "Hallo" } }
}

Use the flat shape when your app loads one dictionary per language, and the split shape when it loads a namespace at a time.

A segment with plural forms carries its whole form map as the value, so plurals survive a release intact.

A key is unique within its file, not within the project, so two files can both have title. In a flat bundle such a key can only appear once, so it is taken from the file that comes last in path and name order, and every language comes from that same file: if that file has no approved translation for a language, the key is absent from that language even when another file has one. The release records this in duplicateKeys: the key, the files it lives in, and usedFrom, the last file the value was taken from. The list comes back with the release and is shown in the UI.

That report is worth reading the first time you release a project split across many files. Either rename the colliding keys, or switch the release to splitByFiles: in a bundle split by files each file keeps its own value, and duplicateKeys is empty.

Every project has one distribution: a permanent access key that serves its releases over HTTP with no authentication.

Find it under Releases → Distribution, or with the GraphQL releaseDistribution query. If the key leaks, regenerate it — the old key stops working immediately, and every client will need the new one.

GET https://api.ownlate.com/public/v1/ota/{accessKey}/manifest
GET https://api.ownlate.com/public/v1/ota/{accessKey}/bundles
GET https://api.ownlate.com/public/v1/ota/{accessKey}/bundles/{language}

The manifest is small and cheap to poll:

json
{ "version": 7, "languages": ["en", "de", "fr"], "publishedAt": "2026-08-20T09:31:00Z" }

The bundle endpoints return the translations, either for all languages or for one:

json
{
"version": 7,
"language": "de",
"splitByFiles": false,
"publishedAt": "2026-08-20T09:31:00Z",
"translations": { "home.title": "Hallo" }
}

All three serve the latest release by default. Add ?version=5 to pin an older one — useful when an old app build expects keys the current release no longer has.

Responses carry an ETag derived from the version and publication time. Send If-None-Match and an unchanged bundle answers 304 Not Modified with no body. Bundles are compressed when the client says it accepts it, and the compressed form is cached too.

A sensible client asks for the manifest on start-up, compares the version to what it has stored, and downloads the bundle only when it differs.