Skip to content
Launch offer: Mac Manager £9.99 first year

Shotlist guide

Captions and composing

Writing captions in a String Catalog, review states, drafting translations with Apple Translation or Claude, composing with templates and layouts, text fitting and overflow, content rules, and creative assets with Apple's safe areas.

Captions and composing

shotlist compose turns raw captures into the screenshots you upload: each capture placed on a canvas with its caption, at the slot’s exact size. It also draws the creative assets: the product page header art and the search results art.

The order of work is:

  1. Write captions in your source language.
  2. Draft the other languages (shotlist captions draft), or write them yourself.
  3. Have someone who reads each language check the drafts, then mark them reviewed (shotlist captions review).
  4. Compose (shotlist compose).
  5. Verify (shotlist verify).

Composing needs a subscription; the captions commands are free. See Subscription.

The captions catalogue

Captions live in Shotlist/Captions.xcstrings, a String Catalog. Xcode’s own editor works on it, so translators and tools that handle String Catalogs can too. shotlist init creates an empty one, with English (en) as its source language.

Keys

Key Required Used for
scene.<id>.caption Yes, for every scene in sequence, in every locale The headline on that scene’s screenshot.
scene.<id>.subtitle No A smaller line below the caption.
creative.header.title Yes, if creative.header is set The header art’s headline.
creative.header.subtitle No A line below it.
creative.search.title No The search art’s headline. Falls back to the search scene’s caption.

<id> is the scene id: scene.library.caption.

Which entry a locale uses

For a locale such as de-DE, Shotlist looks for:

  1. an entry for de-DE exactly;
  2. then one for the language, de;
  3. then, for a regional variant of the source language (such as en-GB or en-US when the source is en), the source entry.

So one German entry covers de-DE, de-AT and de-CH, and an en-GB entry can override en where British wording differs. Norwegian nb and no are treated as the same language.

What it looks like

{
  "sourceLanguage" : "en",
  "strings" : {
    "scene.library.caption" : {
      "extractionState" : "manual",
      "localizations" : {
        "en" : { "stringUnit" : { "state" : "translated", "value" : "Your whole shelf, in one place" } },
        "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Dein ganzes Regal an einem Ort" } }
      }
    },
    "scene.library.subtitle" : {
      "extractionState" : "manual",
      "localizations" : {
        "en" : { "stringUnit" : { "state" : "translated", "value" : "Track what you're reading and what's next" } }
      }
    }
  },
  "version" : "1.0"
}

Edit it in Xcode (File › Open, then choose the file), or with any JSON editor. Don’t add it to your app target: it’s for Shotlist, not the app.

A caption can contain line breaks. Use them to control where a long caption wraps.

Review states

Each entry has a state:

State in the file Shotlist calls it Meaning
translated Reviewed Written or checked by a person. Ready to publish.
needs_review Awaiting review A machine draft nobody has checked. verify and publish refuse it.
new Awaiting review Xcode’s state for an entry that hasn’t been translated or checked. Treated like needs_review.

Captions you write yourself, in Xcode or by hand, are normally translated. Every machine draft is saved as needs_review, and stays that way until someone marks it reviewed.

shotlist captions status

shotlist captions status
✓ ready    en-GB   4 reviewed · 0 awaiting review · 0 missing
! review   de-DE   0 reviewed · 4 awaiting review · 0 missing
✗ missing  fr-FR   0 reviewed · 0 awaiting review · 4 missing

It counts each scene’s caption (not subtitles or creative titles) for every locale in shotlist.json. It exits 1 until every locale is ready, so CI can gate on it. shotlist captions on its own runs status. With --json it prints, for each locale, the counts and the missing keys.

Drafting translations

shotlist captions draft

draft fills every missing caption, subtitle and creative title in every locale whose language differs from the catalogue’s source language. It translates from the source entries, so write those first. It never changes an entry that already exists.

For each locale it uses:

  1. Apple’s on-device Translation, when the language pair is installed on this Mac. Add languages in System Settings › General › Language & Region › Translation Languages. Nothing leaves the Mac.
  2. Otherwise, the Claude API, with your own credential: the ANTHROPIC_API_KEY environment variable, or a profile from ant auth login. The source captions, the target language and your --context are sent to Anthropic’s API.

If neither is available for a locale, it’s skipped, and draft says why.

drafted  de-DE: 8 captions with Apple Translation (on device), marked needs_review
skipped  fr-FR: no on-device en→fr translation is installed, and no Claude API credential (ANTHROPIC_API_KEY or `ant auth login`)
Review the drafts in Xcode (Captions.xcstrings), then `shotlist captions review --locales <locale>`.
Option Effect
--locales <list> Only these locales.
--context "<one line>" A line about the app, to help the translation: --context "A reading tracker for book clubs". Used by the Claude API.
--json Print the drafted counts, the engine per locale and the skipped locales as JSON.

Drafts are saved under the language code (de, not de-DE), so one draft serves every region of a language.

The Claude API is asked to keep each caption short and natural for its market, to keep product and brand names and line breaks, and not to add prices, URLs or claims the original doesn’t make. Read every draft anyway: that’s what the review step is for.

In the Mac app, Draft Missing Captions in the Compose inspector does the same.

Marking drafts reviewed

When someone who reads a language has checked its drafts (and corrected them in the catalogue where needed):

shotlist captions review --locales de-DE
reviewed  de-DE: 8 captions

This marks every needs_review or new entry with text, for that locale and its language, as reviewed. --locales is required, and takes a comma-separated list.

Only run it once the drafts have really been checked. It’s the step that lets them be published.

In the Mac app, Mark Reviewed… beside a locale in the Compose inspector asks for confirmation first. You can also change an entry’s state in Xcode’s String Catalog editor.

Composing

shotlist compose

For every locale and slot, compose makes one screenshot per sequence entry, in order:

Shotlist/Output/composed/<locale>/<slot>-NN.png

NN counts from 01 in sequence order. A scene skipped on a slot (one that doesn’t list the iPhone Duo posture, say) takes no number there. The folder follows fastlane’s <locale>/ layout, so tools that read fastlane screenshot folders can read it.

Each screenshot uses:

  • the capture in the entry’s appearance if it has one, or else in the first appearance in appearances;
  • the template named by the entry’s template, or caption-top;
  • the caption and subtitle for that scene and locale.

Compose renders at the slot’s exact pixel size, flattens to opaque sRGB with no alpha, writes the PNG, then reads it back and checks its size and alpha before counting it done.

32 composed · 0 unchanged · 10 creative files · 0 failed
  ! machine-drafted captions not yet reviewed: de-DE
Option Effect
--slots <list>, --locales <list> Compose only these.
--changed-only Re-render only screenshots whose capture, caption, subtitle, template or partner scene changed since they were last composed.
--debug Also write each creative asset with Apple’s safe area drawn on, as …-safe-area.png.
--json Print the summary as JSON.

Without --changed-only, compose first removes every composed file for each slot and locale it’s composing, then renders them all. With it, unchanged screenshots are left alone, and files numbered past the end of a shorter sequence are removed, so nothing from an old sequence is left to be uploaded.

Compose doesn’t refuse unreviewed drafts: it renders them so you can see them, and lists the locales. verify and publish refuse them.

Compose exits 1 if anything failed: a missing capture, a missing caption, an overflow or a missing template. Each failure names the locale, slot and scene. Other screenshots are still written.

In the Mac app, Compose (⌘B) does the same, with Changed Only on by default.

Templates

A template is one layout plus a style kit: background, caption and subtitle styles, the device treatment and margins.

Built-in templates

Template Layout Look
caption-top caption-top Light warm gradient. Bold dark caption above the device. The default.
caption-bottom caption-bottom Dark green gradient. Bold white caption below the device.
full-bleed full-bleed The capture fills the canvas, with a dark scrim at the bottom and a heavy white caption over it, aligned to the leading edge. No device.
split split Two scenes side by side under one caption. For iPad and the Duo’s inner display.
header-brand header For header art: a dark green gradient with a heavy white title and subtitle, centred in Apple’s safe area.
search-hero search For search art: the title beside a capture, inside Apple’s safe area.

Layouts

Layout Arrangement
caption-top Caption and subtitle in the top part of the canvas; the device below takes device.height of the canvas height.
caption-bottom The device at the top; caption and subtitle below.
full-bleed The capture fills the canvas edge to edge; the caption sits at the bottom over a gradient scrim. The caption area is 30% of the canvas height.
split Caption across the top; two devices side by side below: this entry’s scene and the scene named in with.
header Creative header art: text only by default, centred in the header safe area.
search Creative search art: headline on the leading side, device on the trailing side, inside the search safe area.

split needs room for two devices, so it’s used only on ipad-13 and iphone-duo-inner. On every other slot, an entry with split is composed with caption-top (your own caption-top.json if you have one). Name the second scene with with:

{ "scene": "planner", "template": "split", "with": "library" }

The with scene must also be in sequence, so that it’s captured; it’s composed as its own screenshot too. If it has no capture for that slot, locale and appearance, the entry’s own capture appears twice.

Your own templates

Put a template in Shotlist/Templates/<name>.json and name it in a sequence entry’s template. A file with a built-in’s name replaces that built-in for this project.

A template file is complete in itself; it doesn’t inherit from a built-in. Start from a built-in’s values below and change what you need:

{
  "layout": "caption-top",
  "background": { "type": "gradient", "colors": ["#FFF8EE", "#F2E3CC"], "angle": 90 },
  "caption": { "weight": "heavy", "size": 0.07, "minSize": 0.05, "color": "#2B1A0E", "maxLines": 3, "alignment": "center" },
  "subtitle": { "weight": "regular", "size": 0.036, "minSize": 0.028, "color": "#6B5443", "maxLines": 2, "alignment": "center" },
  "device": { "frame": "frameless", "cornerRadius": 0.11, "shadow": true, "height": 0.74 },
  "padding": 0.07
}

Sizes are fractions of the canvas’s shorter side, so one template works in portrait, in landscape (the Duo’s inner display) and on the wide creative canvases. A caption size of 0.072 on iphone-6.3 (1206 pixels wide) is about 87 pixels.

Key Type Required Default Meaning
layout string yes caption-top, caption-bottom, full-bleed, split, header or search.
background.type string yes color, gradient or image.
background.color string no #000000 For color, and the fallback for image.
background.colors array of strings no black to #333333 For gradient: two or more colours.
background.angle number no 90 For gradient: degrees. 0 runs left to right, 90 top to bottom.
background.image string no For image: a file in Shotlist/Templates/, scaled to fill the canvas.
caption.size number yes Starting point size, as a fraction of the shorter side.
caption.minSize number no 75% of size The smallest it may shrink to before it counts as an overflow.
caption.color string yes Text colour.
caption.weight string no regular regular, medium, semibold, bold, heavy or black. Always SF Pro.
caption.maxLines number no 3 The most lines the caption may take.
caption.alignment string no center leading, center or trailing.
subtitle object no none The same keys as caption. Without it, subtitles aren’t shown.
device.frame string with device frameless: the capture with rounded corners and a soft shadow. The only frame for now.
device.cornerRadius number with device Corner radius as a fraction of the capture’s width.
device.shadow boolean with device Draw a soft shadow.
device.height number with device The share of the canvas height the device may take.
device object no none Leave it out for no device (as full-bleed and header-brand do).
padding number yes The margin, as a fraction of the shorter side.

Colours are #RRGGBB or #RRGGBBAA, in sRGB. The result is flattened, so a translucent colour blends with what’s beneath it.

If a template file isn’t valid, compose stops: “Templates/.json isn’t a valid template: …”. A name with no file and no built-in fails: “no template ‘’: add Shotlist/Templates/.json or use one of caption-bottom, caption-top, full-bleed, header-brand, search-hero, split”.

The built-ins’ values

caption-top caption-bottom full-bleed split header-brand search-hero
Background gradient #F5F2EA → #E4DECF, 90° gradient #1F3A2E → #0E1A14, 90° #000000 gradient #F5F2EA → #E4DECF, 90° gradient #1F3A2E → #3E6B4F, 20° gradient #1F3A2E → #3E6B4F, 30°
Caption bold 0.072 (min 0.05), #0E1A14, 3 lines, centre bold 0.072 (min 0.05), #FFFFFF, 3 lines, centre heavy 0.078 (min 0.054), #FFFFFF, 3 lines, leading bold 0.05 (min 0.036), #0E1A14, 2 lines, centre heavy 0.1 (min 0.06), #FFFFFF, 2 lines, centre heavy 0.08 (min 0.045), #FFFFFF, 3 lines, leading
Subtitle regular 0.038 (min 0.03), #4A5A52, 2 lines regular 0.038 (min 0.03), #C9D6CF, 2 lines medium 0.04 (min 0.03), #E8E8E8, 2 lines regular 0.028 (min 0.022), #4A5A52, 2 lines medium 0.044 (min 0.03), #DCE7E0, 2 lines none
Device corner 0.11, shadow, 0.74 corner 0.11, shadow, 0.74 none corner 0.06, shadow, 0.76 none corner 0.11, shadow, 0.82
Padding 0.07 0.07 0.07 0.05 0.05 0.06

In the Mac app, the Compose inspector shows the selected scene’s template, and Show Templates in Finder opens the folder.

Text fitting

A caption is never clipped. Compose fits each one to its box:

  1. It starts at the template’s size.
  2. It lays the text out with TextKit at 96% of the box width, so SwiftUI’s own wrapping can’t disagree at the edge.
  3. If it doesn’t fit, it shrinks in steps of 2% of the starting size, down to minSize.
  4. If it still doesn’t fit at minSize, that screenshot fails with an overflow, naming the locale, the scene and the reason. Nothing is written for it.

A caption doesn’t fit when:

  • a single word is wider than the box. A long German compound, say. It would have to break mid-word;
  • it needs more lines than maxLines;
  • it’s taller than its share of the caption area. The caption gets 70% of the area’s height and the subtitle 30%.
caption overflow in de-DE, scene 'library': "Leseclubterminverwaltung leicht gemacht" doesn't fit: the word "Leseclubterminverwaltung" is wider than the caption box even at the minimum size (60 px). Shorten it for de-DE in Captions.xcstrings, or add a line break.

To fix an overflow:

  • write a shorter caption for that locale;
  • add a line break, or break a long compound with a hyphen and a line break;
  • or use a template with a smaller minSize, more maxLines or a smaller device.

In the Mac app’s Compose screen, a locale that overflows is outlined and labelled with the reason.

Right-to-left languages

Locales whose language is Arabic (ar), Hebrew (he), Persian (fa) or Urdu (ur) are composed right to left: the layout mirrors, and leading alignment becomes the right-hand edge.

Content rules

Apple’s rules for creative assets forbid prices and discounts, URLs, copyright symbols, mentions of other platforms or stores, and Apple recognitions. They’re good sense for screenshot captions too. Shotlist checks every caption, subtitle and creative title in every locale:

Rule Catches
Price or currency A currency symbol or code next to a number: £5, 9 €, USD 10, 20 kr.
Percentage with a price word A percentage near words such as off, sale, save, discount, deal or free, in English, German, French, Spanish, Swedish, Danish, Norwegian, Dutch and Italian: 50% off, Sparen Sie 30 %.
URL Web addresses and domains: https://…, www.…, myapp.app.
Copyright or trademark symbol ©, ®, ™, (c), (r), (tm).
Another platform or store Android, Google Play, Play Store, Galaxy Store, Samsung, Huawei, AppGallery, Windows, Microsoft Store, Amazon Appstore, F-Droid.
Apple recognition Editors’ Choice, App of the Day, Game of the Day, App of the Year, Game of the Year, Apple Design Award, Featured by Apple, As seen on the App Store.

Matching ignores case. Compose lists findings as warnings (! content: de-DE scene.shop.caption: price or currency: “€5”); verify fails on them.

Creative assets

iOS and iPadOS 27 show two kinds of art on the App Store: a product page header above your screenshots, and search results art in search. Shotlist draws both from your captions and captures. Configure them under creative in shotlist.json (see Configuration):

"creative": {
  "header": { "template": "header-brand", "localised": true },
  "search": { "scene": "library", "template": "search-hero" }
}

What compose writes

File Size Notes
creative/<locale>/header.png 3840 × 1646 One per locale. With "localised": false, one only, in creative/shared/, from the first locale’s captions.
creative/<locale>/header-16x9.png 5244 × 2950 The 16:9 alternative. Left out with "universal": false.
creative/<locale>/search.png 3840 × 2560 One per locale.
creative/<locale>/search-16x9.png 5244 × 2950 The 16:9 alternative.
creative/<locale>/search-preview.png 384 × 256 The search art at about a tenth of its size, as it shows in results.
…-safe-area.png as above With --debug: a copy with Apple’s safe area outlined.

All are opaque PNGs with no alpha channel.

Text and captures

  • Header: creative.header.title, with creative.header.subtitle below if you give one. Header art has no screenshot by default, because the screenshots sit directly below it on the product page. Set header.scene to include one.
  • Search: creative.search.title, or the search scene’s caption if there’s none, beside a capture of search.scene. The capture comes from search.slot, or the first iphone-6.x slot in slots. That scene must be in sequence, so it’s captured.

Safe areas

Apple’s templates leave a safe area in the middle of each canvas; outside it, art can be cropped on some devices. Shotlist keeps all text and captures inside it:

Canvas Safe area
Header, 3840 × 1646 x 1097 to 2743, y 493 to 1154
Search, 3840 × 2560 x 836 to 3004, y 765 to 1795
16:9, 5244 × 2950 The same proportions as the canvas it’s an alternative to. Apple publishes no separate numbers for it.

Run shotlist compose --debug to write copies with the safe area outlined, or turn on Safe Areas in the Mac app’s Creative screen.

Legibility at search size

Search art shows about 365 points wide on an iPhone: roughly a tenth of its pixels. search-preview.png shows it at that size. If the headline would be smaller than 14 pixels there, compose warns:

! search art headline in de-DE is 12 px at search-results size; 14 px or more reads reliably

Use a shorter headline for that locale, or a template with a larger caption.

Uploading

publish --apply uploads header.png and search.png and places them on each localisation. The 16:9 alternatives are written for you to use elsewhere or upload by hand; publish doesn’t upload them. Creative assets are reviewed with the app version. See App Store Connect.

Failures

Message Fix
no creative.header.title caption in <locale> Add the key for that locale.
no creative.search.title (or caption for '<scene>') in <locale> Add either.
no capture of '<scene>' to put in the search art The scene isn’t in sequence, or its capture failed for that slot and locale.
title overflow in <locale>: … As for captions: shorten it or add a line break.