Translating an iOS/Mac app with AI and humans

I’ve long wanted to get Muse translated into multiple languages – Muse has been English-only since launch. The problem isn’t so much the one time translation cost at the beginning, but having a strategy to translate for every single update going forward. Getting Muse into German isn’t a problem, keeping Muse in German is the problem.

Unlike just a few years ago, we now live in a world with high quality AI translations! As good as these AIs are, they’re still not human, and they’ll still miss important context about how these translations are used and viewed in the app, so having a human in the loop is still incredibly important. However – they do reduce the human-time cost of adding and maintaining multiple languages.

I’ve recently shipped a French translation for Muse, and I have German and Spanish cooking and expect those to release in the next week or two. Thats three languages in less than a month of time, and better yet, less than a few days of my coding time. Here’s how I’m able to translate Muse with minimal coder time and minimal volunteer time.

Step 1: Prep the codebase for localization

SwiftUI provides much of localization for free with its Text node, however all of Muse’s codebase is UIKit. Following Apple’s localization documentation, the first step is to add a Localizable.xcstrings String Catalog file into the project.

When you build your project, Xcode will automatically update this strings catalog with all of the localized strings in your codebase. As long as you use Text from SwiftUI or NSLocalizedString, the string keys will appear automatically.

Next, add the new language into the project’s settings:

Now, you’ll see the new language appear in the string catalog that you’ve just added. Xcode shows a fancy editor for the strings file, but what happens if we open as source code instead? the Localizable.xcstrings file is really just a JSON file! And what tool is good with JSON? LLMs.

Step 2: Cursor and Claude

Now that we have our application’s translations as a single JSON file, let’s use AI to automatically translate our app. Open the root folder of your project in Cursor, and open to the Localizable.xcstrings file.

Next, use Command+L to open the Chat interface, and ask to add a new translation to the file. Here’s the prompt I used to translate Muse into German:

Here are the key guidelines for German localization of Muse:

Canonical translation:
- English is the original translation and should be the primary reference

Brand & Common App-Specific Terms:
- "Muse" (never translate)
- "Board(s)" (keep English term)
- "Card(s)" (keep English term)
- "Workspace(s)" (keep English term)
- "Backstage Pass" (keep English)
- File types like "PDF", "URL"
- Technical terms like "debug", "database", "sync", "cache" (only if commonly kept in English when used in German)

Formality & Tone:
- Use informal "du" form rather than formal "Sie"
- Friendly but professional tone
- Direct address to user
- Keep technical explanations simple and clear

Gender & Inclusivity:
- Use gender-inclusive forms with -In suffix where applicable (e.g., "BenutzerIn", "MitarbeiterIn")
- When possible, use gender-neutral terms

Capitalization & Grammar:
- Capitalize nouns per German rules
- Compound words with English terms maintain English spelling (e.g., "Workspace-Karten")
- Maintain English terms in their original form when part of compound words
- Use German quotation marks („") for quotes

UI Elements:
- Button text should be concise
- Menu items use infinitive form
- Error messages should be clear and direct
- Maintain consistent terminology throughout

Special Considerations:
- Preserve emoji and formatting tags (e.g., <highlight>, <small>)
- Maintain placeholder syntax (e.g., %@, %d)
- Keep technical command references (e.g., ⌘, ⇧) unchanged
- Preserve HTML tags and links exactly as in source

Length:
- German translations tend to be longer than English
- Keep UI elements as concise as possible while maintaining clarity
- Consider space constraints in buttons and menus


If you see a string that does not match the guidelines, please generate a replacement JSON that can be applied to the file to replace it with a corrected version. explain the change.

That’s quite a prompt! My goal is for the prompt to give the LLM enough context to know how to translate my app with the correct tone and localization decisions. Each language has its own nuance: gender, plurals, formality, etc. Explain to the LLM how you want your app to be translated for the given language.

For extremely long xcstrings files, I will select the range of keys that I want translated, use Command+Shift+L to add that snippet to the chat context, and ask to translate only that portion. Then I’ll copy/paste into Cursor to replace the selection. Why not use Apply? For extremely long files, Apply can both take too long and use too many tokens for the model’s limited output size.

For all of Muse’s translations, I used Claude Sonnet 3.5.

After updating the xcstrings file, save it in Cursor and open it in Xcode to verify that the JSON is still in the valid format. Save again in Xcode, as Xcode will format it with alphabetical language keys, whitespace, etc. Commit to your repo and off you go!

For Muse, a codebase with ~700 localized keys, it takes me ~2 hours to translate all of them with AI. I could probably optimize my flow to take less than an hour, but that’s only 10-15 hours total for 5 languages!

Step 3: Upload to POEditor

At this point, we have a fairly accurate translation from an AI – but we don’t know how accurate it actually is. In my first pass at German, I didn’t know about its gender or formality, so after a volunteer looked it over and gave their first feedback, I re-ran the AI with a better prompt to get a more accurate translation. Now the volunteers have fewer strings to manually edit.

But how do we get these strings to our volunteers or translation team? I use POEditor to coordinate with volunteers on Muse’s translations.

To get the translations from Xcode into POEditor, use the Export Localizations item in the Product menu.

This will export a folder at the location you choose with 1 bundle file per language that your app supports. POEditor doesn’t handle xcloc bundle files, but it does handle xliff files. To get to your xliff file, right click the bundle and choose Show Package Contents, and find the xliff file in the Localized Contents subfolder.

Upload this xliff file to the same language in POEditor. Note: There’s two different places to upload in POEditor, there’s an import link at the project level, and another import link in the specific language – you want the import link in the language.

Choose to overwrite any existing translations in POEditor, save and upload, and enjoy all of your strings in POEditor!

Step 4: Review translations

Next, add your contributors to POEditor for the language. These are the people who’ll be able to view and make changes to all of the translations.

Once your volunteers or translation team has validated and corrected your translation, it’s time to get their work back into Xcode.

Step 5: Export from POEditor

In POEditor, navigate into the language and choose its Export link. In the export menu, choose the xliff file format, and export to overwrite the exact same file you uploaded to POEditor.

Then, from Xcode, choose Import Localizations from the Product menu and select the Localizations folder that it had exported before. Since you’ve just overwritten the language’s xliff file inside that same folder, Xcode will now read in and use all of those new translations.

Congratulations! You’ve just translated your app into another language using AI, POEditor, and a small focused dose of human help.

Wrap up

This is a long post, but the core idea is simple:

  • Use xcstring files in Xcode to get auto-updated translation keys, including localized plurals
  • Use Cursor and AI to get a draft translation for your new language
  • Upload to POEditor and validate with human helpers
  • Download and import back into Xcode, and enjoy!

Muse is available today in English and French, and soon in German and Spanish as well.