Alexito's World

A world of coding 💻, by Alejandro Martinez

Loading translations with NSLocalizedString dynamically at runtime

One of the best things about developing for iOS is having the power of the amazing frameworks that the Apple platforms have. Foundation is easily one of the best platform frameworks out there and in this post I want to show you how to use a gem that not a lot of people is aware of.

If you care about your users I'm sure you have already used NSLocalizedString many times to easily support localization in your Apps. Non english developers tend to do it almost by default but if you are one of the ones that still doesn't care about localizing your apps you should take a look at Apple guides.

In short, NSLocalizedString works by looking for the value of the given key in a Localizable.strings file. The file needs to be inside a folder with the locale identifier and the .lproj suffix (en.lproj, es_ES.lproj, etc). With this, the system can look for the correct string in the proper language depending on the locale configuration of the user's phone.

It may seem a simple thing but the rules to pick the correct language are not straightforward. Languages are not as simple as "English" or "Spanish". There are many combinations possible, from "U.K. English" versus "U.S. English" to "Spanish" and its many variants between Spain and the different countries in America. And without entering in weird combinations like "en_ES" that don't really exist in the real world but they can in a user's device.

This is all good if the languages are bundled with your App, Foundation takes care of it. But what if the text comes from an API? The easies way for you is to use the HTTP Accept-Language header and let the API deal with the localization. Any good server side application will have a proper localization framework, and if not remember that you can just use Swift on the server and build proper backend software ;)

But there are some cases where the backend can't take care of it. In a small side project of mine I just have a JSON stored in a server. There is no code running there, just file serving. The issue is that I still want to support different languages so the JSON serves the strings in the different languages that I support in an object structure.

We can replicate that in Swift:

let strings = [
    "es": [
        "runtime": "es texto"
    ],
    "es_ES": [
        "runtime": "es_ES texto"
    ],
    "es_MX": [
        "runtime": "es_MX texto"
    ],
    "en": [
        "runtime": "en string"
    ]
]

This is an example of a simple structure that provides the same text in different languages. So now comes the tricky part, how can I select the proper string at runtime? I've seen some people do hardcoded if statements checking for specific language identifiers, without taking into account all the complex logic behind the locale system so we can't use that. But we have Foundation, so let's leverage its power!

In all honestly I would have expected this to be simple. I would like that the code that selects the correct language based on the user's locale would be exposed in a public API directly, but is not. Luckily our friend NSLocalizedString has a version that accepts many more parameters:

func NSLocalizedString(
  _ key: String, 
  tableName: String? = default, 
  bundle: Bundle = default, 
  value: String = default, comment: String
) -> String

The two important new parameters are tableName and bundle. By default when using NSLocalizedString the system uses the App main bundle and the Localizable table, table meaning the name of the strings file. So to hook into the localization system we just need to convert the object structure that we have in memory to the proper file hierarchy that is expected on disk.

As an example of how to do it (obviously you should take care of error handling to make this production ready):

First we create the Bundle directory itself, the one that will contain the different languages.

if manager.fileExists(atPath: bundlePath.path) == false {
    try! manager.createDirectory(at: bundlePath, withIntermediateDirectories: true, attributes: nil)
}

Then we can iterate over the translations

for language in translations {

and create a directory for each language with the suffix .lproj

   let lang = language.key
  let langPath = bundlePath.appendingPathComponent("\(lang).lproj", isDirectory: true)
  if manager.fileExists(atPath: langPath.path) == false {
      try! manager.createDirectory(at: langPath, withIntermediateDirectories: true, attributes: nil)
  }

then we need to grab all the key value pairs in the language and generate a proper .strings file with them.

   language.value.reduce("", { $0 + "\"\($1.key)\" = \"\($1.value)\";\n" })

And finally just save it to a file

   let filePath = langPath.appendingPathComponent("\(tableName).strings")
  let data = res.data(using: .utf32)
  manager.createFile(atPath: filePath.path, contents: data, attributes: nil)

Now we have a Bundle that gives access to the localization files with he proper structure for the system to know where to look for.

So you can just use the NSLocalizedString passing the bundle and the .strings file name. As an alternative you can also just call the localizedString(forKey:value:table) on the Bundle, is what NSLocalizedString does under the hood.

That's it! You just loaded the correct translation dynamically at runtime relying on all the power of Foundation.

You can check a complete implementation in RuntimeLocalizable

If you liked this article please consider supporting me