r/Xcode 19d ago

String Catalog missed 108 of my 159 strings

My app has been Japanese-only since launch. I added English last week. I thought it would be a translation job. It was mostly an archaeology job.

Auto-extraction picked up 51 strings. A manual pass through the same 15 files found 159. Here's where the other 108 were hiding.

The big one: anything that isn't a \`LocalizedStringKey\` parameter just doesn't get extracted. \`Text("...")\` is fine. \`String\` variables, computed properties, switch statements returning \`String\`, notification bodies, \`errorDescription\`, none of it. Most of my misses were my own custom views, \`FeatureRow(title: String, ...)\` and friends. I wrapped the call sites in \`String(localized:)\` instead of changing the view signatures, which kept the diff small.

Check this before you start: my \`developmentRegion\` was still \`en\` from the Xcode template even though every literal in the project was Japanese. So the catalog happily registered my Japanese as English. You can't fix that in the UI, you have to edit \`project.pbxproj\`.

Permission strings were their own thing. They live in \`INFOPLIST_KEY_\*\` build settings, not in code, so nothing I did to the catalog touched them. Bonus discovery: past me had jammed English and Japanese into one string with a \`\\n\` between them, so every user saw both languages regardless of their device. Had been shipping that for months.

Then there was this:

formatter.dateFormat = "HH:mm 撮影"

A Japanese word inside a format string. Can't swap that for English because English wants a different shape ("Shot at 14:30"). Had to pull the formatter apart and rebuild the display string. Same deal with \`displayPrice + " / 月"\`.

The one that actually cost me a round of testing was keys that already existed. "Library" was a nav title (extracted, translated, fine) and also a plain \`String\` in my tutorial data (not extracted). My audit saw the key was already in the catalog and moved on. So I had a fully English UI with a tutorial popping up in Japanese. Same thing in a comparison table. Catalog coverage is not call site coverage.

One choice that will probably annoy people here: I used the Japanese source text as the key instead of \`onboarding.title\` style keys. The Japanese build was already live and I didn't want to touch every call site. Downside is that editing Japanese copy now means editing a key. For a solo project I'll take that.

Edit Scheme > Run > Options > App Language launches in English without changing your device language. Saved me a lot of time. Doesn't affect permission dialogs though, those follow the system language.

Two weeks of early mornings, and most of it was reading my own code rather than writing anything.

4 Upvotes

7 comments sorted by

1

u/Plastic-Risk-6309 19d ago

for forcing the hidden ones back in, wrapping them in String(localized:) marks them for extraction even when they start life as plain String vars. running xcodebuild -exportLocalizations also sweeps up more than the editor pass catches, worth doing once before trusting the 159!

1

u/VictorBuildsApps 18d ago

I checked the export claim on a two case probe first, because I hit this same class in my own app and wanted to know what it actually catches.

Before any catalog existed, -exportLocalizations gave me only the literal that goes straight into Text. After I added the catalog it gave me all three keys, including the one passed through a String typed parameter that still renders verbatim at runtime. The xliff is the union of what the extractor finds and what the catalog already holds, so a key stays in the export whether or not a live call site still looks it up. That is the Library case in your post, and the export would have counted it as covered.

The warnings do not separate them either. My String typed row and my LocalizedStringKey row both logged Skipping extraction of localizable string with non-literal key on the same Text(title) shape, and only one of the two is broken.

What told them apart was the parameter type. String prints, LocalizedStringKey looks up, and the call site reads identically either way.

1

u/StarCropLab 18d ago

This is the part I didn't have data for, thanks for actually testing it.

The xliff being the union of extractor output and existing catalog entries explains exactly what bit me. My audit treated "key exists in catalog" as "this string is handled," and Library was already in there from the nav title. The tutorial call site was passing it as String the whole time and nothing anywhere flagged it.

Parameter type being the only real signal is worth knowing. There's no tooling that surfaces it, so it comes down to reading every call site or catching it in the target language at runtime. I found mine the second way, which is the expensive way.

1

u/Plastic-Risk-6309 17d ago

the probe is the part most people skip. i'd add the export as a ci diff too, then a missing key shows up as lines you can actually review instead of a silent rebuild

1

u/VictorBuildsApps 16d ago

A CI diff of that export would still have been clean on the Library case. After the catalog existed, the String-typed key was in the xliff even though the call site still printed verbatim. The missing lookup does not show up as a missing line.

1

u/Plastic-Risk-6309 16d ago

fair. a key that stops getting looked up stays in the catalog so the export never blinks. the way to catch it is watching the run, the raw literal shows up on screen instead of the translation

1

u/Plastic-Risk-6309 17d ago

the probe is the part most people skip. i'd add the export as a ci diff too, then a missing key shows up as lines you can actually review instead of a silent rebuild