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:

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

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

{
"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 — which is the reason to prefer a bundle over a flat file export for them.

In a flat bundle the same key in two files can only appear once. When that happens the release records it: the key, the files it came from, and which file the value was actually taken from — the last one wins. 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 so the collision disappears.

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:

{ "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:

{
"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.