Contents

App Localization Tips With Swift


Mobile localization is a very important topic. It helps us to reach as many users as possible providing our Apps with different languages. Let’s find out some tips to improve our localization handling.

Introduction

If we want to reach as many users as possible in the AppStore, we should localize our Apps with different languages. It sometimes might increase the complexity of our code. For this reason, I want to share some tips to improve our localization handling.

Happy Reading!

Getting Started

Before looking at the tips, we need a project with the localization enabled.

The first step is adding a new file called Localizable.strings in our project. If you don’t know how to do it, watch the following video:

The syntax of this file is:

1
"<key>" = "<value>";

An example of localized strings may be something like this:

1
2
"loading_data" = "Loading Data...";
"data_loaded" = "Data Loaded!";

With the localization enabled, we are ready to see some tips to improve our localization handling.

String Extension

If we want to use a localized string programmatically, we should use the function NSLocalizedString:

1
func NSLocalizedString(_ key: String, tableName: String? = default, bundle: Bundle = default, value: String = default, comment: String) -> String
  • key: The key of the localized string—like data_loaded in the example of Getting Started.
  • tableName: The name of the file where to search the string. The table name is the name of the .strings file where to search the string. If we omit it, the default value is Localizable. In Getting Started, we saw how to add Localizable.string just for the sake of explanation. We may add several .strings files to organize in a clean way our strings. For example, if we create a file Login.strings—which would contain the localized strings for our login page—we can pass to tableName the value Login to use the strings inside Login.strings.
  • bundle: The bundle which contains the strings file. By default, it’s the main one.
  • value: Default string to show if the function doesn’t find the string associated to key.
  • comment: String used by genstrings to generate the .strings files—you can find more details about the tool genstrings here.

An example of NSLocalizedString usage may be something like this:

1
let string = NSLocalizedString("data_loaded", comment: "") // Data Loaded!

There is nothing wrong with the example above, it’s quite clean. But, I think we can improve this approach. We can refactor it with a String extension:

1
2
3
4
5
6
extension String {

    func localized(bundle: Bundle = .main, tableName: String = "Localizable") -> String {
		return NSLocalizedString(self, tableName: tableName, value: "**\(self)**", comment: "")
	}
}

If the string is not found, we show **<key>** for debugging.

We can use the new extension like this:

1
2
"data_loaded".localized() // Data Loaded!
"hello_world".localized() // **hello_world**

Table Names Handling

As we saw in String Extension, we can have several table names to organize our strings.

If you use just a Localizable.strings file for the whole project, I would suggest you to split in subfiles to clean your strings.

Let’s consider that we have a file DataLoader.strings which contains:

1
2
"loading_data" = "Loading Data...";
"data_loaded" = "Data Loaded!";

With this example, we would have a new table name DataLoader. Then, if we want to use these strings, we should inject every time the table name in the method localized of String extension:

1
"data_loaded".localized(tableName: "DataLoader") // Data Loaded!

This may not be a clean approach, since we should avoid injecting the table name every time. We can clean this approach using an enum per table name to store our strings. In this way, we would have a type-safe approach to manage our localized strings without messing our codebase with hardcoded strings.

We can start creating a new enum DataLoaderStrings which will contain all the strings of DataLoader.strings:

1
2
3
4
enum DataLoaderStrings: String {
    case loadingData = "loading_data"
    case dataLoaded = "data_loaded"
}

Each case is associated to a specific key of our strings file.

Then, we can add a method to localize these strings using a specific table name:

1
2
3
4
5
6
7
8
enum DataLoaderStrings: String {
    case loadingData = "loading_data"
    case dataLoaded = "data_loaded"

    var localized: String {
        return self.rawValue.localized(tableName: "DataLoader")
    }
}

With this approach, we can use the localized strings like this:

1
let string = DataLoaderStrings.loadingData.localized // Loading Data...

We may improve this approach using a protocol extension:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
protocol Localizable {
    var tableName: String { get }
    var localized: String { get }
}

// 1
extension Localizable where Self: RawRepresentable, Self.RawValue == String {
    var localized: String {
        return rawValue.localized(tableName: tableName)
    }
}

enum DataLoaderStrings: String, Localizable {
    case loadingData = "loading_data"
    case dataLoaded = "data_loaded"
    
    var tableName: String {
        return "DataLoader"
    }
}
  1. Extend the extension only if self is a string and conforms to RawRepresentable—enums conform to this protocol by default.

With this approach, we must specify just the table name in the enum. The computation will be done in the protocol extension.

UIKit Components

We sometimes add the strings in UIKit components—like UILabel, UIButton and so on—via Interface Builder. By default, Interface Builder doesn’t allow us to localize the strings in the graphic interface.

For example, if we have an UILabel with a text set in the Interface Builder like this:

We wouldn’t be able to localize the text “Data Loaded!”.

We can solve this problem subclassing UILabel with UILocalizedLabel. Then, we can localize the text programmatically in the method awakeFromNib:

1
2
3
4
5
6
7
8
final class UILocalizedLabel: UILabel {

	override func awakeFromNib() {
		super.awakeFromNib()

		text = text?.localized()
	}
}

The method localized comes from the String extension explained previously.

With this approach, in the Interface Builder we must update the UILabel object class to UILocalizedLabel:

And, finally, we must replace the label text with the key of the string to localize:

We used the UILabel for the sake of explanation. We can use this approach for any UIKit components like UIButton:

1
2
3
4
5
6
7
8
9
final class UILocalizedButton: UIButton {

	override func awakeFromNib() {
		super.awakeFromNib()

		let title = self.title(for: .normal)?.localized()
		setTitle(title, for: .normal)
	}
}

UITextField:

1
2
3
4
5
6
7
8
9
final class UILocalizedTextField: UITextField {

    override func awakeFromNib() {
        super.awakeFromNib()


        text = text?.localized()
    }
}

And so on…

Note

As we saw in Table Names Handling, we should use table names as much as possible to clean our .strings files. Unfortunately, in the examples above, we have no ways to set the table names in these custom UIKit components. We can solve it easily.

First of all, we must add the annotation IBDesignable to our class:

1
2
3
@IBDesignable final class UILocalizedLabel: UILabel {
    //...
}

Then, we must add a new property in our custom class with the annotation IBInspectable:

1
@IBInspectable var tableName: String?

Finally, we can add a didSet observer at this property to use the new table name value:

1
2
3
4
5
6
@IBInspectable var tableName: String? {
    didSet {
        guard let tableName = tableName else { return }
        text = text?.localized(tableName: tableName)
    }
}

We used an optional binding inside the guard since tableName is an optional String.

At the end, our UILocalizedLabel should be like this:

1
2
3
4
5
6
7
8
9
@IBDesignable final class UILocalizedLabel: UILabel {

    @IBInspectable var tableName: String? {
        didSet {
            guard let tableName = tableName else { return }
            text = text?.localized(tableName: tableName)
        }
    }
}

With this approach, we added a new field for our custom UILabel component in the Interface Builder, where we can set the value of tableName:

Pluralization

We sometimes need to handle a string with both singular and plural variants depending on an input value. For example, we may have There is one file and There are 2 files.

We can handle these variants with two separate strings and use them inside an if/else statement:

1
2
3
4
5
if numFiles == 1 {
    return "There is one file"
} else if numFiles > 1 {
    return "There are \(numFiles) files"
}

This approach is not very good since it adds complexity to our codebase to manage the string variants. Fortunately, Xcode provides a better solution to handle pluralization.

First of all, we must add a new file Localizable.stringsdict in our project. We can add it following the same steps of Localizable.strings with the only difference that we must add a new Property List file instead of a Strings File.

The default extension of a Property List file is .plist. We must rename it to stringsdict.

For the sake of explanation, we used the table name Localizable. We can add pluralization for any table names—like DataLoader.stringsdict.

If we open the new file, we should have something like this:

Now, we can add all the sentences for our pluralization. This file has a specific syntax and the result will be like this:

  1. The key of the string to localize—the two %d are the placeholders for the values which we are going to set later.
  2. The format of the localized string. %#@<key>@ is the syntax of a placeholder key.
  3. Placeholder key. We must repeat the steps 3-4-5-6 for each placeholder key of the point 2. In this example, we have two placeholder keys: to_be and num_files.
  4. The type of the key which is always NSStringPluralRuleType.
  5. The type of value received in input. In this case, we have an integer (%d). You can find the list of formats <a href=“https://developer.apple.com/library/content/documentation/CoreFoundation/Conceptual/CFStrings/formatSpecifiers.html#//apple_ref/doc/uid/TP40004265"**** target="_blank”>here.
  6. The variants of the string. We can use: zero, one, two, few, many and other. Each language may have different pluralization rules which use these keys. You can read more details about language plural rules here.

Then, we can use this pluralized string like this:

1
2
3
4
let countFiles = 1
let formatString = "pending_files_%d_%d".localized()

let result = String(format: formatString, countFiles, countFiles) // There is one file

The method localized comes from the String extension explained previously.

For the sake of explanation, we used this string with two placeholders. There are no limits for the placeholders to use.

Conclusion

These are the approaches which I use more often to organize the localization in my side projects. Feel free to add a comment with your tips. Thank you!