iOS app Internationalization(i18n) using Swift — Part2

Rohit Kumar
10 min readAug 26, 2023

--

This article is the continuation of the previous artcile on iOS app Internationalization. You can view the article here.

In the last part, we have covered the basics of Internationalization & Localization concepts, starter project structure, and the approach for internationalization by localizing the storyboard and localizing strings in the swift code. The previous approach was not so efficient as it was a time consuming and had too much manaul tasks.

Live localization via storyboards! What‘s in this new approach?

Let’s take a scenario, Ram is an iOS developer working on a large and a complex app and now he wants to add i18n support for multiple languages (let’s say five different languages).
Now, instead of localizing the whole storyboard, we will add a @IBInspectable elemnet for each objects(Button, Label, TextField,…) in storyboard in such a way that we don’t have to manually localize the text in each object in the storyboard & it should be automatically mamaged by our helper extensions (which we will add in implementation part).
Also, we will add a helper to get the translated strings as per the selected language. So, now let’s jump into the implementation part.

1. Add additional languages support to the app as we do in the last article.
Project Navigator → project name InternationalizationDemo→ Info segment → check the Use Base Internationalization → click on add + button →
Choose Hindi (hi) → but don’t checkmark the storyboards.

2. Add Localizable.strings files for both English(base language) & Hindi that will contain all different texts exisiting in storyboards, xib files and swift codes.

3. Create a helper class LocalizationHelper that will return the localized string for different texts as per the selected language.

4. Add Localizable Extensions for @IBInspectable elements.

5. Add values for localizedKey in the attribute inspector for each objects in the storyboard.

6. Localizing strings in the swift code using LocalizationHelper

Adding the Localizable Strings

Let’s add the localize strings files similarly as we do in the last part.

Select File → New File → search for Strings File → keep the name Localizable as default.
• Xcode will generate Localizable.strings file. In the file inspector, you will see localization panel. Click localize and select “Localize”.
• Again in the file inspector, check Hindi in localization panel. Xcode will generate two Localizable.strings files for both English and Hindi.

Let’s add all the different strings located in the complete project (in storyboards, in xib files, swift codes,…) firstly for the base language.

/* 
Localizable.strings (English)
*/

"sign-in-to-account" = "Sign in to your account";
"enter-email" = "Enter email";
"email" = "Email";
"enter-password" = "Enter password";
"password" = "Password";
"sign-in" = "Sign in";
"sign-up-here" = "Don't have an account. Sign up here";

"back" = "Back";
"back-to-login" = "Back to login";
"create-new-account" = "Create a new account";
"confirm-password" = "Confirm password";
"sign-up" = "Sign up";
"account-already-exists" = "Account already exists.";
"account-created-successfully" = "Account created successfully.";
"invalid-credentials" = "Invalid Credentials";

"welcome-user" = "Welcome %@";
"select-language" = "Select Language";
"done" = "Done";
"error" = "Error";
"ok" = "OK";
"close" = "Close";

Similarly add the translated strings in Localizable (Hindi).


/*
Localizable.strings (Hindi)
*/

"sign-in-to-account" = "अपने अकाउंट में साइन इन करें";
"enter-email" = "ईमेल दर्ज करें";
"email" = "ईमेल";
"enter-password" = "पासवर्ड दर्ज करें";
"password" = "पासवर्ड";
"sign-in" = "साइन इन";
"sign-up-here" = "कोई खाता नहीं है. यहां साइन अप करें";

"back" = "पीछे";
"back-to-login" = "लॉगिन पर वापस जाएं";
"create-new-account" = "एक नया खाता बनाएं";
"confirm-password" = "पासवर्ड की पुष्टि करें";
"sign-up" = "साइन अप";
"account-already-exists" = "खाता पहले से मौजूद है।";
"account-created-successfully" = "खाता सफलतापूर्वक बनाया गया.";
"invalid-credentials" = "अवैध प्रत्यय पत्र";

"welcome-user" = "स्वागत है %@";
"select-language" = "भाषा चुने";
"done" = "हो गया";
"error" = "त्रुटि";
"ok" = "ठीक है";
"close" = "बंद करे";

Localization Helper

What we are going to do first is to create a helper class that will return the localized string for different texts as per the selected language.
Add a new swift file in Helpers folder namely LocalizationHelper.swift .
Create a new singleton class for this LocalizationHelper.

class LocalizationHelper {
public static func localize(_ key: String) -> String {
return NSLocalizedString(key, comment: "")
}

private static func localize(args: [CVarArg], key: String) -> String {
String(format: localize(key), args)
}
}

Above func localize(:), will return the loclaized strings for both cases with or without arguments for localization where aruguments refers to the data to be inserted at run time.

class LocalizationHelper {
// ------

static var signInToAccount: String { localize("sign-in-to-account") }
static var enterEmail: String { localize("enter-email") }
static var email: String { localize("email") }
static var enterPassword: String { localize("enter-password") }
static var password: String { localize("password") }
static var singIn: String { localize("sing-in") }
static var signUpHere: String { localize("sign-up-here") }

static var back: String { localize("back") }
static var backToLogin: String { localize("back-to-login") }
static var createNewAccount: String { localize("create-new-account") }
static var confirmPassword: String { localize("confirm-password") }
static var signUp: String { localize("sign-up") }
static var accountAlreadyExists: String { localize("account-already-exists") }
static var accountCreatedSuccessfully: String { localize("account-created-successfully") }
static var invalidCredentials: String { localize("invalid-credentials") }

static var selectLanguage: String { localize("select-language") }
static var done: String { localize("done") }
static var error: String { localize("error") }
static var ok: String { localize("ok") }
static var close: String { localize("close") }
static func welcomeUser(_ args: [CVarArg]) -> String {
localize(args: args, key: "welcome-user")
}
}

Now, added static variables corresponding to each different texts present in entire project. So, if we haveb to access the transalted string for text “Sign in”, we simply will call — LocalizationHelper.singIn.

Localizable Extensions to create @IBInspectable elements

Let’s start with the starter project. Create a new swift file in Extensions folder LocalizableExtensions.swift. Add a protocol StbIBLocalizable (storyboard localizable) which our storyboard objects will conform for an adding an additional element.

//  LocalizableExtensions.swift

import Foundation

protocol StbIBLocalizable {
var localizedKey: String? { get set }
}

To use this StbIBLocalizable protocol, we will use extensions of UILabels, UIButton,.. to conform to this protocol. Let’s start by adding extensions for UILabel in LocalizableExtensions.swift .

extension UILabel: StbIBLocalizable {
@IBInspectable var localizedKey: String? {
get { nil }
set(key) {
if let key = key { text = LocalizationHelper.localize(key) }
}
}
}

After conforming to the StbIBLocalizable protocol by UILabel, making variable localizedKey as an @IBInspectable element, you will see an additional field in the attribute inspector in main.storyboard. We can use this field to directly add the text and it will be automatically translated.

Let’s add the text “enter-email” in the localizedKey, and run the he application in Hindi. You can run your application using different locales by Option + Clicking on the (Play) Start active scheme button, then going to Run → Options → App Language → Change to a supported language (Hindi in this case) → Click Run.

You will see that that the “Enter email” is being showed as “ईमेल दर्ज करें”.

Let’s add the extensions for other elements. You can customize the thsese as per your need.

extension UIButton: StbIBLocalizable {
@IBInspectable var localizedKey: String? {
get { nil }
set(key) {
if let key = key {
setTitle(LocalizationHelper.localize(key), for: .normal)
setTitle(LocalizationHelper.localize(key), for: .highlighted)
setTitle(LocalizationHelper.localize(key), for: .selected)
setTitle(LocalizationHelper.localize(key), for: .disabled)
}

}
}
}

extension UINavigationItem: StbIBLocalizable {
@IBInspectable var localizedKey: String? {
get { nil }
set(key) {
if let key = key { title = LocalizationHelper.localize(key) }
}
}
}

extension UIBarItem: StbIBLocalizable {
@IBInspectable var localizedKey: String? {
get { nil }
set(key) {
if let key = key { title = LocalizationHelper.localize(key) }
}
}
}

extension UITextField: StbIBLocalizable {
@IBInspectable var localizedKey: String? {
get { nil }
set(key) {
if let key = key {
text = LocalizationHelper.localize(key)
placeholder = LocalizationHelper.localize(key)
}
}
}
}

Add the localized key from strings file in the localizedKeyin the attribute inspector for each objects. Now, we are done with the localizing the storyboard objects, let’s jump how to localize strings in the swift code.

Replace the label text in WelcomeViewController by using LocalizationHelper class member to get the translated string as per the current selected language & our welcome page will look like this:

override func viewDidLoad() {
super.viewDidLoad()
welcomeLabel.text = LocalizationHelper.welcomeUser([user!])
}

How to manually change the app language?

Apple doesn’t suggest us to manually change the app language, but since we are doing it, we will create a new view controller that will gives us option to choose among the available app languages. We will use UserDefaults to store and track the current selected langauage.
Our app will initialize with selected langauage (base langauage if not any selected). Also, on change of selected langauage, we will revert the user to home page (login page in our case). So, let’s start the implemenataion.

Go to the Extension file, and add the following extension for the UserDefaults. We will store the current selected language with the "i18n_language" key and the variable selectedLanguage will be used to set and get the selected language.

If you want to learn more about the UserDefaults , you can go through this article here.

extension UserDefaults {
var selectedLanguage: String? {
get { string(forKey: "i18n_language") }
set { set(newValue, forKey: "i18n_language") }
}
}

Also, we need to modify func localize() in our LocalizationHelper class, so that it checks the current selected language in UserDefaults & return the translated strings as per the selected language.

class LocalizationHelper {
public static func localize(_ key: String) -> String {
if UserDefaults.standard.selectedLanguage == nil {
UserDefaults.standard.selectedLanguage = "en"
UserDefaults.standard.synchronize()
}

let lang = UserDefaults.standard.selectedLanguage
guard let path = Bundle.main.path(forResource: lang, ofType: "lproj") else {
return NSLocalizedString(key, comment: "")
}

let bundle = Bundle(path: path)
return NSLocalizedString(key, bundle: bundle!, comment: "")
}
// -------
}

SelectLanguageViewController

Till now, we have added all the necessary requirements for translating the strings as per the selected langauge saved in the UserDefaults. Now, let’s add a page for selecting the language.
I have created a new UITableViewController into the Main.storyboard and linked it to the classSelectLanguageViewController. Also, added a settings button onto the navigation right bar button item in the WelcomeViewController, which will redirect the user to the SelectLanguageVC page. The SelectLanguageVC tableview list items displays avialable language and is calculated from the localization bundle. So, here the code for the SelectLanguageViewController, SelectLanguageViewModel, SelectLanguageTableViewCell & the Language model.

class SelectLanguageViewController: UITableViewController {
private let viewModel = SelectLanguageViewModel()

override func viewDidLoad() {
super.viewDidLoad()
let doneButton = UIBarButtonItem(title: LocalizationHelper.done, style: .plain, target: self, action: #selector(self.doneButtonPressed))
self.navigationItem.rightBarButtonItem = doneButton
}

@objc func doneButtonPressed() {
if UserDefaults.standard.selectedLanguage != viewModel.selectedLanguage.code {
UserDefaults.standard.selectedLanguage = viewModel.selectedLanguage.code
SceneDelegate.shared?.reloadWindow()
}
else {
self.dismiss(animated: true, completion: nil)
}
}

// MARK: - Table view data source
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
viewModel.availableLanguage.count
}

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! SelectLanguageTableViewCell
cell.languageNameLabel.text = viewModel.availableLanguage[indexPath.row].name
cell.accessoryType = viewModel.selectedLanguageIndex == indexPath.row ? .checkmark : .none
return cell
}

override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
viewModel.selectedLanguageIndex = indexPath.row
tableView.reloadData()
}
}

struct Language {
var code: String
var name: String?
}

class SelectLanguageViewModel {
var availableLanguage = [Language]()
var selectedLanguageIndex: Int
var selectedLanguage: Language {
availableLanguage[selectedLanguageIndex]
}

init() {
availableLanguage = Bundle.main.localizations
.filter { $0 != "Base" }
.map { Language(code: $0, name: Locale.current.localizedString(forLanguageCode: $0)) }

selectedLanguageIndex = availableLanguage
.map { $0.code }
.firstIndex(of: UserDefaults.standard.selectedLanguage ?? "en") ?? 0
}
}

class SelectLanguageTableViewCell: UITableViewCell {
@IBOutlet weak var languageNameLabel: UILabel!
}

So what I’m doing here first is to find the available languages for the tableview datasource. Also, I’m using the UserDefaults to track the current selected language, so when the done button is pressed, we reload the entire rootViewController window, and then all our strings are translated through the LocalizationHelper as per the current selected language.

To reload the entire rootViewController window, I had created a func reloadWindow in SceneDelegate which will set the window rootViewController as the new instantiateInitialViewController .

class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
static weak var shared: SceneDelegate?

func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// -----
Self.shared = self
}

// ----------------
func reloadWindow() {
let vc = UIStoryboard(name: "Main", bundle: nil).instantiateInitialViewController()!
window?.rootViewController = vc
}
}

Here, is the final output

Summary

Hurray, we have now completed the better approach to add internationalization support in our app. Adding the internationalization support to an app by this approach is little less complex than the last one and can easily be done for multiple languages. Also, we have learned how we can add options to manually change the app language.
Here, is the github link for the completed project.
You can also find the previous discussed approach here.

Thanks for Reading!
If you liked this article, please share and 👏 so other people can read it too :)

--

--