David Cordero
A different way to deal with Localized strings in Swift
Published on 31 May 2016
Dealing with localized strings has never been an easy task in iOS development. Apple has never provided a good way to deal with this issue in a clean and organized way, and our strings at the end tends to be scattered all around the project.
In addition Localizable.strings files, without any syntax validation at all, are a very error prone way to deal with strings, making very easy to have problems in runtime or even in Production.
And even worse, due to the horrible syntax of NSLocalizedString, it is quite easy to be tempted with one of these extensions out there that help to create strings without any context comment at all…
extension String {
var localized: String {
//🖕Fuck the translators team, they don’t deserve comments
return NSLocalizedString(self, comment: “”)
}
}
These extensions btw also make the command genstrings fail miserably, because it basically looks for the use of NSLocalizedString.
Fortunately, apart from Localizable.strings there is another type of files that we can localize in our projects, and they provide a better organisation, better structure and better format validation than Localizable.strings files.
They are very common, and you have at least one for sure in your project, they are plist files…
Strings in plist files
It is a straightforward process to move our strings to a plist file, just create a new plist resource in your Resources folder
and press the button “Localize” in the File inspector menu
Then you can write and organize your strings as you want in this new file, with your custom structure and also including context comments. In the following image you can find a very simple example with a few strings:
Here we can definitely be much more flexible than in Localizable.strings, we can create groups (dictionaries or arrays) to organize our strings by sections, and Xcode will always guarantee the format of the file
Working in your App with strings in a Plist
It could be nice to be able to load the strings defined in the plist in an easy way from our code right?
In fact it is a very simple issue using Swift with just a few lines of code.
import Foundation
private class Localizator {
static let sharedInstance = Localizator()
lazy var localizableDictionary: NSDictionary! = {
if let path = NSBundle.mainBundle().pathForResource("Localizable", ofType: "plist") {
return NSDictionary(contentsOfFile: path)
}
fatalError("Localizable file NOT found")
}()
func localize(string: String) -> String {
guard let localizedString = localizableDictionary.valueForKey(string)?.valueForKey("value") as? String else {
assertionFailure("Missing translation for: \(string)")
return ""
}
return localizedString
}
}
extension String {
var localized: String {
return Localizator.sharedInstance.localize(self)
}
}
Now we could get our strings in a very simple way, just as:
“Accept”.localized
And… No worries about missing context comments, because they were already defined in our plist remember?
But dude !!! where the heck is my Localizable.strings file, I need it to be exported to X website
It is very common that companies delegates to external services the translation of their strings, using POEditor.com, webtranslate.it, etc… And these companies usually work importing Localizable.strings files.
Well as I said before, it is quite easy with Swift to do this type of tasks.
We can implement our custom genstrings tool to generate Localizable.strings files which doesn’t work with source files but with our source of truth for strings, our plist file
#!/usr/bin/swift
import Foundation
guard Process.arguments.count == 2 else {
print("Syntax: genstrings [Localizable.plist]")
exit(0)
}
guard let localizableDictionary = NSDictionary(contentsOfFile: Process.arguments[1]) else {
print("Invalid Localizable.plist file provided")
exit(0)
}
for key in localizableDictionary.allKeys {
let comment = localizableDictionary.valueForKey(key as! String)?.valueForKey("comment") as! String
let value = localizableDictionary.valueForKey(key as! String)?.valueForKey("value") as! String
print("/* \(comment) */")
print("\"\(key)\" = \"\(value)\";\n")
}
dcordero@grog:~$ ./genstrings.swift Localizable.plist
/* Generic string for Accept action */
“Accept” = “Aceptar”;
/* Generic string for Cancel action */
“Cancel” = “Cancelar”;
/* Label for username in the Login screen */
“Username” = “Usuario”;
/* Label for Forgot Password action in the Login screen */
“ForgotPassword” = “¿Olvidaste la contraseña?”;
/* Label for password in the Login screen */
“Password” = “Contraseña”;
In fact it could be an awesome feature if the same script could also have the option to send the generated results directly to your external service right?… and it shouldn’t be that hard using their API.
But because it depends on your specific external service, it is up to you dude…
Special thanks to phelgo and Chris Goldsby for reviewing the beta version of this post.