String localisation is the most important step to take for making your app accessible to users across the world. By translating your app's copy to multiple languages, you are able to provide the user experience that your customers expect in locations around the globe. Today we will take a look at how the process of string localisation has been managed on Apple platforms up until now, and then take a dive in to the advancements that have been made in Xcode 15.
App localisations broadly come in two flavours - simple messages that may optionally contain placeholders for content provided at runtime, as well as more advanced examples that vary their message based on pluralisation rules or by the type of device that the app is running on. Let's start with the simple ones:
"welcome_message" = "Welcome!";
"hello_format_string" = "Hello, %@";
"review_rating" = "%1$d out of %2$d people found this helpful";
These simple messages live in a .strings
file, which maps a localisation "key" to a value. In the example above, the "welcome_message"
key maps to a welcome message ("Welcome!"
). We access the value of this localisation in code using the NSLocalizedString
function:
/// Produces "Welcome!"
NSLocalizedString("welcome_message", comment: "")
The second of these messages, the one keyed by "hello_format_string"
contains a placeholder (the %@
portion of the text). This allows a value to be inserted in to the string at runtime, like the user's name:
let name = "Emily"
/// Produces "Hello, Emily"
String.localizedStringWithFormat(NSLocalizedString("hello_format_string", comment: ""), name)
In the final example we have a localisation that contains two placeholders that have positional specifiers, %1$d
referring to the first placeholder, and %2$d
referring to the second:
/// Produces "3 out of 4 people found this helpful"
String.localizedStringWithFormat(NSLocalizedString("review_rating", comment: ""), 3, 4)
Why would we want to specify positions like this? Because in some languages it may be preferable to have the order of the arguments reversed i.e the total number of people before the number for people who found the review useful. Code to check the language and swap the parameters passed to localizedStringWithFormat
would be error prone, and would need to be revisited whenever new localisations or languages are added to an app. Instead, we have positional specifiers to do the job. When a translation is provided for a language that would prefer the order of the arguments to be swapped, then its value simply has the %2$d
placeholder positioned before the %1$d
placeholder.
Speaking of other languages, how do we provide translations for them? By duplicating our .strings
file and replacing the values with the correctly translated copy. Here's how our messages would look if translated to French:
"welcome_message" = "Bienvenue!";
"hello_format_string" = "Salut, %@";
"review_rating" = "%1$d personnes sur %2$d ont trouvé cela utile";
Each of these copies of the .strings
file must be kept in a separate directory, keyed by the locale specifier for the language:
This produces the following in our Xcode project:
That's how we handle simple localisations, but what about the more complicated ones that vary by plurals and device types? These are handled by .stringsdict
files. Let's take a look at an example where we would want to vary a piece of text based on a pluralised item it contains. We may wish to have a string that says something along the lines of "You have 5 new messages". We could use a simple localisation like we saw earlier, something along the lines of "new_messages" = "You have %d new messages"
. However, if the user only has one new message, this would produce "You have 1 new messages", whereas in this scenario we would want it to say "You have 1 new message". A .stringsdict
file allows us to cater for both of these scenarios:
Our string "key" here is "new_messages"
, which we use in the same way as we did with the simple localisation string keys:
/// Produces "You have 1 new message"
String.localizedStringWithFormat(NSLocalizedString("new_messages", comment: ""), 1)
/// Produces You have 2 new messages
String.localizedStringWithFormat(NSLocalizedString("new_messages", comment: ""), 2)
That's pluralisation, but what about device variation? .stringsdict
has us covered:
This will vary the message based on device. By default, the message will be "Tap here", but on macOS it will be "Click here". Variations can be provided for all Apple device types, such as Apple Watch, Apple TV, iPad etc. We provide translations for these more complex localisations in the same way as we did with the simpler ones in the .strings
files - we duplicate the .stringsdict
files and store them in the locale-keyed subdirectories.
This concludes our brief overview of the state of string localisation on Apple platforms as it has existed since Mac OS X was introduced. It has served us well, but is not without its problems.
First of all, the primary purpose of string localisation is to support translation in to different languages. However, the process for doing this revolves around duplicating files. This presents problems for keeping these files in sync. Any new localisations need to be entered in to each file for translation - it doesn't take much to simply miss a file or not to notice when a translation hasn't been provided in one of them. As for strings with pluralised or device variants, the file format is a property list for which Xcode's editor leaves a lot to be desired. It is easy to accidentally misconfigure something and break your app's localisations for a particular language. There must be a better way than this…
Xcode 15 introduces a new way to manage our string localisations, in the form of a new .xcstrings
file type, which is referred to as a String Catalog. Xcode 15 also provides a simple migration process to move our old .strings
and .stringsdict
files over to .xcstrings
files. Let's see what our examples from earlier look like when migrated to a string catalog:
We now have a single file for both our simple and complex localisations, and can easily see the placeholders as well as different variations based on plurals and devices.
But what about other languages? Do we need to duplicate the .xcstrings
file like we did before? No! Instead, we add new languages to the same file and can see them alongside our default language strings. Let's take a look at how this works by adding French localisations to our .xcstrings
file. We start by selecting the language we want to add from the + button in Xcode's editor:
Having selected this new language, we have a new set of localisations in our .xcstrings
file:
Xcode has marked all of these localisations as "New" because we have not yet provided the translations for this new language. It is therefore easy for us to see which ones need updating, especially compared to simply duplicating a .strings
file. We can also see that 0% of the required translations have been provided, so let's add them now:
Voila! We can easily see that we have provided all of the translations needed for the French version of our app. What's more, if we update any of the strings in the English version of our localisations then the corresponding strings for the other languages that our app supports will be flagged as needing review. Again, this makes it much simpler to keep localisations up to date and in sync compared to the old system.
The .xcstrings
file editor has a few more tricks up its sleeve. Let's add a string that has not one, but two plural variations. We may want to give the user a detailed description of how many new messages they have, that also includes the number of inboxes that the messages span. For this, we will need to vary the text based on two different pluralisations - the number of messages, and the number of inboxes. Let's update our "new_messages"
localisation to support this:
We've updated our localisation to include two numerical placeholders. If we right click on the localisation, we are given the option to provide plural variations for both of these placeholders:
By default, our placeholders are renamed to @arg1
and @arg2
, but we can given them any names we like, so let's rename them to something more meaningful:
As you can see, we can provide specific plural variations for each part of the string. Again, we make use of this using the same API that we did before:
/// Produces You have 14 new messages in 2 inboxes
String.localizedStringWithFormat(NSLocalizedString("new_messages", comment: ""), 14, 2)
/// Produces You have 1 new message in 1 inbox
String.localizedStringWithFormat(NSLocalizedString("new_messages", comment: ""), 1, 1)
Notice also that our French localisations have now dropped to a completion rate of 80%. That is because we modified the "new_messages"
localisation in the base language (English), so the corresponding French translation has now been marked so that we know it needs to be updated.
Further than this, you can even have strings varied by device, the variations of which also contain multiple plural variations with substitutions. If your copywriting team can dream it up, it can almost certainly be supported by String Catalogs.
You may be thinking that this is too good to be true. String Catalogs are so much better than managing multiple .strings
and .stringsdict
files, surely there must be a fly in the ointment somewhere. But no! When Xcode builds your app with String Catalogs, it converts the catalogs in to .strings
and .stringsdict
files as part of the build process. This means two things - one, you don't have to change your code when migrating to String Catalogs, your existing calls to NSLocalizedString
will continue to work. And two - because String Catalogs are converted to .strings
and .stringsdict
files automatically, there is no minimum iOS version requirement for using them! You can convert to String Catalogs as soon as you start using Xcode 15, without affecting your app's minimum deployment target. Nice!
String Catalogs represent a great improvement in how we manage the localisation process for our apps. We no longer have to manage a proliferation of files, and can easily see when our translations are complete and up to date. As soon as you can switch to Xcode 15, give String Catalogs a go, and make life easier for yourself and your localisation team.