Software localization
A Step-by-Step SwiftUI Tutorial
At the WWDC 2019, Apple surprised the developer community by introducing SwiftUI, a modern, declarative API for building user interfaces for all Apple platforms. Ever since, the tech giant from Cupertino has continuously improved the framework, making it a compelling solution for building out production apps.
In light of this popularity, the following SwiftUI tutorial seeks to outline the key steps in localizing iOS apps built with SwiftUI. To keep things straightforward, we will be building a simple application along the way. At the time of writing, we are using Xcode 12.3. Make sure you use the latest version when referring to this tutorial.
Basic setup
To get the ball rolling, open Xcode and create a new iOS project. We will call ours 'SwiftUILocalization'. This is just a simple application that will help us get a better understanding of how to approach localization in SwiftUI. Our app's base language will be English while supporting French and Chinese at the same time.
Once you have created the project, from the project navigator, select the project name, and click on the Info tab. Under Localizations, click the plus symbol to add the languages and regions you want to support, in our case, Simplified Chinese and French.
In the sheet that comes next, select the resource file you want to localize – LaunchScreen.storyboard in our case – and click Finish. This is a basic setup that applies to localizing iOS apps built with UIKit as well.
Localization in SwiftUI works automatically, out of the box. Quite amazing, right? This is possible because most SwiftUI views and view modifiers take up LocalizedStringKey struct as an argument. More on this later on.
Let us move on to create a string file called 'Localizable' that will hold the text we want to localize. Choose File → New → File ..., select Strings File under Resources, and click Next. Name it Localizable, then click on Create.
Localizing UI strings
In ContentView.swift, we have a text view whose string would need to be localized eventually.
The 'Hello, World!' string inside the text view is the key that would be used to look up the translated version of the text in the Localizable.strings file. Usually, if the LocalizedStringKey API did not find any matching text for the key, it would display the key in the text view for an app that supports localization.
This comes as a result of LocalizedStringKey struct conforming to the ExpressibleByStringInterpolation protocol, which in turn inherits from the ExpressibleByStringLiteral protocol. This allows you to use a string literal, like 'Hello, World!', in a text view, that is turned into an instance of LocalizedStringKey struct automatically. Basically, you can either use Text("Hello, World!")
or Text(LocalizedStringKey("Hello, World!"))
and you will still achieve the same result.
Note that if you initialize the text view with a string literal, it will use the init(_:tableName:bundle:comment:)
initializer, which interprets the string as the key and uses it to search the table you provide for a matching pair. It would use the default table Localizable.strings if you provided none. However, if you initialized the text view with a variable name, the view would use the init(_:)
initializer, which does not localize the string.
You can specify the table name for the localization file, the bundle containing the string file (it would use the main bundle if you did not provide one), and the comment containing the contextual information about the key-value pair for your localizers in the text view initialization above.
If your app is pretty large, with many chunks of content for localization, you can decide to split it into different .strings files, and indicate the file name as the table name. In the Localizable.strings file (English), add the following:
"hello-title" = "Hello, World!";
In your text view, use the newly added key to initialize the view, run the app, and you will get 'Hello, World!' displayed on the screen. Do not forget to add the semicolon at the end of your key-value pair in your .strings file, otherwise, Xcode will trigger a build error with invalid input.
Your ContentView.swift file should look like this so far:
import SwiftUI struct ContentView: View { var body: some View { Text("hello-title") .padding() } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } }
Localizing interpolated strings in SwiftUI
SwiftUI supports string interpolation out of the box since LocalizedStringKey conforms to the ExpressibleByStringInterpolation protocol that allows us to inject dynamic data into our string value.
Here is an example for a better understanding. Let's say we want to display something like 'My name is John' in the text view, where the name is dynamic, perhaps returned from an API call to the backend. However, in our example, we will just have a variable name of String type. Add the following to your Localizable.strings (English) file.
"title-name %@" = "My name is %@";
In the ContentView.swift file, introduce a new variable name for the type string var name = "John"
. Your ContentView.swift file will then look as follows:
struct ContentView: View { var name = "John" var body: some View { Text("title-name \(name)") } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } }
When you run the app, you should see 'My name is John' displayed on the screen. Now, let us experiment a little bit. Assign an integer value of six to the name variable, and try to run the app again. What do you notice? The key is displayed on the screen. The reason for this is because %@ is a specifier for an object (which includes strings). We will have to use a different specifier for int to get it working as it should be. In your Localizable.strings file, change the specifier to for title-name from %@ to %lld so that the line looks like as follows:
"title-name %lld" = "My name is %lld";
Try running the app again, and everything will look good again. To learn more about String formatters, kindly check out format specifiers.
Handling plurals in SwiftUI localization
In order to create a plural variant of the localizable strings you pass to LocalizedStringKey, you need to add a stringsdict file to your project. Adding plural variants comes in handy when you want to render something like 'one apple' in your view when you have only one apple and 'ten apples' when you have ten of those.
Let us add a stringsdict file to our project. Choose File → New → File ... and make sure you are in the right target operating system (iOS in this case). Select Stringsdict File under Resources and click Next. In the sheet that appears, type in Plurals as the file name and click Create.
Now, let us edit the file we just added. Localized String Key is the key that we will be passing to the LocalizedStringKey struct. Change this to fruit-count %lld
.
Change the NSStringLocalizedFormatKey value %#@VARIABLE@
to %#@apple@
, and change the VARIABLE dictionary below to 'apple'. Expand the 'apple' dictionary and add a lld string formatter as the value to the NSStringFormatValueTypeKey. This string format specifier will vary depending on the type of data you are dealing with. In our case, we are handling Int, which is why we use lld.
We now have to define our plural variants by entering our formatted strings in the 'zero' to 'other' categories we have. We are only concerned with two cases, 'one' and 'other'. For 'one', add the value %lld apple
, and for 'other', add the value %lld apples
. Go ahead and remove the other categories you are not using. If you have done everything correctly, your modified file should look as shown below.
Let us test to see if everything works as expected. However, before we do that, we will be introducing the use of VStack in our view body to help us render two text views. Introduce a new variable appleCount of type Int. Inside the VStack closure, add another text view i.e Text("fruit-count \(appleCount)")
.
Your code structure should now look as follows:
import SwiftUI struct ContentView: View { var name = "John" var appleCount = 2 var body: some View { VStack { Text("title-name \(name)") Text("fruit-count \(appleCount)") } } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } }
Try testing the app. It seems that it does not work as expected, what could have been the cause? Well, the reason why we had fruit-count 2, which is our key displayed in the second text view, is that we named our stringsdict file 'Plurals'. The default lookup table name is usually 'Localizable.stringsdict'. If we had named our stringsdict file 'Localizable.stringsdict,' everything would have worked as expected. To get it working, we have to specify the Plurals as the tableName without the extension part in our second text view initialization, i.e. Text("fruit-count \(appleCount)", tableName: "Plurals")
Test again, and everything will work fine.
Great work so far! Let us move forward and add a twist to it. Suppose you wanted to display other types of fruit, like orange and mangos, i.e. display something like '2 apples,' '1 mango,' or '3 oranges'.
Open the Plurals.stringsdict file, and change NSStringLocalizedFormatKey to %#@apple@
, %#@mango@
and, %#@orange@
, the way we want it rendered ('2 apples,' '1 mango,' and '3 oranges'). Duplicate the 'apple' variant into 'mango' and 'orange,' and make the necessary changes.
And since we will be passing in three variables in our string interpolation in our text view, we need to add two more %lld string format specifiers to our Localized String Key, i.e. fruit-count %lld %lld %lld
.
The newly modified file should look like this:
All good to go. Inside our code, we need to introduce two more variables to keep the count of mango and orange. Update your code to the one below.
import SwiftUI struct ContentView: View { var name = "John" var appleCount = 2 var mangoCount = 1 var orangeCount = 3 var body: some View { VStack { Text("title-name \(name)") Text("fruit-count \(appleCount) \(mangoCount) \(orangeCount)", tableName: "Plurals") } } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } }
Test out your code once again, and everything should work as expected. It has been quite a learning journey so far, you are doing really great having gotten here! For more information on adding plural variants to your localization, check out this Xcode Help doc.
Adding translations
We now have our app fully prepared for localization. Click on the Localizable.strings file that we created at the very beginning. From the file inspector, choose Localize. A pop-up would ask you if you want to localize the file with the base language English. Click on Localize.
You also have to check the boxes for the other languages, Chinese and French. Once done, you will have three files under the Localizable.strings folder for the base language English, as well as for Chinese and French. Do the same thing for the Plurals.stringsdict file.
For localizing the text content, you should handle the process very carefully because if app localization is done wrongly, it will defeat its purpose. You simply do not want to end up losing users who might feel offended by the content of your app in their native language. That is why we will not be providing translation to the localizable strings in this article. For learning purposes, you can go ahead, and translate the content yourself, but in real life, this is where you should rely on professional translators.
You can use Google Translate or any other machine translation service to translate the texts into Chinese and French, respectively. You only need to change the value and not the key while providing the localized texts for Chinese and French. For example, in your Localizable.strings file, you only need to translate My name is in "title-name %@" = "My name is %@";
to Chinese and French in the Localizable.strings(Chinese, Specified) and Localizable.strings(French)files respectively.
You also have to do the same for the Plurals.stringsdict file. You will only have to translate apple, mango, and orange to Chinese and French in your Plurals.stringsdict(Chinese, Specified) and Plurals.stringsdict(French) files respectively. At this point, you have fully localized your application. Cheers to the good work so far!
Localization testing
SwiftUI is so powerful that you can, actually, preview your app localization on the fly, without having to run it on a simulator or physical device, which definitely helps to cut your development time.
This is particularly helpful when you have many languages and regions you need to support and want to get a feel for their look on the UI. For this purpose, Apple introduced environment variables – one of them dedicated to languages and regions. Let us try to use this environment variable for languages and regions in the preview.
Add .environment(\.locale, .init(identifier: "zh-Hans"))
where zh-Hans is the locale identifier for Chinese, specified to the ContentView instance. Click on the Resume button on the canvas to refresh the canvas if no change is reflected after adding the environment variable. The good thing is that you can test for both the Chinese and French versions at the same time from the preview. All you need to do is to use Group in SwiftUI to group your views. Create a new instance of ContentView and add an environment variable with the 'fr' identifier.
Your ContentView_Previews struct will then look as follows:
struct ContentView_Previews: PreviewProvider { static var previews: some View { Group { ContentView() .environment(\.locale, .init(identifier: "zh-Hans")) ContentView() .environment(\.locale, .init(identifier: "fr")) } } }
You can now test out for Chinese and French in the preview simultaneously. Quite impressive, right?
Note that changing this language identifier in your environment variable will only be reflected in the preview in the canvas. However, if you want to test if your localization is working well in a simulator or real device, then follow the steps below.
Select Edit Scheme → Run → Options and change the Application Language to any language you want to test for. You can also select the app region. After you edit your settings, run the app again.
Exporting localizations
The next step is to export localizations by selecting the languages you want to support, and send them off to your localizers. Follow the steps below.
In the Project navigator, choose the project, then Editor → Export for Localization.
In the dialog box, enter a folder name, select a location where you want it exported, and uncheck English because we do not want to translate that, and finally, click Export.
Xcode creates an Xcode Localization Catalog folder (with .xcloc extension) for each of the languages. Each of these folders contains the resources and assets you marked localizable. You can then send the catalog folder to your localizers to start work immediately. The .xcloc folder contains the .xliff (standard XML Localization Interchange File Format ) file with the extracted strings that our localizers can start translating.
Importing localizations
Once your localizers are done with localizing the contents in your catalog folder, they will send it back to you, and all you need to do is import it back to your project. Xcode handles everything for you. In the Project navigator, choose the project, then choose Editor → Import Localizations. If you have more than one .xcloc folder and they are contained in one folder for all the languages, select the folder, and then import the individual .xcloc folders back to your project. Once imported, your app is fully localized.
Calling it a day
Well done! If you have gotten this far, you have done an amazing job, following all the steps of our SwiftUI tutorial on localization. Now that you have your app ready for localization, you can let a localization solution, such as Phrase, do the heavy lifting.
A professional localization solution, the Phrase Localization Platform features a flexible API and CLI, and a beautiful web platform for your translators. With GitHub, GitLab, and Bitbucket sync, Phrase does the heavy lifting in your localization pipeline, so you can stay focused on the code you love.
Check out all Phrase features for developers and see for yourself how it can help you take your apps global.
If you want to learn more about iOS app localization, we recommend the following guides: