Live localization via storyboards (iOS + Swift)

Eddie Long
6 min readJan 10, 2019

For a while I’ve been a bit dismayed at viewing localisations in Storyboards or NIBs. There have been a few different solutions suggested:

  • Have a different storyboard/XIB per language, this is a terrible idea.
  • Have different .strings for storyboards but these have a few drawbacks. * They don’t update when you update the storyboard content.
    * Strings in the storyboard `.strings`file are also not in a readable format.
    * You’re also splitting up your localisation files, generally I’ve found that localisation companies just want a single dump of localisation strings.
    * Having multiple files makes it easy to forget files.
    * Strings are not shared so you cannot re-use existing strings (how many ‘Ok’ strings do you want to have?)

Below is one solution proposed to this problem. It might not be the best solution but is one way around having multiple strings files strewn around the place and have live content.

The solution uses strings from the main Localizable.strings file, renders the text dynamically in XCode and allows the language to be set dynamically in Interface Builder.

The key is to use @IBDesignable and @IBInspectible.

@IBDesignable allows the content to be updated and rendered dynamically in Interface Builder.

@IBInspectible allows properties to be inspected and set via Interface Builder.

This solution It requires you to derive from UILabel (for example). It needs to be a custom class as we need to override layoutSubviews which is not possible in extensions. Without this the content is not dynamically reflected in Interface Builder.

Below is LocalisedLabel.swift :

import Foundation
import UIKit
@IBDesignable
class LocalisedLabel : UILabel {
@IBInspectable public var localisationId : String = ""

override func layoutSubviews() {
super.layoutSubviews();
syncWithIB()
}

func syncWithIB() {
if !self.localisationId.isEmpty {
let ret = Bundle(for: type(of: self)).localizedString(forKey: self.localisationId, value: "", table: nil);
self.text = ret
}
}
}

Let’s go through this step by step.

  • @IBDesignable is a keyword to enable runtime rendering of the UILabel
  • Derive LocalisedLabel from UILabel, for new labels you need to set the class in the Xcode Identity Inspector pane.
  • The property localisationId is a key to look up in your Localizable.strings file. Because it uses @IBInspectable you will see the entry in the LocalisedLabel Attributes Inspector to set the value.
Localisation Id in Attributes Inspector
  • To get the text to render dynamically in XCode layoutSubviews is needed. The syncWithIB function reads the string from the main bundle’s Localizable.strings file and sets the UILabel text.

My Localizable.strings looks like this:

"Test_Localisation" = "This is from Localizable.strings";

Putting it all together we get:

As you can see with the screenshot above the text is set to Label but the text rendered is This is from Localisable.strings which is now coming directly from the Localizable.strings of the main bundle.

UIButton

For another example, let’s try UIButton next.

Buttons have different text for different control states, capturing all of these in a new LocalisedButton.swift type:

import Foundation
import UIKit
@IBDesignable
class LocalisedButton : UIButton {
@IBInspectable public var NormalText : String = ""
@IBInspectable public var HighlightText : String = ""
@IBInspectable public var DisabledText : String = ""
@IBInspectable public var SelectedText : String = ""
@IBInspectable public var FocusedText : String = ""

override func layoutSubviews() {
super.layoutSubviews();
syncWithIB()
}

func syncWithIB() {

guard let localisedBundle = LanguageSelectorView.getBundle(object: self) else {
return
}
if !self.NormalText.isEmpty {
let ret = localisedBundle.localizedString(forKey: self.NormalText, value: "", table: nil);
setTitle(ret, for: .normal)
}

if !self.HighlightText.isEmpty {
let ret = localisedBundle.localizedString(forKey: self.HighlightText, value: "", table: nil);
setTitle(ret, for: .highlighted)
}
if !self.DisabledText.isEmpty {
let ret = localisedBundle.localizedString(forKey: self.DisabledText, value: "", table: nil);
setTitle(ret, for: .disabled)
}

if !self.SelectedText.isEmpty {
let ret = localisedBundle.localizedString(forKey: self.SelectedText, value: "", table: nil);
setTitle(ret, for: .selected)
}

if !self.FocusedText.isEmpty {
let ret = localisedBundle.localizedString(forKey: self.FocusedText, value: "", table: nil);
if #available(iOS 9.0, *) {
setTitle(ret, for: .focused)
}
}
}
}

This takes the same concept and just adds more types of text, for Normal, Selected, Highlighted, Disabled and Focused. You may have spotted the LanguageSelectorView.getBundle new bit of code, more on this later.

Adding a few more strings to Localizable.strings :

"Test_Button_Normal" = "Normal";"Test_Button_Selected" = "Selected";"Test_Button_Highlighted" = "Highlighted";"Test_Button_Disabled" = "Disabled";

As before we set the type to LocalisedButton and set the string keys via the Attribute Inspector. In the image below I’ve added 3 LocalisedButtons which have different states set via IB. Again you can see the Title text is set to Button but Selected is what is displayed.

It is pretty trivial to extend this to more types UITextView, UIBarButton item etc.

Occasionally Interface Builder doesn’t update your content. If this happens go to File->Close “Main.storyboard” and re-open it. This forces a reload of the storyboard and rectifies cached content

This approach works pretty well, however we want to go further.

Viewing Different Languages

It’s quite useful to be able to check different languages in our UI. Interface Builder doesn’t appear to allow you to change languages in IB preview, I’ve seen various mentions of a way of doing this but cannot find it in my version of Xcode (10.1). If you can do this then please let me know!

To change languages we could potentially add a new property to each new type we’ve created that contains the language we want to view. However language is really a global setting. We want a static setting that affects all views.

Below is a bit of a hack but I think it’s acceptable for what it gives you.

To do this I created a simple LanguageSelectorView.swift UIView class that has a static ForceLanguage property:

import Foundation
import UIKit
@IBDesignable
class LanguageSelectorView: UIView {

static var ForcedLanguage: String = "en"
@IBInspectable public var Language : String = LanguageSelectorView.ForcedLanguage {
didSet {
LanguageSelectorView.ForcedLanguage = self.Language
}
}

static func getBundle(object: AnyObject) -> Bundle? {
let path = Bundle(for: type(of: object)).path(forResource: LanguageSelectorView.ForcedLanguage, ofType: "lproj")
guard let bundlePath = path else {
NSLog("Unable to find bundle for language: %@", LanguageSelectorView.ForcedLanguage)
return nil
}
let bundle = Bundle.init(path: bundlePath)
guard let localisedBundle = bundle else {
NSLog("Missing bundle at path: %@", bundlePath)
return nil
}
return localisedBundle
}
}

Language is a string property that appears in the Attributes Inspector in XCode. When set it sets the global ForcedLanguage property. A string here is not an ideal choice. Unfortunately Xcode has limited support for types via IBInspectable. Arrays aren’t supported which would be perfect here, we could display all languages configured in the Apple. Instead we use the 2 letter language code that’s associated with lproj files.

Next, add a UIView to your view hierarchy that is hidden (you can make it 1x1 or off-screen if you don’t want it to appear at all) and set it’s Custom Class to LanguageSelectorView.

In the image above you can see that en is set as the value, which is the default value.

In our custom types we can now use the LanguageSelectorView static function to get the localised Bundle for our languages.

guard let localisedBundle = LanguageSelectorView.getBundle(object: self) else {
return
}

Now we add another language to the project. You can do this by going into the Project->Info view and hitting ‘+’

To the French Localizable.strings we add:

"Test_Button_Normal" = "Normal_FR";"Test_Button_Selected" = "Selected_FR";"Test_Button_Highlighted" = "Highlighted_FR";"Test_Button_Disabled" = "Disabled_FR";

Now let’s try it out. Select the Language Selector View in Interface Builder and type in fr instead of en .

You can see that invalid languages e.g. gf revert back to the default text set via Interface Builder.

References

  1. https://medium.com/@mario.negro.martin/easy-xib-and-storyboard-localization-b2794c69c9db
  2. https://ayeohyes.wordpress.com/2015/07/24/localizing-storyboards-and-xibs/
  3. http://gtlcodes.blogspot.com/2016/03/using-ibdesignable-ibinspectable.html

--

--

Eddie Long

Working at Apple on Radar. All opinions are my own etc.