iOS localization: Step-by-step guide

The process of iOS localization is often tedious and unorganized. But fear not! Let Lokalise help you venture step by step through this undeniably important iOS task in a breeze.

In this tutorial, we’ll focus on enabling localization and going from zero to hero in your Xcode Swift iOS application. We’ll explore adding, managing, and integrating translations in your project and using Lokalise as the translation management platform.

If you’re new to iOS localization, we believe this will be a handy guide to keep in your l10n back pocket; and if you are a seasoned localization master, we’d still recommend going through and taking notes of some specific localization details.

In this tutorial, we’ll cover:

  • iOS localization/internationalization.
  • Using Swift .strings files to add language resources.
  • Utilizing NSLocalizedString, String(localized:), and custom postfix operators to fetch resource values.
  • Using didFinishLaunchingWithOptions or the SwiftUI-2-preferred “application struct initializer approach” to execute tasks on app launch.
  • Setting up a Lokalise project and managing localizations via the Lokalise dashboard, GUI, API, and OTA (over the air).
  • Pluralization using Stringsdict files.
  • Date-time localization with the help of Swift DateFormatter.
  • Programmatically selecting the app language using the Localize-Swift library.

The source code is available on GitHub.

This post was initially written in 2018 and updated in 2021-2022 by Fedya Levkin, Ilya Bodrov-Krukowski, and Dasun Nirmitha.

Part I: Setting up your iOS localization app

Step 1: Create an Xcode iOS application

First up, let’s create a simple iOS app to test out iOS localization functions. Hence, launch Xcode and create an application with the following configuration:

template:                iOS App
Product Name:            iOSSwiftI18n
Organization Identifier: com.lokalise
Interface:               SwiftUI
Language:                Swift

Step 2: Create a Localizable.strings file

Localizable.strings files store application strings as key-value pairs for each language. We’ll create this file for the base language first. Select File → New → File… or just tap Ctrl+N or Cmd+N. Search for strings, and you will see a Strings File in the prompt window:

Choose the Strings File, and name the file Localizable:

Now, let’s select the Localizable.strings file in the Navigator.

In Utilities, you will see a localization panel. Click Localize… within this panel and continue to the next step.

 

Step 3: Adding languages

Let’s add some languages to the application and create .strings files for each language.

Select your root project file, and then proceed to the project panel. Find the Localization section, click the “plus” (+) icon, and add the desired languages. Select only the Localizable.strings file for localization. In this tutorial, we’ll also add the German and Russian languages.

In the Choose files and reference language… dialog select the Localizable.strings file for localization (we are not covering .storyboard localization in this post):

Let’s find the  Localizable.strings using the Navigator. Now you can expand it and see a Localizable.strings file for every language:

Step 4: Working with iOS app localization

Our Localizable.strings files are now ready, so let’s populate these. As previously mentioned, the localization process involves keys and values. A key is a unique identifier that is used in the source code; when the application is served to the user, keys are replaced with the corresponding translations for the language set by the user.

Add translations

Let’s add two entries into our iOSSwiftI18n app’s .strings files:

Localizable.strings (English):

"welcome_screen_title" = "Welcome";
"login_button" = "Log in";

Localizable.strings (German):

"welcome_screen_title" = "Willkommen";
"login_button" = "Einloggen";

Localizable.strings (Russian):

"welcome_screen_title" = "Добро пожаловать";
"login_button" = "Вход";

Use translations

Once the keys and their translations are set up, let’s learn how to use these to programmatically localize our app. To perform this, we will use:

to perform a string localization once our iOSSwiftI18n iOS app finishes launching.

Let’s head over to the iOSSwiftI18nApp class in our iOSSwiftI18n project. Then, we’ll add an initializer inside its IOSSwiftI18nApp struct:

init() {
	print(NSLocalizedString("welcome_screen_title", comment: ""))  // 1
	print(String(localized: "login_button"))  // 2
}

  1. Call the NSLocalizedString function passing in a “welcome_screen_title” key. This call retrieves the localization value for the “welcome_screen_title” key from the default table (default .strings file – in our case the .strings file for the English locale) and returns it.
  2. Pass a String.LocalizationValue of “login_button” to the String(localized:) function.

Introduce a custom postfix operator

Typing NSLocalizedString("key", comment: "comment") every time can get tedious and make our code look heavier and harder than it needs to be. To make our lives easier we can implement a custom postfix operator in our iOSSwiftI18n application.

Let’s add the following lines to the iOSSwiftI18nApp file outside of its struct:

postfix operator ~
postfix func ~ (string: String) -> String {
    return NSLocalizedString(string, comment: "")
}

This postfix operator denoted by the “~” symbol takes in a string variable, retrieves its localized value with the help of NSLocalizedString, and returns its value as a string.

Let’s add these highlighted lines inside our iOSSwiftI18nApp struct’s initializer to see how our postfix operator works:

init() {
.
print("""
\("welcome_screen_title"~)
\("login_button"~)
""")
}

Hurray! Running our app should show us multiple statements printed in the console holding the correctly localized values according to the current system language.

didFinishLaunchingWithOptions as an alternative

Although once widely used, application delegates have been outdated since the introduction of a new application life cycle in SwiftUI 2. At the time of writing, AppDelegates isn’t deprecated and is the preferred method for iOS 12 and earlier.

Hence, let’s see how to place the same code we discussed earlier inside an AppDelegate class within the iOSSwiftI18nApp file to achieve the same result.

Note: If you already successfully used the “iOSSwiftI18nApp struct initializer” method discussed previously, you may safely skip this section.

Firstly, let’s add a new class conforming to UIApplicationDelegate inside our project’s iOSSwiftI18nApp file:

class AppDelegate: NSObject, UIApplicationDelegate {
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {

        print(NSLocalizedString("welcome_screen_title", comment: ""))
        print(String(localized: "login_button"))
        print("""
        \("welcome_screen_title"~)
        \("login_button"~)
        """)

        return true
    }
}

As you can see, we’ve put all the previous localization test code inside a function that’s modifying the application(_:, didFinishLaunchingWithOptions:) method of our application delegate.

Secondly, we’ll add this code inside our iOSSwiftI18nApp struct, like so:

@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate

This line instructs the app to create a UIKit app delegate using the AppDelegate class.

A tip for easier testing

You can change the language of your app by modifying your Scheme. Click Product on your Xcode IDE and navigate to Edit Scheme… under Scheme.

Then, under the Options tab select the App Language that you want to test:

Part II: Managing your iOS app’s localization

Now your application is set up and you know the basics of the localization process on iOS. So, let’s see what all the fuss is about. Imagine thousands of keys across dozens of language files, making sure all are in sync, translated, and proofread. Pretty scary, huh? We’ve built Lokalise to help you avoid all the chaos!

Step 1: Create a project in Lokalise

Once you sign up to Lokalise (you can grab a free 14-day trial, no credit card is required), just tap the New project button, or explore the Sample Project if you wish to get more familiar with the Lokalise features first. Give your project a name and optionally a description. You can modify the base language setting later if you change your mind.

Each project has a unique ID (you can see it in the project settings) that is used when referring to a project over Lokalise API.

Step 2: Upload your files or add new keys

To import your .strings file to Lokalise, click Upload in the top menu, and drag the Localizable.strings file to the drop area. Alternatively, you can manually translate the keys in your project.

Step 3: Invite team members

Getting localization work done quickly requires a team of contributors. Switch to the Contributors section by clicking the option in the project’s icon bar and start adding teammates. Any user can be granted admin privileges, i.e., the same rights on the project as you. If a user is not an admin, you can specify per-language access to the project, identifying languages as reference (read only) or contributable (read and update). Only admins have access to key modification, importing, exporting, settings, and so on.

Step 4: Edit your iOS app’s localization values

The Lokalise editor is feature-packed. We’ve worked hard to deliver a light and responsive interface while still offering all the features you’ll need while working with app or web project copy. Feel free to explore the Sample Project, or just take a peek at our Documentation page to read about the basic concepts and options.

Part III: Integrating with your iOS localization project

Finally, the translation part is done. Now, how do you get the texts out of Lokalise and make them usable in your app? There are three main options, so choose the one you prefer:

Option 1: Download the files via GUI

Click the Download option in your project icon bar and select Apple Strings as the exporting format. Click Build and download to get the translation bundle. Then move the .lproj folders from the downloaded archive into your project, replacing the existing localization. That’s it!

Option 2: Use an API or CLI tool

Depending on your project deployment setup, you may want to use either the Lokalise API or the CLI tool – both approaches give you a simple way to automatically generate and download localization files.

Option 3: Integrate using Lokalise iOS SDK

Using our framework gives you the opportunity to update your app texts over the air without the need to resubmit for App Store review. Please refer to the Lokalise iOS SDK instructions. We will discuss the step-by-step instructions on setting up Lokalise iOS SDK later in this article.

The displayed logo is a trademark of Apple Inc. Lokalise is an independent product and has not been authorized, sponsored, or otherwise approved by Apple Inc.

Some iOS localization extras

Alright, we’ve explored all the basics of iOS localization. But, before we call it a day, how about we look into some extra features – including a Lokalise feature that’ll save hours and hours of waiting time and manual work?

Pluralization

Pluralization? Isn’t it just the work of a simple if/else block? You might think to yourself.

Well, the answer would be yes and no. The former if your app supports just one language – which surely can’t be the case if you’re reading this i18n article, *wink*, the latter if your application needs to support multiple languages, each bringing their own pluralization rules to the table.

We have to go beyond our usual .strings files to face this pluralization challenge.

Stringsdict to the rescue

.stringsdict files allow us to define plural rules for the resource values of each language.

Firstly, select File → New → File… and search for “stringsdict”. Then, choose Stringsdict File from the results shown and name it as “Localizable”:

Secondly, just like last time, let’s select the newly created Localizable file from the navigation bar on the left, and click on Localize… on the right. Let’s not forget to check German and Russian in the Localization list that’s shown.

Note: a restart of Xcode might be required for the German and Russian localized Localizable.stringsdict files to show on the navigator.

Thirdly, let’s select the base Localizable file (Localizable (English)) and add an “apple_count” entry to its Strings Dictionary as follows:

'Localized String Key': apple_count  // 1
'NSStringLocalizedFormatKey' value: Plucked %#@localizedAppleCountSentence@  // 2
'VARIABLE': localizedAppleCountSentence  // 3
'NSStrinfFormatValueTypeKey' value: u  // 4
'one' value: 1 apple  // 5
'other' value: %u apples  // 6

  1. Swift code sends this key — in our case, “apple_count” — to find a match with this dictionary entry.
  2. The ‘NSStringLocalizedFormatKey’ value containing a stringsdict variable parameter.
  3. Name of the dictionary holding the values for each plural form. Note that this dictionary name must match with the stringsdict variable name from step 2.
  4. The ‘NSStringFormatValueTypeKey’ value with an unsigned int format specifier.
  5. Value for the plural form of ‘one’.
  6. Value for the plural form of ‘other’.

Don’t forget to remove other plural forms except ‘one’ and ‘other’ from this Strings Dictionary.

Similarly, let us add the relevant values for the German and Russian locales, as well:

For German:

'Localized String Key': apple_count
'NSStringLocalizedFormatKey' value: %#@localizedAppleCountSentence@
'VARIABLE': localizedAppleCountSentence
'NSStrinfFormatValueTypeKey' value: u
'one' value: 1 Apfel gepflückt
'other' value: %u Äpfel gepflückt

For Russian:

'Localized String Key': apple_count
'NSStringLocalizedFormatKey' value: %#@localizedAppleCountSentence@
'VARIABLE': localizedAppleCountSentence
'NSStrinfFormatValueTypeKey' value: u
'one' value: сорвал 1 яблоко
'few' value: сорвал %u яблока
'many' value: сорвал %u яблок

Test it out

Let us introduce a countApples function to our iOSSwiftI18nApp:

func countApples(of quantity: Int) -> String {
    let localizedFormatStr = NSLocalizedString("apple_count", comment: "")  // 1
    return String(format: localizedFormatStr, quantity)  // 2
}

  1. Retrieves the localized string for the ‘apple_count’ key from Localizable.stringsdict.
  2. Uses localizedFormatStr as a format string template and substitutes the value in quantity variable to it.

Calling this countApples function inside the init method of our iOSSwiftI18nApp struct would show that our pluralizations are successful!

Date and time localization

Just like with plurals, the seemingly harmless date and time become a beast to behold when localization is involved. So, let’s find out how to properly use dates and times in our iOS apps before we send birthday gifts on January 4th instead April Fool’s Day!

Let’s open the iOSSwiftI18nApp struct in our iOSSwiftI18n project and add this code snippet to its initializer:

let currentDate = Date()  // 1
let dateFormatter = DateFormatter()  // 2
dateFormatter.locale = Locale(identifier: "de")  // 3
dateFormatter.dateStyle = DateFormatter.Style.medium  // 4
dateFormatter.timeStyle = DateFormatter.Style.medium  // 5

print(dateFormatter.string(from: currentDate))  // 6

  1. Initialize a Date object with the current date and time. The retrieved object is stored inside a currentDate constant.
  2. Create a dateFormatter constant holding a DateFormatter object.
  3. Set the locale of dateFormatter to a Locale object initialized with a Russian identifier.
  4. Put the date style for dateFormatter as DateFormatter.Style.medium.
  5. Put the time style for dateFormatter similarly as DateFormatter.Style.medium.
  6. Print a string representation of the current date and time localized to the German locale.

Test it out

Testing the code with different locales set on step 3 shows us the current date and time as localized to each locale:

German locale:

English locale:

Russian locale:

Programmatically select the iOS app localization language

For this purpose, let’s use the help of an external library named Localize-Swift.

Initial setup

The easiest way to introduce Localize-Swift to our project seems to be as a CocoaPod. But hold up! Right now neither our system nor our iOSSwiftI18n project has CocoaPods installed. So, let’s make it so.

Note: We’ll keep this CocoaPod setup brief in order to stick to the scope. Feel free to refer to this CocoaPods setup tutorial for more extensive details on these steps.

Install CocoaPods on the system

Open up a terminal and enter these commands:

sudo gem install cocoapods
pod setup --verbose

Create a Podfile inside the project

  1. Close Xcode. This is since we’re about to use the terminal to make external changes within the project – this could confuse the IDE if it’s active at the same time.
  2. Open a terminal and navigate into the iOSSwiftI18n project.
  3. Execute these lines in the terminal:

pod init
open -a Xcode Podfile

  1. Modify the platform value inside the Podfile to hold the latest iOS version:

platform :ios, '15.3.1'

With CocoaPods set up in both the machine and the project, we’re ready to add the Localize-Swift pod to our iOSSwiftI18n project. Let’s see how in the next section, shall we?

Add Localize-Swift pod

  1. Open a terminal inside the iOSSwiftI18n project.
  2. Use this command to open the project’s Podfile:

open -a Xcode Podfile

  1. Add this line at the top of the Podfile to introduce the external source of Localize-Swift to the project:

source 'https://github.com/CocoaPods/Specs.git'

  1. Put this line under “Pods for iOSSwiftI18n” to add Localize-Swift in the iOSSwiftI18n project:

pod 'Localize-Swift', '~> 3.2'

Note: the value coupled with the optimistic operator (~>) means version 3.2 and versions up to 3.3 (non-inclusive).

  1. Open a terminal within the iOSSwiftI18n project and execute the following:

pod install

  1. Launch Xcode and click on Open a project or file in the dialog box that pops up.
  2. Browse to the iOSSwiftI18n project directory and use the ‘iOSSwiftI18n.xcworkspace‘ file to open the project.

Important note: Make sure to use the .xcworkspace file instead of .xcodeproj to launch the project to avoid build-time errors.

Create language selection GUI

It’s time to make a simple view with buttons representing our locales; and, with the click of each button, our iOSSwiftI18n app should change locale.

There’s no need to create a new view when our app already provides a default ContentView. So, let’s navigate to the ContentView struct within our iOSSwiftI18n project and replace the struct’s content with this:

@State var localizedWelcomeText = "welcome_screen_title".localized()  // 1
var body: some View {
    VStack {  // 2
        Button("English") {  // 3
            Localize.setCurrentLanguage("en")  // 4
            self.localizedWelcomeText = "welcome_screen_title".localized()  // 5
        }
        Button("German") {  // 6
            Localize.setCurrentLanguage("de")
            self.localizedWelcomeText = "welcome_screen_title".localized()
        }
        Button("Russian") {
            Localize.setCurrentLanguage("ru")
            self.localizedWelcomeText = "welcome_screen_title".localized()
        }
        Text(localizedWelcomeText)  // 7
    }
}

  1. Call the localized string extension that Localize-Swift provides on the “welcome_screen_title” string. The retrieved value is saved in a localizedWelcomeText variable marked as a @State. Thanks to @State, each time the localizedWelcomeText variable changes value, SwiftUI updates all parts of the view affected by it.
  2. Add a VStack to stack the items vertically.
  3. Put in a button with the label “English” and an action specifying language selection tasks for the same locale.
  4. Ask Localize-Swift to change our iOSSwiftI18n app’s current language to English.
  5. Set the localizedWelcomeText @State variable’s value to the “welcome_screen_title” key’s value, localized to the current language. Since the current language was updated to English in step 4, localizedWelcomeText would retrieve an “en” localized value.
  6. Similarly, put in buttons for the German and Russian locales. These would change the current language to each locale and update the localizedWelcomeText value with their localized “welcome_screen_title” values.
  7. A text view to test if the language is changed as expected.

Don’t forget to import the Localize_Swift library to the ContentView by adding this line at the top:

import Localize_Swift

Test it out

That’s all it takes! running our iOSSwiftI18n app now would show the three buttons correctly switching the language of the app:

Have it over the air

Forget manually downloading resources, then updating, rebuilding, and redeploying them for the App Store to review. As a step above — both literally and figuratively — Lokalise OTA brings your updated localizations straight to your mobile apps. Let’s find out how to set this up.

Create a Lokalise iOS SDK bundle

Firstly, open up your Lokalise project in the Lokalise dashboard and take these steps:

  1. Head over to the Download tab.
  2. Choose Lokalise iOS SDK as the Format.
  3. Click Build only to build a bundle holding all of your language localizations.

Performing the third step will take you to a Lokalise iOS SDK bundle management page. Make sure the bundle you just created is marked as the Production bundle:

Note down the Project ID and SDK token

Take a second to jot down your Lokalise Project ID and an SDK token as well because we’ll need them in the upcoming steps.

Within your Lokalise project dashboard, navigate to the More… tab and click on Settings:

Now, note down your Project ID, press Generate new token, and take note of the resulting value as well:

Add the Lokalise SDK to the iOS project

Now it’s time to introduce the Lokalise SDK to our iOSSwiftI18n project!

Firstly, let’s head over to the Podfile in our iOSSwiftI18n project and add the Lokalise SDK dependency to it:

# Pods for iOSSwiftI18n
.
pod 'Lokalise', '~> 0.10.0'

Don’t forget to execute a pod install command in the iOSSwiftI18n project directory to finish the dependency installation.

Secondly, let’s see how to provide the Lokalise Project ID and token to our iOSSwiftI18n application:

Through iOSSwiftI18nApp struct initializer

Let us browse to our project’s iOSSwiftI18nApp struct and add these lines inside its init function:

Lokalise.shared.setProjectID("<your-project-id>", token:"<your-sdk-token>")  // 1
Lokalise.shared.swizzleMainBundle()  // 2

NotificationCenter.default.addObserver(forName: UIApplication.didBecomeActiveNotification, object: nil, queue: .main) { _ in  // 3
    Lokalise.shared.checkForUpdates { (updated, errorOrNil) in
        print("Updated: \(updated)\nError: \(errorOrNil)")  // 4
    }
}

  1. Set the previously noted Project ID and token on the Lokalise SDK.
  2. Force iOSSwiftI18n to use resources from Lokalise instead of local resources.
  3. Check for localization updates when the iOSSwiftI18n applications become active (“when it is receiving events“).
  4. Print an updated boolean that returns true if any new updates were loaded. Further, print an error if any occurred when checking for updates.

Be sure to import the dependency at the top of the iOSSwiftI18n file:

import Lokalise

Alternatively using didFinishLaunchingWithOptions

Are you sticking with the good ol’ didFinishLaunchingWithOptions instead of an IOSSwiftI18nApp struct initializer to perform tasks at app launch? No worries! Simply follow the same steps as in the previous section, remembering to put the code inside the didFinishLaunchingWithOptions function instead.

Test it out

Firstly, let’s head over to the ContentView within our IOSSwiftI18n project and replace the content inside the some View:

Text(NSLocalizedString("welcome_screen_title", comment: ""))

Secondly, let’s open the Editor tab of the Lokalise dashboard. In here, how about we change the values for the key “welcome_screen_title” a bit?

Thirdly, let’s move to the Download tab. Here, we’ll generate a new bundle for the Lokalise iOS SDK format, and click Build only.

Finally, let’s not forget to mark the newly created bundle as the Production bundle and press Save changes:

Running our iOSSwiftI18n project now would show our localization changes swiftly transferred to our app:

Refer to the Lokalise iOS SDK documentation for further details on setting up Lokalise OTA.

Conclusion

So, in this article, we explored how we can get started with iOS localization through an Xcode Swift iOS project. We gathered how to store localizations using .strings files and use them with NSLocalizedString, String(localized:), or custom postfix operators. Then, we observed how Lokalise steps in to make translation management way easier.

Further, we dived into how we could pluralize and perform date-time localizations, and make a language selection GUI. Furthermore, we checked out how Lokalise OTA helps retrieve updated localizations straight to our apps.

Hence, it’s time for me to once again swiftly make my exit. Please don’t hesitate to reach out if you have any further questions.

Until next time, forget your ex if you ever had one, but remember your Xcode, iOS, and localization!

Further reading

Related posts

Learn something new every week

Get the latest in localization delivered straight to your inbox.

Related articles

To understand what localization is, begin with what it isn’t. It’s not “translation”. That’s just where it starts. True localization means communicating across different markets in accordance with their cultures…

June 16, 2022
Localization made easy. Why wait?
The preferred localization tool of 2000+ companies