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 — and any pre-rendered panels 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_strip({ 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 on render_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 render_project 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.
render_project 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; emit_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
Render each locale on its own, with that locale's words, and keep the panel refs:
{
"locale": "de-DE",
"output": "urls",
"panelPresetId": "r69",
"screenshots": ["…the same five entries…"],
"style": { "layout": "bleed", "captions": ["…die deutschen Worte…"] }
}
Repeat for en-US. Each Local call gives you its panels synchronously. A full-resolution Hosted call
may return an operation id first; poll it with production_operation and use
action: "result" after success to collect the per-panel refs without rerendering.
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
Compose the locales you already rendered into a single zip. Nothing re-renders, so this call returns near-instantly:
{
"bundleId": "com.tidebook.app",
"locales": ["en-US", "de-DE"],
"panels": [
{ "locales": { "en-US": { "ref": "…p1-en…" }, "de-DE": { "ref": "…p1-de…" } } },
{ "locales": { "en-US": { "ref": "…p2-en…" }, "de-DE": { "ref": "…p2-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.
There is a one-call form — emit_bundle with screenshots plus locales — and it works, but it
renders once per locale with the same captions for every locale, and it multiplies the slow
full-resolution render by the number of locales. Prefer the compose flow for real jobs.
The exception is a saved project: emit_bundle({ project, locales }) renders each locale with that
locale's own words out of the record, which makes it the only one-call route to a genuinely
multi-locale bundle. It is still one full-resolution render per locale, per device the project
targets, so budget the time.