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.

View as Markdown

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-US and de-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:

PrecedenceChain for German on a non-base device family
PixelsDevice family dominates, strictlythat family's German cell → that family's base cell → the base cell
WordsLocale dominates, over the full latticethat 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.

Stage four: a screenshot bundle for the weather app, shaped for fastlane with one folder per locale and numbered screenshots.
One zip, one folder per locale, uploaded by your own fastlane.