DRY String Localization with Interface Builder
Posted 21 Aug 2017
Great applications should have great localization. And users will appreciate an option to use beloved apps in their native language. There is no excuse for developers not to support interface localization even on early stages of the development process, especially when it’s so easy to do.
I prefer to design mostly with the Interface Builder. In this article I would like to share an approach I use in my projects to localize those resources.
Normally, when you try to localize a XIB file or storyboard, Xcode will happily clone the resource and you get stuck with duplicated view layouts. Yuck… That’s hardly a good option if you are trying to follow the DRY methodology 😕
Instead of doing that I suggest filling “at”-prefixed localization terms in the Interface Builder and
replacing them with the localized values in viewDidLoad
or awakeFromNib
methods of the corresponding objects.
Here is an example of how it looks like in the Interface Builder:
I use @
prefix for localization terms to allow mixing them with any real values as needed. Additionally,
leading @@
sequence is replaced with a single @
in case you need to specify unlocalized string starting
with the “at” symbol.
As it usually happens in Swift, we start with a protocol:
public protocol Localizable {
func localize()
}
Followed by an extension containing some helpers to localize strings and apply localized values to properties:
public extension Localizable {
public func localize(_ string: String?) -> String? {
guard let term = string, term.hasPrefix("@") else {
return string
}
guard !term.hasPrefix("@@") else {
return term.substring(from: term.index(after: term.startIndex))
}
return NSLocalizedString(term.substring(from: term.index(after: term.startIndex)), comment: "")
}
public func localize(_ string: String?, _ setter: (String?) -> Void) {
setter(localize(string))
}
public func localize(_ getter: (UIControlState) -> String?, _ setter: (String?, UIControlState) -> Void) {
setter(localize(getter(.normal)), .normal)
setter(localize(getter(.selected)), .selected)
setter(localize(getter(.highlighted)), .highlighted)
setter(localize(getter(.disabled)), .disabled)
}
}
Note:
substring
API is deprecated in Swift 4, should be replaced withdropFirst
which also describes the original intent better.
Note: second localization helper should be upgraded to use the new KeyPath syntax when moving to Swift 4.
Ok, so far, so good. Now let’s start implementing some localization. The process itself is clearly recursive where each container asks its children to localize themselves:
extension UIView: Localizable {
public func localize() {
subviews.forEach { $0.localize() }
}
}
Having implemented that, let’s add localization support for common controls required in most applications. The implementation is straightforward:
public extension UILabel {
public override func localize() {
super.localize()
localize(text) { text = $0 }
}
}
public extension UIButton {
public override func localize() {
super.localize()
localize(title(for:), setTitle(_:for:))
}
}
Notice that for UIButton
title we use another helper function which applies localization for all
possible control states.
Views are not the only objects we can configure with the Interface Builder. We should keep in mind the following objects too:
-
UIBarItem
with it’s subclasses:UIBarButtonItem
andUITabBarItem
-
UINavigationItem
Any other objects you decide to use in your app
extension UIBarItem: Localizable {
public func localize() {
localize(title) { title = $0 }
}
}
public extension UIBarButtonItem {
public override func localize() {
super.localize()
customView?.localize()
}
}
extension UINavigationItem: Localizable {
public func localize() {
localize(title) { title = $0 }
localize(prompt) { prompt = $0 }
titleView?.localize()
leftBarButtonItems?.forEach { $0.localize() }
rightBarButtonItems?.forEach { $0.localize() }
}
}
Finally, we have to start the flow somewhere. I usually rely on the following events:
-
Localize
title
,navigationItem
,tabBarItem
andview
in theviewDidLoad
method of myUIViewController
subclasses. -
Localize contents of
UITableViewCell
andUICollectionViewCell
subclasses in the correspondingawakeFromNib
methods.
For the UIViewController
here is a helper method to localize content, navigation and tab items:
extension UIViewController: Localizable {
public func localize() {
localize(title) { title = $0 }
navigationItem.localize()
tabBarItem?.localize()
view.localize()
}
}