I ship a few small apps on my own, and localization was always the task I put off. Pasting strings into a chat window "works" until a translation drops a %@ or turns %1$@ … %2$@ into something String(format:) doesn't like, and you find out from a crash report.
Here's the approach that finally made it boring (in a good way):
1. Only translate what's missing. .xcstrings is just JSON. For each key, check localizations[lang]: no entry, an empty value, or state == "new" means it needs work. Skip shouldTranslate: false. If there's no source localization, the key itself is the English text. That one tripped me up.
2. Send comments, not just strings. I batch around 40 strings as JSON {id: {text, comment}}. The comment field (the one you set with String(localized:comment:)) is what stops "Delete" and "Save" from being translated as the wrong part of speech. Strings with no comment get noticeably worse translations.
3. Validate placeholders with code, not trust. Count the placeholder tokens in source and target with a regex (%@, %lld, %1$@, %.1f, %%, \(name), tags) and compare them as multisets. Reordering positional ones is fine; missing or extra ones are not. On a mismatch, retry that single string once with the error in the prompt. If it fails again, leave it untranslated and report it.
4. Plurals need the target's categories. English has one/other. Russian and Polish need one/few/many/other, and Japanese only other. Ask for exactly those keys and validate them.
5. Write it back the way Xcode does. json.dumps(data, indent=2, separators=(",", " : "), sort_keys=True, ensure_ascii=False) matches Xcode's own formatting, so the diff only shows real changes.
6. Mark everything needs_review. Then you can see in Xcode what no human has checked yet.
The system prompt I use:
You are a senior software localizer translating iOS app UI strings from English into {language}.
- Keep EVERY placeholder and markup token exactly: %@, %d, %lld, %1$@, %.1f, %%, \(name), <b>.
You may reorder positional placeholders for grammar, never drop or add one.
- Use the developer comment to understand where the string appears.
- Buttons/tabs stay about as short as the source. No added punctuation.
- Follow the glossary exactly; never translate brand names.
Happy to answer questions about edge cases (device variations and substitutions I still do by hand).