Xcode String Catalogs 101

 ยท 10 min
Marjan Blan on Unsplash.com

Xcode String Catalogs

Localization, or l10n, is an important step for any app to reach a broader market and be more inclusive.

Up to Xcode 14, localizing your Strings always felt like a chore, as they were saved in multiple plain-text key-value .strings files that easily broke your build if there was a mistake. Supporting variations on plural required .stringsdict files that were property lists internally.

With Xcode 15, the IDE gained a versatile and easy-to-use way of handling localization: String catalogs.


How to Localize an App

Regardless of the chosen language or framework, l10n mostly boils down to using String-based identifiers to look up the correct translation instead of using String literals directly in your code.

For example, Java has Property files that append their Locale to their name, like TransactionComponent_de.properties:

# This is a comment
content=Das allerbeste Angebot!
action.buy=Kaufen
action.cancel=Abbrechen

Xcode uses a similar system with its .strings-files:

/* This is a comment */
"content" = "Das allerbeste Angebot!";
"action.buy" = "Kaufen";
"action.cancel" = "Abbrechen";

As you can see, there’s a lot more ceremony involved, like double quotes and semicolons. That’s why you could break your build with a missing semicolon in a .strings-file!

If you needed a variation based on counts, like “1 item” vs “2 items”, you needed to either handle it yourself with multiple key-value pairs, or create a .stringsdict-file specifying rules based on the count. A better way to do l10n was overdue for a long time…


What’s a String Catalog?

String Catalogs replace the previous .strings and .stringdict files by using a new format and a nice visual editor in Xcode to manage multiple languages and variations. The general concept isn’t new, Xcode uses other catalogs, like Asset catalogs, to manage images, colors, etc.

To add a String Catalog to your project, select File > New > File... and then String Catalog.

A simple String Calatog
A simple String Calatog

You can now start filling the Catalog by hand. But then, it would still be quite a chore, wouldn’t it?

Instead, we let Xcode do the heavy lifting and concentrate on developing our apps.

Letting Xcode Do The Work

The “Build Settings” have a new option under “Localization” called “Use Compiler to Extract Swift Strings”.

It does what it says on the box: it extracts all localizable Strings during each build and adds them to the catalog, or if already present, updates them.

Build Settings
Build Settings

However, extracting all Strings without any differentiation would make little sense, as it would pick up a lot of non-localizable String, too, like identifiers, notification names, NSPredicate formats, etc. Thankfully, the automatic extraction only picks up two types of Strings.

The first type is any String created via one of String type’s init(localized: ...) initializers.

swift
// LOCALIZED
let localized1 = String(localized: "Localized text")
let localized2 = String(localized: "Another localized text",
                        defaultValue: "This is the default localization",
                        comment: "There are 5 localizable inits")

// NON-LOCALIZED
let nonLocalized1 = "English-only plz"
let nonLocalized2 = String("Also English-only plz")

The different initializers give you various options, like the default value, the bundle, the locale, etc.

The second type of automatically extracted Strings is any String literal used in SwiftUI:

swift
struct SwiftUIView: View {
  var body: some View {
    HStack(spacing: 22) {
      Text("content.text")

      Button("action.buy") {
        // ...
      }

      Button("action.cancel") {
        // ...
      }
    }
  }
}

If you don’t want a String literal to be interpreted as a possible localization key, you can use Text(verbatim: "...") instead:

swift
struct SwiftUIView: View {
  var body: some View {
    HStack(spacing: 22) {
      Text(verbatim: "Please don't localize")

      Button("action.buy") {
        // ...
      }
  
      Button {
        // ...
      } label: {
        Text(verbatim: "Unlocalized Button")
      }
    }
  }
}

The automatic extraction works bi-directional on each build if you haven’t changed anything of that localization. Change any property, like the translation, its managed status, etc., and it won’t get removed automatically.


Localization Variations

Localization is a useful tool, even without bringing another language into the mix. By aggregating all your app’s Strings into a single location, they’re way easier to proofread and change if necessary.

But there’s still the issue of context-specific variations, like respecting counts. Creating an extra localization key for each variant is another chore Xcode 15 tries to eliminate by providing “variations”.

Vary By Plural

For example, displaying an item count in a shopping cart requires the differentiation between singular, plural, and zero items, even if you only want English:

  • 0 items
  • 1 item
  • 2 items

Before String Catalogs, you either multiple localization and decide in your code which one to use, or use a .stringdict file to retrieve the correct String format specifier, and then build your String. It got the job done, but multiple steps were involved, making it hard to maintain and share with others.

String Catalogs, however, integrate an easy way to vary by counts in a single editor mask without changing your code (much).

First, Xcode needs a localizable String that uses String interpolation:

swift
let itemCount = String(localized: "\(items.count) items")

After the next build, you will find your String catalog has a new key called %lld items.

What happened here is Xcode picked up the localized String and created a key with a format specifier, in this case, long long int.

Now, choose “Vary By Plural” in the context menu:

Vary By Plural in the context menu
Vary By Plural in the context menu

This adds two sub-options: One, and Other.

‘One’ and ‘Other’
‘One’ and ‘Other’

The format specifier part is no longer text and can be positioned as needed. If you go to the context menu again, you can also add another option: Zero.

It might not be as flexible as .stringdict rules, but it should cover most languages and scenarios without introducing unnecessary complexity.

Vary By Device

Varying localizations by singular/plural isn’t the only context that might require a different localization. That’s why there’s also “Vary By Device” where you can split the possible options by devices like iPhone, Apple Watch, Mac, etc., including Other so you don’t have to add each one.

Vary By Device
Vary By Device

The reasoning behind this additional localization axis is manifold.

Due to the available screen real estate, different texts might be required. Or the type of interaction must differ between devices. A button on an iPhone or iPad might be “Tap to learn more”, but on a Mac, using “Click to learn more” is the better choice.

And, of course, “Vary By Plural” and “Vary By Device” can be combined to cover all your bases.

Vary By Device and Plural
Vary By Device and Plural

Once again, almost all your localization needs (that depend on devices and plural) should be covered.


Managing Localizations

You most likely already noticed the little orange “NEEDS REVIEW” badge in one of the previous screenshots. This is the automatic review mechanism of String catalogs.

There are 4 possible states for automatically managed localizations:

  • NEW: a new String that’s not translated yet. These appear in additional languages.
  • NEEDS REVIEW: a String has changed, or was added by a variation, and should be checked if the localization is still correct.
  • STALE: the String is no longer found in your code.
  • TRANSLATED: Doesn’t have a badge, so no action is needed.

As keeping up with changing localizations can be cumbersome, any support from Xcode is welcome. For automatically managed key-values, Xcode is doing its best to keep the state up-to-date with the badges and a little warning about the language itself.

Reviewing String Catalogs
Reviewing String Catalogs

You can use this feature for manually managed ones, too, as they can be marked for review or reviewed via the context menu.

Where this really shines is when more than a single language is involved, as Xcode will show you how much of a language is translated, and even have little green checkmarks for a job well done.

Reviewing String Catalogs
Reviewing String Catalogs

Migrating Your Localizations to String Catalog

New apps are started every day, and many utilize the latest and greatest features available. But what about existing apps already containing .strings- and .stringdic-files?

Well, there’s a “Migrate to String Catalog” option in the context menu that does most of the work.

The migration converts the .strings-file into a String catalog with base localization set in the “Info” tab of the project settings. Even though it creates keys, values, and even comments, it won’t change your actual code. However, comments from a .strings-file used to separate groups might end up at the wrong location, as there is no grouping in the String Catalog editor.

Still, using NSLocalizedString instead of String(localized: ...) works fine, so there’s no actual need to change your code (yet). Using the String initializer feels more Swifty, though, and I highly recommend using them, at least going forward with translations.


Behind The Scenes

Most Xcode’s fancy editors are just property lists or JSON files behind the scenes, and String Catalogs aren’t any different, as the new .xcstrings files are just JSON files:

json
{
  "sourceLanguage" : "en",
  "strings" : {
    "%lld items" : {
      "comment" : "kk",
      "localizations" : {
        "en" : {
          "variations" : {
            "device" : {
              "applewatch" : {
                "variations" : {
                  "plural" : {
                    "one" : {
                      "stringUnit" : {
                        "state" : "needs_review",
                        "value" : "%lld items"
                      }
                    },
                    "other" : {
                      "stringUnit" : {
                        "state" : "needs_review",
                        "value" : "%lld items"
                      }
                    }
                  }
                }
              },
              // ... snip ...
            }
          }
        }
      }
    },
    "action.buy" : {
      "extractionState" : "stale",
      "localizations" : {
        "en" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Buy"
          }
        }
      }
    },
    "action.cancel" : {
      "extractionState" : "manual",
      "localizations" : {
        "de" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Abbrechen"
          }
        },
        "en" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Cancel"
          }
        }
      }
    },
    // ... snip ...
  },
  "version" : "1.0"
}

Being JSON makes it harder to be “human-readable”, but makes it easily “machine-readable”, which is why we got the fancy editor in Xcode in the first place. Also, what I’m looking forward to is the possibility of additional tooling thanks to a more meaningful format, so .xcstrings-files can be shared with translators who don’t or can’t use Xcode directly.


Conclusion

Making Localization easier is a step in the right direction, as it gives smaller and indie developers an easy-to-use tool for translations. Before that, you had to develop your own workflow or rely on third-party software/services to make it more manageable. Making it easier also (hopefully) means more apps will be translated in the future, so the whole ecosystem might get more inclusive.

Thanks to the new JSON-based filed format, many new tools will spring forth to integrate l18n more tightly in CI/CD pipelines, like a GitHub action giving a detailed report on the current state of an app’s translation.

Even without an actual translation into another language, String catalogs aggregate all of the app’s Strings into a single place with review capabilities, making them a powerful new tool for my projects working with customers.

For me, Xcode isn’t my favorite IDE, at least compared to other IDEs like the different JetBrains ones or even Eclipse. But in my opinion, String Catalogs is a fantastic and well-thought-out addition that makes the previous chore of localization much more bearable and actually a joy to use.


Resources