Skip to content
Development

Translating Deep Cut Atlas into Spanish, and the stale-binary bug that wasn't a translation bug

By Victor Da Luz
swiftiosi18ndev-logdeep-cut-atlas

153 strings. That’s how many keys were sitting in Deep Cut Atlas’s String Catalog after the retrofit work, all in English, all waiting for a translation pass. This was that pass: Spanish, specifically Latin American Spanish (es-419), not the Spain or US variants. I translated all 153 by hand, checked the result on a real screen, and along the way spent about twenty minutes debugging a bug that turned out not to exist.

The mechanical part

Translating 153 strings by hand sounds like it should be the hard part, and it wasn’t, really. It’s mechanical once you have the list: read the English, write the Spanish, keep every %@ and %lld placeholder in the exact same position, don’t translate “To Check Out” because that’s a literal playlist name the app creates in the user’s actual Apple Music library, not UI chrome. The only real judgment calls were things like whether “Comp” (short for Compilation, a filter chip label with about four characters of room) should become “Comp” or something else in Spanish - I kept it, since there’s no clean four-letter equivalent and it’s used as an abbreviation either way.

The bug that wasn’t a translation bug

Then I went to verify it on the iOS Simulator, switched the simulator’s language to Spanish (Latin America), rebuilt, and launched the app. Still English. Every tab, every label, still English, like nothing had changed.

I went down the obvious paths first. Checked the compiled app bundle directly - yes, es-419.lproj/Localizable.strings was sitting right there with the correct translated text. Checked the simulator’s actual system-level language preference - yes, Spanish was first in the list, for real. Tried writing the language preference directly into the app’s own preference domain, bypassing the system setting entirely, in case something about per-app overrides was the missing piece. Nothing. The app kept rendering in English no matter what I changed on the language side.

The compiled resource was right. The device setting was right. The only thing left was the running app itself, and it turned out that was the actual problem: the build step I’d been using compiles the app but doesn’t install it. Every “fix” I tried was relaunching the same old binary from before I’d even written a single translation. Once I explicitly installed the freshly built app - one extra command I’d been skipping - the whole thing worked immediately. Every tab, correctly translated, on the first try.

It’s a very ordinary mistake in hindsight - forgetting a build actually needs to land on the device before you can see it - but it’s the kind of thing that’s invisible until you go looking for it, because every other signal you’d normally check (the compiled resource, the device setting) looks completely correct. The bug wasn’t in the translation. It wasn’t even really a bug. It was a stale binary wearing a translation bug’s clothes.

A real bug it did surface

Once the fresh build was actually running, almost everything rendered correctly - except three buttons that stayed in English no matter what: “Add to library,” “Add to Playlist,” and “Add album to To Check Out.” Those three turned out to be raw string literals returned from a Swift computed property, never wrapped in the call that makes a string catalog-aware. It’s the exact category of gap the original retrofit work called out by name and swept for - and still missed these three, because the files containing them didn’t exist yet at the time of that original sweep. Grepping for the raw pattern (a return statement building a string that isn’t wrapped) caught all three in one pass, which is now the technique I’d reach for first next time instead of another manual review.

What I’d take from this

When a translation seems to not be applying and every individual piece you check looks correct, check whether the piece you’re actually looking at is the piece that’s actually running. And when a “we already swept for this” claim comes up against new code that didn’t exist during the original sweep, a mechanical grep beats a second manual pass every time - no extra intelligence required, just exhaustiveness of a kind a second read-through never quite manages.

Related reading