Workflows
Ship every locale
Caption files in your repo, locale variants, explicit readiness for missing or inherited cells, and one screenshot bundle for every locale.
Tidebook ships in en-US and de-DE. One bundle carries both, fastlane deliver uploads both in a
single run, and the German panels say German words — but only because you passed them. Nothing about
a locale code makes copy appear.
Render the approved Tidebook strip in
en-USandde-DE, then package both locale folders in one screenshot bundle.
Expected result: both locale previews use their own words, and the final zip contains one numbered screenshot folder for each locale.
#Caption files in your repo
ShotOps never stores or looks up your shipping copy. The words are per-render input and they come from the caller, which in practice means a file in your own repository, one per locale, keyed by panel ordinal — 0-based, in strip order, so the file survives every re-render.
// captions.de-DE.json
{
"0": { "headline": "Die Gezeiten kennen, bevor du losfährst" },
"1": { "headline": "Sieben Tage Swell" },
"2": { "headline": "Jeder Spot, den du surfst" },
"3": { "headline": "Dein eigenes Session-Log" },
"4": [{ "text": "NEU" }, { "text": "Beim richtigen Swell aufwachen" }]
}
A panel's entry is either one caption (headline, optional subtitle) or an array of layers stacked
in order — use the array when a panel needs more than a headline and a subtitle.
To render a locale, read the file, map it onto style.captions[] in the same order (index i →
panel i, null for a panel with no caption), and pass locale alongside so the render and the
bundle are labelled and routed consistently.
#Locale screenshot variants
German captions over English pixels is a legitimate place to be for a week. When you do have
localized screens, any screenshots entry can carry per-locale variants instead of one image:
{
"screenshots": [
{ "locales": { "en-US": { "ref": "…01_today_en…" }, "de-DE": { "ref": "…01_today_de…" } } },
{ "ref": "…02_forecast…" },
{ "ref": "…03_spots…" },
{ "ref": "…04_log…" },
{ "locales": { "en-US": { "ref": "…05_alerts_en…" }, "de-DE": { "ref": "…05_alerts_de…" } } }
]
}
A plain entry serves every locale. render({ locale: "de-DE", … }) picks the de-DE variant of
each entry that has one, and echoes the declared locales back as screenshotLocales.
On the hosted door, request_screenshot_upload({ count, names, family?, locale? }) returns a
variant on every slot. Pass it through beside the ref when you render a project: it names
the device-family and locale cell the upload overrides, not a bookkeeping tag. The name chooses
which shot; the variant chooses which of that shot's cells.
#What happens when a cell is missing
A missing coordinate first tries the documented fallback chain. When a fallback exists, nothing is skipped and the selected source is specific enough to predict.
For an entry you pass on the wire, resolution is: the requested locale's variant, else the base
locale's variant (en-US unless the project says otherwise), else the first variant declared. An
entry whose locales map is empty is a caller error and is refused with a message saying so.
For a project's stored screenshots, cells inherit along two different precedence orders, because pixels and words fail differently:
| Precedence | Chain for German on a non-base device family | |
|---|---|---|
| Pixels | Device family dominates, strictly | that family's German cell → that family's base cell → the base cell |
| Words | Locale dominates, over the full lattice | that family's German → any-family German → that family's base → the base cell |
The strictness on the pixel side is the point: the chain never reaches another family's German screenshots, because iPhone pixels under an Android listing are a store-policy problem rather than an aesthetic one. On the caption side, generic German copy beats device-specific English copy every time.
Both read_project and a project render report the outcome per cell rather than leaving you to
guess. read_project returns screenshots.cells[] — for each shot × family × locale, the variant
asked for, the resolvedAt cell actually used, and an inherited array naming which axes fell back.
A project render reports the same resolvedAt and inherited per shot in its shots array. A
doubly-inherited cell — reflowed pixels under untranslated captions — is the panel most likely to ship
wrong, and it is exactly the one those fields name.
If the chain reaches no screenshot at all, preview can still show the incomplete panel and reports the gap. A render can remain incomplete while the strip is being assembled; a bundle refuses an empty panel source before it creates the store artifact. Locale or device fallback is different: final output requires the authenticated owner to accept that exact readiness finding, or to supply the missing cell.
#Render per locale
Put each locale's words into the project, then preview that locale:
{
"project": "Tidebook 2.4 launch",
"captions": [
{ "frame": 1, "locale": "de-DE", "text": "Kenne die Tide, bevor du losgehst" },
{ "frame": 2, "locale": "de-DE", "text": "Sieben Tage Dünung" }
]
}
That is an edit_project call; a locale the project does not have yet is added. It
saves only once every caption clears the devices, and says which frame to fix when one cannot. Then
render({ "project": "Tidebook 2.4 launch", "locale": "de-DE" }) previews the German strip.
Preview each locale first. A German headline is routinely a third longer than its English original,
and the length is what decides whether the panel wants standard or bleed.
#One bundle, every locale
One call renders every locale with its own words out of the project and zips them:
{
"project": "Tidebook 2.4 launch",
"output": "bundle",
"bundleId": "com.tidebook.app",
"locales": ["en-US", "de-DE"]
}
The zip carries fastlane/screenshots/en-US/ and fastlane/screenshots/de-DE/, each with its panels
numbered in strip order, and fastlane deliver uploads both in one run. It is one full-resolution
render per locale, per device the project targets, so budget the time. A full-resolution Hosted call
may return an operation id first; poll it with production_operation and use
action: "result" after success.
Supplied screenshots with locales work too, but every locale then carries the same
style.captions. For localized words, save the project first.