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.
Creating a release
Section titled “Creating a release”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.
| Field | Meaning |
|---|---|
version | Assigned automatically, incrementing per project |
name, description | Yours, to say what changed |
splitByFiles | Whether the bundle is grouped by file — see below |
languages | Which languages ended up in the bundle |
segmentCount | How 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.
Bundle shape
Section titled “Bundle shape”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.
Duplicate keys
Section titled “Duplicate keys”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.
OTA distribution
Section titled “OTA distribution”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.
The endpoints
Section titled “The endpoints”GET https://api.ownlate.com/public/v1/ota/{accessKey}/manifestGET https://api.ownlate.com/public/v1/ota/{accessKey}/bundlesGET 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.
Caching
Section titled “Caching”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.