August 9, 2026 · Lazare Kolebka

How I Automate 450 Localized App Store Screenshots

For a long time, updating my App Store screenshots took an afternoon. I took screenshots on my phone, framed them in Screeny, imported the mockups into Sketch, lined them up, and exported.

Then I looked at what I had shipped to the Chinese App Store.

A Chinese App Store screenshot for Screeny with a translated headline above a phone whose on-screen UI is still in English: Color, Image, Unsplash, None, Solid, Gradient
What I shipped. The headline is Chinese and the app is English.

The headline is in Chinese. Everything inside the phone is in English. I had translated the caption and left the app untranslated, in the one image whose job is to show the app.

Apple treats screenshots as localized metadata, like the description and the keywords. Its guidance is to “localize your app description, keywords, app previews, and screenshots, for each of the markets in which you offer your app” (App Store product page guidance ).

Fixing that by hand meant doing the same clicks in every language, and I got fed up with clicking. So I built an MCP server into Screeny, and now Claude does everything.

This post covers how the pipeline captures the app in 15 languages, how it gets a 9:41 status bar out of the simulator, and how the result ends up in Sketch.

Why 450 screenshots?

Screeny runs on iPhone, iPad and Mac. It is translated into 15 languages. Each listing has 10 screenshots.

The ten English iPhone screenshots in Screeny's App Store listing, side by side, each with a headline above a framed phone
One listing. There are 45 of these.

3 × 15 × 10 = 450 PNGs.

I rarely need to redo all 450. Usually one screen in the app changes. But one screen is still 3 × 15 = 45 files, and all 45 are out of date the day that change ships.

At 45 files per UI change, I was spending more time on screenshots than on the app.

The whole job is one prompt now

A Claude Code remote control session on an iPhone. The prompt reads: update my appstore material for screeny, just for the zoom since this screen changed. Run the screenshot pipeline for iphone, ipad and mac and only re-capture the zoom view. Once it's done, frame it with screeny, the mcp is running, use the appstore preset and ipad preset. Once done, use the sketch mcp to replace the corresponding asset, Frame it pixel perfect, on every language.
The zoom screen changed, so 45 files were out of date. I typed this on my phone.

That is a real message. The zoom screen had changed, so its screenshot needed replacing in 45 places.

Claude isn’t doing design work here. The pipeline is three scripts, a JSON file and a Sketch document, and I wrote all of them before pointing an agent at them. What Claude does is the bookkeeping: which scene, which language, which preset, which artboard. That was the tedious part when I did it by hand.

The pipeline has three steps, and each one hands a PNG to the next:

  1. CaptureiOS SimulatorA UI test opens each screen listed in scenes.json, once per language, under a 9:41 status bar.Raw screenshot1206 × 2622
  2. FrameScreeny MCP serverA saved preset puts each iPhone and iPad capture inside a device frame.Framed mockup1879 × 3840
  3. ComposeSketchA script swaps the mockup into its artboard on each language page, then exports.App Store screenshot1320 × 2868
One pass per language, per platform. Mac captures skip step 2. The pixel sizes are the iPhone ones.

The rest of this post goes through them in order.

Step 1: Capture the app without tapping through it

The capture script never taps a button. A file called scenes.json describes each screenshot as data: which tab is open, how tall the bottom sheet is, which background is selected.

{ "id": "01-background-gradient", "panel": "editor", "tab": "background", "detent": "medium", "tabBounce": true, "background": { "type": "gradient", "index": 3 } }

Launch the app with -screenshotScene 01-background-gradient and a debug-only hook puts it straight into that state. The same hook unlocks Pro for that launch and skips onboarding, so no paywall or welcome screen ends up in the shot.

A UI test does the launching. For each scene it starts the app, waits for a marker the app sets when the screen is ready, and saves a full-resolution screenshot.

Changing the language is one flag:

./screenshots/generate.sh --scene 01-background-gradient --lang zh-Hans

The flag passes -AppleLanguages "(zh-Hans)" to the app at launch. Nothing in the pipeline translates anything. The app loads its own Chinese strings, so Color becomes 颜色 and Gradient becomes 渐变, the same way they would on a phone set to Chinese.

Before the first capture, the script does one more thing: it sets the status bar.

How to force the simulator’s status bar to 9:41

Every screenshot needs the same status bar: 9:41, full signal, full Wi-Fi, full battery. (Why 9:41?) Apple’s marketing guidelines ask for the icons too: the status bar “should show a full network icon or product designator, a full Wi-Fi icon, and a full battery icon.”

The simulator can fake all of it with one command:

xcrun simctl status_bar "$UDID" override \ --time "9:41" \ --dataNetwork wifi --wifiMode active --wifiBars 3 \ --cellularMode active --cellularBars 4 \ --batteryState discharging --batteryLevel 100

$UDID is the simulator’s ID. You can write booted if only one simulator is running. The override changes what the status bar draws, and the simulator’s real clock keeps running underneath.

That command is enough for a one-off screenshot. To get the same bar on every run, on iPhone and iPad, my script adds four things.

Force 12-hour time

On a simulator set to a 24-hour region, the bar reads 09:41. Apple’s screenshots read 9:41. A preference forces 12-hour time:

xcrun simctl spawn "$UDID" defaults write -g AppleICUForce24HourTime -bool false xcrun simctl shutdown "$UDID" xcrun simctl boot "$UDID"

The simulator reads that preference when it boots, so the script restarts it and then sets the status bar.

Use a full date for iPad

An iPad shows the date next to the time, and --time "9:41" only sets the time. Give --time a full ISO date and it sets both:

--time "2026-04-01T07:41:00.000Z"

The timestamp is in UTC and the simulator shows it in local time. 07:41Z reads 9:41 for me because I’m at UTC+2 in April, so pick the hour that lands on 9:41 where you are. The weekday next to the date still follows the real clock.

Keep xcodebuild on the same simulator

Xcode can run UI tests in parallel by cloning the simulator, and a clone doesn’t carry the override. Turning parallel testing off keeps the test on the simulator the script prepared:

xcodebuild test-without-building \ -xctestrun "$XCTESTRUN" \ -destination "id=$UDID" \ -parallel-testing-enabled NO

Set it again after a SpringBoard restart

Restarting SpringBoard, the process that draws the home screen, clears the override. Brim, my other app, has to do that: its screenshots include a home screen widget, widgets follow the simulator’s system language, and changing that language needs a restart. So Brim’s script runs the override again right after.

When you are done, this brings the normal status bar back:

xcrun simctl status_bar "$UDID" clear

simctl only talks to simulators. For a screenshot taken on a real iPhone, see How to Clean Up the Status Bar in App Screenshots.

How the Mac version gets captured

Screeny for Mac is a Mac Catalyst app. There is no simulator to boot, so a second script launches the real app once per scene, with the same -screenshotScene flag, and captures its window with screencapture.

Capturing a real window takes a little more setup than a simulator does:

  1. The Mac app is sandboxed and can’t read scenes.json from my source folder, so the script copies the file into the app’s container first.
  2. The capture is a rectangle of the screen, so the script makes sure Screeny’s window is in front before it shoots.
  3. macOS restores the last window size on launch, so the script deletes the saved window frame and every capture comes out at the same dimensions.

Step 2: Frame the captures with a preset

A preset in Screeny is a saved set of framing choices: device, background, status bar. I keep one for iPhone and one for iPad, and use them for all 15 languages.

PlatformPresetWhy
iPhoneDevice frame on, background transparentThe mockup sits on whatever background the Sketch artboard has
iPadDevice frame on, background included, its own sizingAn iPad needs different proportions to read well at App Store size
MacNone, the capture goes straight to SketchA window capture already looks like an App Store screenshot

The Mac row is a choice. Screeny can put a Mac capture inside a MacBook frame, but I went with the plain window, so Screeny isn’t involved in the Mac set at all.

Both presets leave Screeny’s Clean Status Bar option off. That option rebuilds the status bar on a screenshot from a real phone, and these captures already have a 9:41 bar from the simulator.

Claude sends each iPhone and iPad capture to Screeny’s MCP server with a preset name and gets a framed PNG back. MCP (Model Context Protocol) is the standard that lets an agent call tools in other apps, and Screeny for Mac runs a local server for it. The setup is in Automate Your App Store Screenshots with Claude and MCP.

Step 3: Swap the mockup into Sketch

A Sketch document's page list showing a Component template page followed by one page per language, English, Arabic, Hindi, Chinese, Japanese and more, with a separate iPad section repeating the same list, plus four framed screenshot artboards on the canvas
One template page, then one page per language. The list repeats for iPad.

I use Sketch as my design tool. I moved over from Figma when I saw how Figma meters its MCP server: 20 tool calls a month on the Starter plan, 200 a day on a paid Full or Dev seat . Replacing one screen in this pipeline touches 45 artboards. Sketch’s Mac-only license is a one-time payment, and its MCP server  runs locally inside the Mac app, so nothing counts the calls. It works brilliantly for this.

The Sketch document has a Component page with the master design, then one page per language. The list repeats for iPad, which has a different canvas size, and again for Mac.

Two rules make the document safe for a script to edit.

Every language page has the same artboards with the same names. 06 — Frame it pixel perfect exists on all 15 pages, spelled identically. The script finds an artboard by name. If it used position, it would break the first time I reordered a page.

Every artboard has a Header group and an Image layer. The header holds the translated headline, which I adjust by hand, and the script never touches it. The Image layer holds the mockup. It is the only thing the script replaces.

Scene names and artboard names don’t match, by the way. Scene 05-zoom goes into artboard 06 — Frame it pixel perfect. Scenes are numbered by app state and artboards by their order in the store listing, so the mapping between them is written out by name.

The swap and the export

Claude runs the script through Sketch’s MCP server, whose run_code tool executes JavaScript against the same API a Sketch plugin uses. The script finds the artboard, checks that it contains exactly one image, and replaces that image:

const frame = sketch.find('#' + frameId, doc)[0]; const imgs = sketch.find('Image', frame); // imgs.length !== 1 → abort with EXPECTED_ONE_IMAGE, rather than guess which layer imgs[0].image = { path: sourcePath };

Then it exports the artboard:

sketch.export(frame, { output: outDir, filename: frame.name + '.png', formats: ['png'], scales: ['1.0'], overwriting: true, });

Each swap logs the image’s pixel size before and after, and whether the exported file is the size Apple expects. A mismatch prints a ⚠ on that line, which is much easier to spot than a slightly smaller thumbnail in a grid of 15.

What one screenshot looks like at each step

Here is the first screenshot of the listing going through all three steps, in English and in Chinese.

A raw simulator capture of Screeny's Background editor tab in English, showing Color, Image and Unsplash source options with Gradient selected
1. Raw capture, en
Screeny's own framed mockup for the English hero screenshot: a real iPhone bezel around the Background editor screen, no hand yet
2. Framed by Screeny, en
The final exported English hero screenshot: the same framed mockup composited into the hand photo, with the Demos and Mockups headline above
3. Exported from Sketch, en
The same Background editor tab captured with the app launched in zh-Hans, with all the same controls now in Simplified Chinese and the status bar reading 9:41
1. Raw capture, zh-Hans
The same framed mockup for zh-Hans, a distinct render because the Background tab's labels are in Simplified Chinese
2. Framed by Screeny, zh-Hans
The final exported Chinese hero screenshot, same hand photo as the English version, translated headline above, and the app UI now in Chinese too
3. Exported from Sketch, zh-Hans

Both raw captures show 9:41 with full bars. That is the simulator override, and it looks the same in every language because the system draws the status bar, not the app.

This first artboard is the one exception to the single-image rule. The mockup sits behind a stock photo of a hand holding a phone with the screen cut out, so the artboard has two images. The photo is the same on all 15 pages, and the swap targets the mockup behind it specifically.

The other nine screenshots go through the same three steps, in every language.

Frequently asked questions

How do I set the iOS simulator’s status bar to 9:41? Run xcrun simctl status_bar booted override --time "9:41" --batteryLevel 100 --cellularBars 4 --wifiBars 3. If the clock reads 09:41, set AppleICUForce24HourTime to false with defaults write inside the simulator and reboot it.

Does the 9:41 override work on a real iPhone? No. simctl only controls simulators. For a screenshot from a real device, use a tool that rebuilds the status bar on the image, such as Screeny’s Clean Status Bar.

Does any of this leave my machine? No. Screeny’s MCP server only listens on 127.0.0.1 and Sketch’s MCP server runs locally too. The only upload is the finished PNGs going to App Store Connect.

Do I have to redo all 15 languages every time one screenshot changes? All 15 languages, yes, but only for the screen that changed. That is the 45-file case, not the 450-file one.

What if a screen in my app has no localized text? Several languages will produce identical captures. Hash them to confirm, render once, and copy the result. Identical screenshots are correct in that case.

Do I need Sketch for this? No. Sketch does the compositing, and any design tool with a scripting API that can replace a layer’s image and export an artboard can do the same. Capture and framing don’t depend on it.