6 min read

One of the most common tasks when building a production app is translating the user interface into multiple languages. I won’t go into much detail explaining this or how to set it up, because there are lots of good articles and tutorials on the topic.

As a summary, the default system is pretty straightforward. You have a file named Localizable.strings with a set of keys and then different values depending on the file’s language. To use these strings from within your app, there is a simple macro in Foundation, NSLocalizedString(key, comment: comment), that will take care of looking up that key in your localizable strings and return the value for the user’s device language.

Magic numbers, magic strings

The problem with this handy macro is that as you can add a new string inline, you will presumably end up with dozens of NSLocalizedStrings in the middle of the code of your app, resulting in something like this:

mainLabel.text = NSLocalizedString("Hello world", comment: "")

Or maybe, you will write a simple String extension for not having to write it every time. That extension would be something like:

extension String {
    var localized: String {
        return NSLocalizedString(self, comment: "")
    }
}

mainLabel.text = "Hello world".localized

This is an improvement, but you still have the problem that the strings are all over the place in the app, and it is difficult to maintain a scalable format for the strings as there is not a central repository of strings that follows the same structure.

The other problem with this approach is that you have plain strings inside your code, where you could change a character and not notice it until seeing a weird string in the user interface. For that not to happen, you can take advantage of Swift’s awesome strongly typed nature and make the compiler catch these errors with your strings, so that nothing unexpected happens at runtime.

Writing a Swift strings file

So that is what we are going to do. The goal is to be able to have a data structure that will hold all the strings in your app. The idea is to have something like this:

enum Strings {
    case Title
   
    enum Menu {
        case Feed
        case Profile
        case Settings
    }
}

And then whenever you want to display a string from the app, you just do:

Strings.Title.Feed // "Feed"
Strings.Title.Feed.localized // "Feed" or the value for "Feed" in Localizable.strings

This system is not likely to scale when you have dozens of strings in your app, so you need to add some sort of organization for the keys. The basic approach would be to just set the value of the enum to the key:

enum Strings: String {
    case Title = "app.title"
   
    enum Menu: String {
        case Feed = "app.menu.feed"
        case Profile = "app.menu.profile"
        case Settings = "app.menu.settings"
    }
}

But you can see that this is very repetitive and verbose. Also, whenever you add a new string, you need to write its key in the file and then add it to the Localizable.strings file. We can do better than this.

Autogenerating the keys

Let’s look into how you can automate this process so that you will have something similar to the first example, where you didn’t write the key, but you want an outcome like the second example, where you get a reasonable key organization that will be scalable as you add more and more strings during development.

We will take advantage of protocol extensions to do this. For starters, you will define a Localizable protocol to make the string enums conform to:

protocol Localizable {
    var rawValue: String { get }
}

enum Strings: String, Localizable {
    case Title
    case Description
}

And now with the help of a protocol extension, you can get a better key organization:

extension Localizable {
     var localizableKey: String {
        return self.dynamicType.entityName + "." rawValue
    }
   
    static var entityName: String {
        return String(self)
    }
}

With that key, you can fetch the localized string in a similar way as we did with the String extension:

extension Localizable {
    var localized: String {
        return NSLocalizedString(localizableKey, comment: "")
    }  
}

What you have done so far allows you to do Strings.Title.localized, which will look in the localizable strings file for the key Strings.Title and return the value for that language.

Polishing the solution

This works great when you only have one level of strings, but if you want to group a bit more, say Strings.Menu.Home.Title, you need to make some changes.

The first one is that each child needs to know who its parent is in order to generate a full key. That is impossible to do in Swift in an elegant way today, so what I propose is to explicitly have a variable that holds the type of the parent. This way you can recurse back the strings tree until the parent is nil, where we assume it is the root node.

For this to happen, you need to change your Localizable protocol a bit:

public protocol Localizable {
    static var parent: LocalizeParent { get }
    var rawValue: String { get }
}

public typealias LocalizeParent = Localizable.Type?

Now that you have the parent idea in place, the key generation needs to recurse up the tree in order to find the full path for the key.

rivate let stringSeparator: String = "."

private extension Localizable {
    static func concatComponent(parent parent: String?, child: String) -> String {
        guard let p = parent else { return child.snakeCaseString }
        return p + stringSeparator + child.snakeCaseString
    }
   
    static var entityName: String {
        return String(self)
    }
   
    static var entityPath: String {
         return concatComponent(parent: parent?.entityName, child: entityName)
    }
   
    var localizableKey: String {
        return self.dynamicType.concatComponent(parent: self.dynamicType.entityPath, child: rawValue)
    }
}

And to finish, you have to make enums conform to the updated protocol:

enum Strings: String, Localizable {
    case Title
   
    enum Menu: String, Localizable {
        case Feed
        case Profile
        case Settings
       
        static let parent: LocalizeParent = Strings.self
    }
   
    static let parent: LocalizeParent = nil
}

With all this in place you can do the following in your app:

label.text = Strings.Menu.Settings.localized

And the label will have the value for the “strings.menu.settings” key in Localizable.strings.

Source code

The final code for this article is available on Github. You can find there the instructions for using it within your project. But also you can just add the Localize.swift and modify it according to your project’s needs. You can also check out a simple example project to see the whole solution together.

 Next time

The next steps we would need to take in order to have a full solution is a way for the Localizable.strings file to autogenerate.

The solution for this at the current state of Swift wouldn’t be very elegant, because it would require either inspecting the objects using the ObjC runtime (which would be difficult to do since we are dealing with pure Swift types here) or defining all the children of a given object explicitly, in the same way as open source XCTest does. Each test case defines all of its tests in a static property.

About the author

Jorge Izquierdo has been developing iOS apps for 5 years. The day Swift was released, he starting hacking around with the language and built the first Swift HTTP server, Taylor. He has worked on several projects and right now works as an iOS development contractor.

LEAVE A REPLY

Please enter your comment!
Please enter your name here