---
title: "Ship every locale"
description: "Caption files in your repo, locale variants, explicit readiness for missing or inherited cells, and one screenshot bundle for every locale."
canonical: "https://shotops.dev/docs/mcp/workflows/localization"
last_updated: "2026-09-07"
shotops_mcp_version: "0.9.6"
result_contract_version: "2.0.0"
---
# 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-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.

> Note A saved project is the exception worth knowing about. The record holds the designer's per-locale words, [`read_project`](https://shotops.dev/docs/mcp/reference/read_project) returns them in `project_file.captionText`, and `emit_bundle({ project, locales })` renders each locale with that locale's own words out of the record. Studio's captions are still a visual preview: your `captions.<locale>.json` is what ships.

## 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`](https://shotops.dev/docs/mcp/reference/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.

> A locale code does not translate anything `locale` labels the render, picks screenshot variants and routes the bundle folder. It does not select caption text on [`render_strip`](https://shotops.dev/docs/mcp/reference/render_strip), and it does not translate. A German render with English `style.captions` produces English headlines over whatever pixels resolved, reports success, and looks entirely correct in the response.

## 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`](https://shotops.dev/docs/mcp/reference/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.](https://shotops.dev/studio/docs/media/assets/spine-bundle.svg)

_One zip, one folder per locale, uploaded by your own fastlane._
