# General Translation Knowledge Base: How do I localise an iOS app with String Catalogs?
URL: https://generaltranslation.com/en-GB/kb/localize-ios-app.mdx
Docs index: https://generaltranslation.com/llms.txt
Description: Add your languages in Xcode, write user-facing text with localisable APIs such as SwiftUI Text and String(localized:), and build so Xcode collects those strings into a String Catalog (.xcstrings). Translate in Xcode, through XLIFF, or with General Translation's CLI when catalogues should update with releases and share terminology with your website and Android app.
Category: Internationalisation
Last updated: 2026-10-20
Authors: Kevin Wu, Jackie Chen
Published: 2026-10-20

A String Catalog holds one string table in every language in a single JSON file, and Xcode syncs it with your code on every build. That leaves you two jobs: getting every user-facing string into the catalogue, and getting good translations back into it.

## Set up localisation in Xcode

### Add your languages

In the project editor, select the project, open **Info**, and click the add button under **Localizations** (the String Catalog editor&#39;s sidebar has one too). Each entry has a language ID such as `de`, `ja`, or `en-IN` ([Apple: Adding support for languages and regions](https://developer.apple.com/documentation/xcode/adding-support-for-languages-and-regions)). Choose a regional variant only when the text differs, as with `pt-BR` and `pt-PT`. (See [locale codes](/docs/platform/core/guides/locale-codes)).

### Add a String Catalog and build

Choose **File &gt; New &gt; File from Template**, filter for &quot;string&quot;, and pick **String Catalog**. The default name is `Localizable.xcstrings`. Then choose **Product &gt; Build**: Xcode discovers the localisable strings in your code and adds them to the catalogue, and it updates the catalogue on every later build ([Apple: Localizing and varying text with a string catalog](https://developer.apple.com/documentation/xcode/localizing-and-varying-text-with-a-string-catalog)).

String Catalogs arrived in Xcode 15 ([release notes](https://developer.apple.com/documentation/xcode-release-notes/xcode-15-release-notes)). At build time they compile to `.strings` and `.stringsdict` files, so adopting them doesn&#39;t require raising your minimum deployment target ([WWDC23: Discover String Catalogs](https://developer.apple.com/videos/play/wwdc2023/10155/)).

### Write text with localisable APIs

Xcode only extracts strings passed to localisable APIs. In SwiftUI, string literals in views are [`LocalizedStringKey`](https://developer.apple.com/documentation/swiftui/localizedstringkey) values and are extracted automatically. Elsewhere, use `String(localized:)` ([Apple: Preparing your app&#39;s text for translation](https://developer.apple.com/documentation/xcode/preparing-your-apps-text-for-translation)):

```swift
import SwiftUI

struct CartView: View {
    let itemCount: Int
    let couponCode: String

    var body: some View {
        VStack {
            // String literals in SwiftUI views are LocalizedStringKey values,
            // so Xcode adds them to the String Catalog when you build.
            Text("Your cart")

            // Interpolation becomes a format specifier (%lld) that you can vary by plural.
            Text("\(itemCount) items")

            // A comment tells translators where and how the text is used.
            Text("Checkout", comment: "Button that starts payment for the items in the cart.")

            // A String variable is displayed as-is. Text(verbatim:) makes that explicit.
            Text(verbatim: couponCode)
        }
    }
}

// Outside SwiftUI views, create localizable text with String(localized:).
let emptyCartMessage = String(
    localized: "Your cart is empty",
    comment: "Shown when the cart has no items."
)
```

The common miss is `Text(someString)`: a `String` variable isn&#39;t looked up in the catalogue. To pass localisable text between views, use `LocalizedStringResource`, which Apple recommends and which also carries a comment and table name.

Comments matter because &quot;Checkout&quot; alone could be a noun or a verb. Add them with the `comment:` parameter, or Control-click a key in the catalogue and choose **Generate Comment** to have Xcode write one from the surrounding code.

To find text that never reached the catalogue, open **Product &gt; Scheme &gt; Edit Scheme &gt; Run &gt; Options** and turn on **Show non-localized strings**; those strings render in all caps when the app runs ([Apple: Preparing your interface for localization](https://developer.apple.com/documentation/xcode/preparing-your-interface-for-localization)). (See [How do I find hardcoded strings that aren&#39;t translated?](/kb/find-hardcoded-strings)).

## Handle plurals, devices, and Info.plist

Control-click a key with an interpolated number and choose **Vary by Plural**. Xcode adds the plural categories each language needs: `one` and `other` for English, and `one`, `few`, `many`, and `other` when you add Russian. **Vary by Device** gives a key a different value per device, such as &quot;Tap to learn more&quot; on iPhone and &quot;Click to learn more&quot; on Mac.

Under the editor, the catalogue is JSON, which diffs cleanly in pull requests. Here&#39;s a small excerpt with an English plural and a Spanish translation:

```json title="Localizable.xcstrings"
{
  "sourceLanguage" : "en",
  "strings" : {
    "%lld items" : {
      "comment" : "Number of items in the shopping cart.",
      "localizations" : {
        "en" : {
          "variations" : {
            "plural" : {
              "one" : {
                "stringUnit" : {
                  "state" : "translated",
                  "value" : "%lld item"
                }
              },
              "other" : {
                "stringUnit" : {
                  "state" : "translated",
                  "value" : "%lld items"
                }
              }
            }
          }
        }
      }
    },
    "Checkout" : {
      "comment" : "Button that starts payment for the items in the cart.",
      "localizations" : {
        "es" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Pagar"
          }
        }
      }
    }
  },
  "version" : "1.0"
}
```

System-facing text in `Info.plist`, such as the app&#39;s display name and the usage descriptions in permission prompts, goes in a separate catalogue. Add an `InfoPlist.xcstrings` file to the target, and Xcode fills it with the known localisable `Info.plist` keys on each build.

## Track string states

Each translation in the catalogue has a state, shown in the editor&#39;s **State** column:

* **New:** not yet translated into that language.
* **Needs Review:** the source changed after translation, or someone flagged it; choose **Mark as Reviewed** to keep the current value.
* **Translated:** a green tick; nothing left to do.
* **Stale:** Xcode can no longer find the string in code. Untranslated strings that disappear from code are removed; translated ones are kept and marked stale until you delete them.

Strings you add by hand, such as keys built at run time, can be set to manual management, so builds never update or remove them.

## Migrate from .strings and .stringsdict

To convert an existing project, choose **Edit &gt; Convert &gt; To String Catalog** (or Control-click a file and choose **Migrate to String Catalog**). Migration runs per file and per target, and catalogues can coexist with legacy files while you migrate. A catalogue and a `.strings` or `.stringsdict` file with the same name in the same target is a build error, so finish each table before moving on. Migrating `InfoPlist.strings` keeps its comments when it has them.

## Choose how to translate

These approaches put translations into the catalogue:

| Approach                                                                          | Who translates                | Context the translator gets                                                                 | Repeatable on every release                                                   | What it leaves to you                                                                                    |
| --------------------------------------------------------------------------------- | ----------------------------- | ------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
| Type in the catalogue editor                                                      | You or a bilingual teammate   | The whole app                                                                               | No                                                                            | Every string in every language, by hand                                                                  |
| Xcode coding agent (Xcode 27)                                                     | The agent you enable in Xcode | Where each string is used, similar strings, plural needs, Apple&#39;s language style guides | Run per request in Xcode                                                      | Agent set-up, reviewing machine-translated strings, keeping terms in sync with your web and Android apps |
| Export `.xcloc` and XLIFF to translators or a translation management system (TMS) | Professional translators      | Comments, plus screenshots you add to the `Notes` folder                                    | Scriptable with `xcodebuild`                                                  | A round trip per release, and waiting on it                                                              |
| General Translation CLI                                                           | GT&#39;s translation service  | Catalogue comments plus shared Glossary and Custom Prompts                                  | Run [`gt translate`](/docs/cli/reference/commands/translate) before the build | Review in the Dashboard or Xcode, then test the app                                                      |
| Script machine translation or an LLM over the JSON                                | A model or translation API    | Whatever your script sends                                                                  | Yes                                                                           | Variations, substitutions, plural categories per language, format specifiers, states, and review         |

**Exporting.** **Product &gt; Export Localizations** creates one Xcode Localization Catalog (a `.xcloc` folder) per language, with an XLIFF file inside ([Apple: Exporting localizations](https://developer.apple.com/documentation/xcode/exporting-localizations)). Turn on the **Use Compiler to Extract Swift Strings** build setting so the export includes every Swift string. Bring translations back with **Product &gt; Import Localizations**, or script both directions with `xcodebuild -exportLocalizations` and `xcodebuild -importLocalizations` ([Apple: Importing localizations](https://developer.apple.com/documentation/xcode/importing-localizations)).

**What changed in Xcode 27.** Agents in Xcode can now translate String Catalogs: Xcode adds the languages, creates any missing String Catalogs, and gives the agent context for each string ([Xcode 27 release notes](https://developer.apple.com/documentation/xcode-release-notes/xcode-27-release-notes), [WWDC26: Translate your app using agents in Xcode](https://developer.apple.com/videos/play/wwdc2026/213/)). The catalogue editor gains a **Generate Translations** button, agent output gets the **Machine Translated** state, and exported XLIFF marks it with `state-qualifier="leveraged-mt"`. A localisation comment of &quot;do not translate&quot; now marks a string **Don&#39;t Translate** in the catalogue and `translate="no"` in XLIFF. You can point `AGENTS.md` or `CLAUDE.md` at a `TRANSLATION.md` file with a glossary and a list of strings the agent shouldn&#39;t translate ([Apple: Localizing your app using agents](https://developer.apple.com/documentation/xcode/localizing-your-app-using-agents)).

Use the catalogue editor for one or two languages you speak. Use Xcode&#39;s agent when you want machine translation inside Xcode. Choose General Translation when a repeatable repository command and shared terminology across products matter; you keep using Xcode and Apple&#39;s native localisation APIs. Use an XLIFF export when professional translators or an existing [TMS](/kb/what-is-a-translation-management-system) own the work.

## Test every language

Choose **Product &gt; Scheme &gt; Edit Scheme**, select **Run**, then **Options**, and set **App Language** and **App Region** for the next run ([Apple: Testing localizations](https://developer.apple.com/documentation/xcode/testing-localizations-when-running-your-app)). The same **App Language** menu lists pseudolanguages: **Double-Length** for text expansion, **Right-to-Left** for mirrored layouts, **Accented** and **Tall** for taller glyphs, and **Bounded String** for truncation. (See [What is pseudo-localisation?](/kb/pseudo-localization)). For SwiftUI previews, set `.environment(\.locale, .init(identifier: "de"))`.

App Store metadata (name, description, screenshots) is localised separately in [App Store Connect](https://developer.apple.com/help/app-store-connect/manage-app-information/localize-app-information).

## Translate String Catalogs with General Translation

When translation should run as a repeatable command and terminology should match across your iOS app, website, and Android app, General Translation&#39;s CLI, `gt`, translates `.xcstrings` files in place (`gt` 2.21.0 or later). Add an `xcstrings` entry to `gt.config.json` that points directly at each catalogue:

```json title="gt.config.json"
{
  "defaultLocale": "en",
  "locales": ["es", "ja"],
  "files": {
    "xcstrings": {
      "include": ["Resources/Localizable.xcstrings", "Resources/InfoPlist.xcstrings"]
    }
  }
}
```

Every locale lives in the same file, so the `include` paths have no `[locale]` placeholder, and the catalogue&#39;s `sourceLanguage` must match `defaultLocale` or the run stops before uploading. Sign in with [`npx gt login`](/docs/cli/reference/commands/login), set `GT_PROJECT_ID` ([CLI credentials](/docs/cli/guides/configuring#credentials)), and, for an existing app, import the source and reviewed translations with [`npx gt upload`](/docs/cli/reference/commands/upload). Then run [`npx gt translate`](/docs/cli/reference/commands/translate) to generate updates. In CI, store a project API key with **Files &gt; Write** and **Translation queue &gt; Enabled** as `GT_API_KEY`.

The CLI uploads only the source-language slice and merges each target locale back into the file, preserving unrelated locales and catalogue structure. Its String Catalog processing uses the target language&#39;s plural categories when translating variations and preserves format specifiers and substitutions. (See the [`.xcstrings` reference](/docs/cli/reference/formats/xcstrings-files).) For the legacy layout, the `dotStrings` and `dotStringsdict` keys translate `.strings` and [`.stringsdict`](/docs/cli/reference/formats/dot-stringsdict-files) files instead.

Review the catalogue in Xcode or use the Dashboard&#39;s [raw file editor](/docs/platform/dashboard/guides/reviewing-translations). If you edit translations in Xcode, run [`npx gt save-local`](/docs/cli/reference/commands/save-local) so later runs keep them. Assign one [Context Group](/docs/platform/dashboard/guides/defining-context-for-translations) to your iOS, web, and [Android](/docs/cli/reference/formats/android-strings-files) projects, and a Glossary term like your plan names stays the same on every surface.

Ramp uses GT for 11+ locales across its dashboard, landing page, and iOS app, an example of the same translation platform serving web and native products. (See [production deployments](https://generaltranslation.com/enterprise).)

## Key takeaways

* Build once after adding `Localizable.xcstrings`: Xcode extracts SwiftUI literals and `String(localized:)` text, but not `Text` built from a `String` variable.
* Use **Vary by Plural** for counts; Xcode adds each language&#39;s categories, such as four for Russian.
* Translated strings removed from code become **Stale** instead of disappearing, so clean them up on purpose.
* Xcode 27 adds agent translation, a **Machine Translated** state, and `leveraged-mt` in exported XLIFF.
* General Translation&#39;s CLI translates the catalogue in place with `"xcstrings": { "include": [...] }` and [`npx gt translate`](/docs/cli/reference/commands/translate), preserving unrelated locales and the catalogue&#39;s structure while producing target-language variations.

## Related questions

- [What is pseudo-localisation?](https://generaltranslation.com/en-GB/kb/pseudo-localization.mdx): Pseudo-localisation is a test that swaps your source text for an altered version, accented, padded about 40% longer, and wrapped in brackets, so hardcoded strings, truncation, broken fonts, and concatenated sentences show up before any translation exists. Android ships the en-XA and ar-XB pseudo-locales for it.
- [How do I find hardcoded strings that aren't translated?](https://generaltranslation.com/en-GB/kb/find-hardcoded-strings.mdx): Run a linter that flags user-facing string literals in CI, such as eslint-plugin-i18next's no-literal-string rule for JSX, Android Lint's HardcodedText check, or erb_lint's HardCodedString linter for Rails. Then click through the app in a pseudo-locale to catch text that static checks can't trace.
- [How do I support RTL (right-to-left) languages in CSS?](https://generaltranslation.com/en-GB/kb/right-to-left-css.mdx): Put dir="rtl" and lang on the html element, because lang alone doesn't change direction, then replace physical CSS such as margin-left with logical properties such as margin-inline-start. Flex rows, grid columns, and default text alignment flip on their own; translateX(), left, and right don't.
- [General Translation vs. a TMS (Crowdin, Lokalise, Phrase)](https://generaltranslation.com/en-GB/kb/general-translation-vs-tms.mdx): Choose General Translation when developers want translation generation, code-derived context, and review connected to product releases. Keep your existing catalogues and rendering library, or author inline JSX without translation keys. A TMS fits programmes centred on language vendors, CAT editors, and XLIFF exchange; its integrations can automate the file transfers.

## Previous and next questions

- Previous question: [How do I find hardcoded strings that aren't translated?](https://generaltranslation.com/en-GB/kb/find-hardcoded-strings.mdx)
- Next question: [How do I make a website multilingual?](https://generaltranslation.com/en-GB/kb/make-website-multilingual.mdx)

## Sitemap

See the [Knowledge Base sitemap](https://generaltranslation.com/en-GB/kb/sitemap.md) for every article.
