●FORK — Claude Code 2.1.212 changes what /fork does: it copies your conversation into a new background session with its own row in claude agents, so you can keep working. The old in-session subagent is now /subtask●LIMITS — WebSearch calls are now capped at 200 per session by default, and subagent spawns get the same 200 ceiling, so a runaway search or delegation loop stops on its own●MCPBG — MCP tool calls running past two minutes now move to the background automatically, keeping the session usable. Tune the threshold with CLAUDE_CODE_MCP_AUTO_BACKGROUND_MS●PLANFIX — Fixed plan mode auto-running file-modifying Bash commands such as touch and rm without a permission prompt or an SDK canUseTool callback●SONNET5 — Claude Sonnet 5 is running on introductory pricing of $2 per million input tokens and $10 per million output. After August 31 it moves to $3 and $15●IPO — Bankers are reportedly lining up investor meetings for Anthropic ahead of a possible public listing as soon as October●FORK — Claude Code 2.1.212 changes what /fork does: it copies your conversation into a new background session with its own row in claude agents, so you can keep working. The old in-session subagent is now /subtask●LIMITS — WebSearch calls are now capped at 200 per session by default, and subagent spawns get the same 200 ceiling, so a runaway search or delegation loop stops on its own●MCPBG — MCP tool calls running past two minutes now move to the background automatically, keeping the session usable. Tune the threshold with CLAUDE_CODE_MCP_AUTO_BACKGROUND_MS●PLANFIX — Fixed plan mode auto-running file-modifying Bash commands such as touch and rm without a permission prompt or an SDK canUseTool callback●SONNET5 — Claude Sonnet 5 is running on introductory pricing of $2 per million input tokens and $10 per million output. After August 31 it moves to $3 and $15●IPO — Bankers are reportedly lining up investor meetings for Anthropic ahead of a possible public listing as soon as October
Localizing an App Store Screenshot PSD into 15 Languages Overnight with Cowork
A work log of localizing the store screenshots for the Android wallpaper app 'Beautiful Wallpapers' into many languages with Cowork: extracting text with psd-tools, pouring translations into layer names, and the real walls I hit with Thai and Korean glyphs.
The moment I opened the PSD from my designer, I froze. 1440x13333px, 220MB. It held the store screenshots for my Android wallpaper app "Beautiful Wallpapers," with Japanese headline copy scattered across five artboards. I wanted to expand it into at least ten, ideally fifteen languages — and I wanted to start swapping the store assets the next morning.
Translating and replacing the Photoshop text layers by hand would mean 5 boards x 15 languages, or 75 screens of copy-paste, where mix-ups are guaranteed. So I handed the PSD itself to Cowork and let it process the file mechanically.
Doing so, I ran straight into an obvious truth: fonts and glyphs are far more stubborn than translation. This is the log of that one night.
Why screenshot localization can be cleanly separated from making art
The first thing I settled in my own mind was where to draw the line on letting AI help. I make contemporary art alongside indie development, and I have consistently kept AI out of the artwork itself. Making things by hand carries meaning of its own, and I want to preserve that.
But putting a work or an app "out into the world" is a different job from making it. A store screenshot is not the substance of the wallpaper product; it is closer to the price tag or the point-of-sale display that puts it on the shelf. So screenshot layout and localization fall, without hesitation, into the designer work I am happy to delegate to AI.
Keeping creation and promotion consciously separate lets me make this call in an instant. When that line stays blurry, you end up relitigating "how far should I automate this?" on every single task.
Count what the PSD is made of, first
Before localizing, I need to know exactly what the PSD is built from. The Cowork environment can install the Python edition of psd-tools, so I opened the file and dumped its structure.
from psd_tools import PSDImagepsd = PSDImage.open("screenshot.psd")print(f"canvas: {psd.width} x {psd.height}")for layer in psd.descendants(): kind = layer.kind # 'type' / 'pixel' / 'group', etc. if kind == "type": print(f"[text] {layer.name!r} @ {layer.bbox} -> {layer.text!r}") elif kind == "group": print(f"[group] {layer.name!r} children={len(list(layer))}")
The output showed that each of the five artboards was a group, with roughly 40 layers per board (two text streams: headline and description).
One thing mattered here: the PSD that had been sitting in my workspace and the uploaded one had different resolutions. The canonical file was the uploaded 1440x13333px, 220MB version, so I pinned the script to extract and render from that file. When you receive several copies, deciding which one is canonical exactly once, up front, saves you from rebuilds later.
If the PSD arrives through a cloud-synced folder, you can hit a stranger problem: bash claims the file does not exist while Finder shows it plainly. I wrote about that in Why Cowork's bash says "no such file" when Finder can see it. With a 220MB file, materialize it before you touch it.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦Follow the full hands-on flow of taking a 1440x13333px, 220MB store PSD, counting its artboards and text layers mechanically with psd-tools, and pouring 15 languages into it
✦See working code for the mac_roman trap, where psd-tools fails to save non-ASCII layer names, and how writing the luni block directly resolves it
✦Take away an auto-shrink implementation for translations that overflow the canvas, plus a clear line for where automation should stop and hand back to a human
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
Put the translation itself into each text layer's name
The natural way to localize is to rewrite each text layer's content into the translated string. I do that too, but in addition, I set each text layer's name to the translated string itself.
The reason is so that opening the Photoshop layers panel tells you at a glance "which sentence in which language" you are looking at. If a layer name stays headline_1, you cannot read the content in a language whose glyphs do not render. With the translation in the name, even a language like Thai — whose glyphs my environment lacks and which cannot be rasterized — can still be checked and copied from the panel.
If you are returning a PSD that assumes manual polishing afterward, this small step helps the downstream work the most.
I created a group per language and named each group like th_ไทย or ko_한국어 — the language code plus the language's own endonym. Using the native name rather than the English name (Korean) removes ambiguity when handing each language to a reviewer.
The font and glyph wall — stumbling on Thai and Korean
This is where the real work begins. Even after rewriting the text, if the environment has no font that can draw those glyphs, you get tofu boxes the moment you rasterize. The work environment had restricted networking and could not fetch the Noto fonts, so I first checked how far the already-installed fonts could go.
Script / language
Font tried
Result
Decision
Latin (English, German, French, Spanish, Italian, Turkish, etc.)
DejaVu Sans
All glyphs render
Rasterize as-is
Cyrillic (Russian)
DejaVu Sans
All glyphs render
Rasterize as-is
Arabic (40 layers)
DejaVu Sans
Renders; joined forms need a look
Rasterize, final check by a human
Chinese (Simplified, 35 layers)
Droid Sans Fallback
Renders
Rasterize as-is
Korean (Hangul)
Droid Sans Fallback
Some glyphs missing
Switch to an alternative; leave the rest name-only
Thai
No matching font in the environment
Cannot render (tofu)
No raster; hand off via layer name and Excel
So I decided that Thai would not be rasterized into the PSD, but kept only in the layer name (the translation) and an Excel handoff. In the end, the 15 Latin-script languages plus Arabic and Chinese rendered, while parts of Thai and Korean were left "name only" due to missing glyphs.
Because the fonts were meant to be swapped to the production set (Noto / IPAex) later, it was more realistic not to chase perfection here but to hand over the file with the swap points clearly marked. Polishing the official look is something you do in an environment where the glyphs are present.
Kill the "do nested groups survive a save-and-reopen" risk on a small file
Onto the nested structure of five artboards x per-language groups, I add new pixel layers (the rasterized translations). Editing a complex hierarchy with psd-tools can corrupt the structure when you save and reopen. On a 220MB production file, where saving alone takes minutes, you would notice that far too late.
So before the production run, I verified on a tiny PSD that "add a pixel layer into a nested group + subgroup, save, then reopen" was safe.
from psd_tools import PSDImagedef verify_roundtrip(path: str, expected_boards: int) -> None: """Reopen a saved PSD and check mechanically that the structure survived.""" psd = PSDImage.open(path) boards = [l for l in psd if l.kind == "group"] assert len(boards) == expected_boards, f"artboards: {len(boards)} != {expected_boards}" added = [l for l in psd.descendants() if l.kind == "pixel" and l.name.startswith("t_")] assert added, "the pixel layers we added are gone after reopen" for l in added: assert l.bbox != (0, 0, 0, 0), f"{l.name} lost its coordinates" print(f"ok: boards={len(boards)} added_layers={len(added)}")verify_roundtrip("mini_test.psd", expected_boards=5)
Writing the assertions removes any room to glance at the output and call it "probably fine." Only after confirming that the five artboards were preserved and that the added layers' kind and coordinates read back correctly did I proceed to the production build. The heavier the operation, the more it pays to do one round trip on a miniature first.
psd-tools fails on non-ASCII layer names (the mac_roman problem)
The rendering itself passed, but saving threw an error. The cause: psd-tools tries to write layer names in the legacy mac_roman encoding, so names containing non-ASCII characters like 한국어 or Русский failed to encode.
This trap collided head-on with my plan to put translations and endonyms into the layer names.
A PSD layer name is actually a dual structure. One is the legacy Pascal string in the layer record (ASCII-oriented, written as mac_roman). The other is the luni (Unicode Layer Name) additional-info block, which holds UTF-16BE correctly. Photoshop prefers luni when reading.
In other words: put a safe ASCII placeholder in the legacy slot, write the real name into luni, and both saving and display work.
from psd_tools.psd.tagged_blocks import TaggedBlocks, TaggedBlockfrom psd_tools.constants import Tagimport redef safe_ascii(name: str, index: int) -> str: """For the legacy slot: drop non-ASCII, fall back to a serial name if empty.""" ascii_only = re.sub(r"[^\x20-\x7E]", "", name).strip() return ascii_only or f"layer_{index}"def set_layer_name(record, name: str, index: int) -> None: """Legacy name stays ASCII; the real name goes into luni (UTF-16BE).""" record.name = safe_ascii(name, index) # only values mac_roman can encode blocks = record.tagged_blocks or TaggedBlocks() blocks[Tag.UNICODE_LAYER_NAME] = TaggedBlock( key=Tag.UNICODE_LAYER_NAME, data=name, # 'ko_한국어' verbatim ) record.tagged_blocks = blocksfor i, (record, translated) in enumerate(zip(layer_records, translations)): set_layer_name(record, translated, i)
Since separating the two, saving has never failed once. If you choose a design that "puts multiple languages into the names," this save path is the one thing to verify first. Two of my three rebuilds traced back to this single point.
Translations overflow — adding auto-shrink to the font size
Japanese headline copy is short, and the same meaning in English or German is reliably longer. Render an overflowing layer as-is and the text either clips or collides with the element beside it.
So I added a step that steps the font size down until it fits, with a floor at 50% of the original. Below that, a screenshot stops being readable.
from PIL import ImageFontMIN_SCALE = 0.50STEP = 0.95def fit_font(text: str, font_path: str, base_size: int, max_width: int): """Return the largest font size that fits max_width, or None if it never fits.""" size = base_size floor = int(base_size * MIN_SCALE) while size >= floor: font = ImageFont.truetype(font_path, size) width = font.getbbox(text)[2] if width <= max_width: return font, size, size < base_size # (font, actual size, was it shrunk) size = int(size * STEP) return None, floor, True # does not fit even at the floor -> hand back to a human
The third return value is the point. I log only the places where shrinking happened, so they can be eyeballed afterward.
Of 31 total edits, 2 to 11 locations per language ended up shrunk; languages with long words, like English and German, needed more. Nothing failed to fit even at the 50% floor this time, but when it does, the function returns None, halts the run, and pushes the decision — shorten the translation itself — back to a human. Deciding in advance where automation stops is what keeps it from quietly making bad calls.
I also standardized how numbers appear across languages: small badges read "100K+" while big headlines read "100,000+". All 16 languages' translations are laid out side by side in an Excel file. The pipeline for producing that kind of tabular summary in Cowork is the same one described in Cowork x Python data analysis automation.
Folding the work into five steps
Looking back on that night, the reproducible procedure folds into five steps. Next time I swap screenshots, following this order removes all hesitation.
Fix the canonical file: if several PSDs arrive, pick one by resolution and size (here, 1440x13333px, 220MB)
Dump the structure: count artboards and text layers with psd-tools (5 boards x ~40 layers)
Check font availability: confirm the glyphs for each target language exist before rendering
Verify saving on a small PSD: make sure adding layers into nested groups survives a reopen
Build and export: render with translations in the layer names, then export per-store sizes
Glyphs rendered cleanly for 13 of 15 languages, about 87%. The remaining two (Korean and Thai, about 13%) were left name-only due to missing glyphs and pushed to a finishing pass in an environment with the fonts present.
Seeing it as a ratio makes the boundary explicit: how far automation reaches, and where manual work begins. I strongly recommend deciding up front where you stop and hand the file to a human. Chase perfection across every language in one pass and you will usually stall at the glyph wall.
Exporting for Play Console and App Store Connect
The polished PSD is finally resized and exported to each store's recommended dimensions. Google Play Console and App Store Connect demand different aspect ratios and pixel counts, so preparing a process that generates multiple sizes from one master makes the next version's screenshot swap reusable as-is.
This time I kept the master PSD as the source of truth and automated export as a language x size product. Pushing the exported images into the store connects to the flow I described in Automating App Store Connect with Claude in Chrome.
This kind of "putting it on the shelf" work is exactly where systematization pays off most. The substance of an app, I make by hand. The layout that delivers it to people around the world, I delegate to AI to create time. That line is, for me, the realistic landing point for running multilingual store operations as an individual.
Update — the editable PSD as a landing point
The work continued after I wrote this, with a few steps forward.
The glyph problem I started with was resolved not by rasterizing directly into the PSD, but by switching to exporting an editable PSD with live text layers (localized_editable.psd). The flow is: drag each language's group into the original screenshot PSD, hide the Japanese layers, swap the font, then export. Whether the glyphs render is confined to the final font-application step.
For that font swap, I prepared a per-language recommended-font cheat sheet (FONT_GUIDE.md). It maps the design's Bold, Medium, and Light weights to Noto Sans / Noto Sans KR, SC, TC, Thai, and Arabic, with IPAexGothic for Japanese. For a unified look across languages, standardizing on the Noto family felt safest.
In the end I reviewed all 16 languages and confirmed there are no mistranslations and that the app name follows the official ASO name (with only minor wording variance between a category label and body text in Russian, not a meaning error).
What I feel having reached this point is that, rather than chasing a perfect raster in a constrained environment, handing things over as "an editable state plus a font-swap instruction sheet" is in the end the fastest and most reliable. Set the exit of your automation at "something a human can finish," not at "the finished thing." That alone removes one place to get stuck.
If you want to try this next, start by handing one PSD to Cowork and dumping the list of its text layers. How many artboards, how many strings? The moment you see those numbers, the reason not to do it by hand becomes obvious.
Share
Thank You for Reading
Claude Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.