Localizations from NIB files

Cocoa Touch have good support for localizations. Pretty much any file can be localized, including NIB-files with user interface layouts. Unfortunately a NIB-file is an atomic file. So if you later need to do changes, like adding a new button, you will have to add this button to each and every NIB-file of each locale you support.

In practice this means that most developers tend to do the localization of NIBs manually in -[viewDidLoad]. When finalizing Tweet Note for a Øredev 2010 release I once again hit this problem. A frequent problem should be abstracted away!

Here Android has the upper hand over iOS development. In Android the text localizations are separate from the layout localizations. If the layout need changes, then only the text needs to be localized.

Let’s do the same for iOS

All texts that are set in Interface Builder are part of the archiving, and therefor set before -[awakeFromNib] is called. And -[awakeFromNib] is in turn called before the UI is ever displayed to the user. So we have a hook that is guaranteed to

  1. Be called for every object in the NIB.
  2. Be called before the UI is ever displayed to the user

Let’s use this to our advantage. Thankfully UIKit is convention over configuration, so there are very few property names that we need to hook into; text, title and placeholder, are all shared by labels, text fields, bar button items, etc.

If we set a title for a textfield in Interface Builder to "@username", then we want to use the "username" key from the Localizable.strings file, just as if we manually called upon NSlocalizableString(@"username", nil).

All we need to do in -[awakeFromNib] is to check if any of these properties exist on the current instance, if so check if the first character of said property is '@', then we do a translation. There will be a slight overhead, but nothing comparable to the I/O access for reading the NIB from disk.

As an added bonus, we also check if the property starts with "@" in wich case we remove the backward slash, this could be useful if we ever needed a text to begin with the actual '@' character.

But what about buttons?

Turns out that one very common UI component, the UIButton, do not have a title property. Instead the button have -[setTitle:forState:], so that different titles can be used depending on if the button is in normal, selected, disabled, etc, state. The states are actually a bit-mask, where 0 is the normal state. If no title has been set for the more specific states, then the normal state title will be used.

Let’s add support for localizing buttons as well. We do this by localizing the normal state first. This way states without an overriding title will be ignored:

Conclusions

Works like a charm. Adding a linear layout container view to adjust for different sizes in different locales is the final sprinkle of fairy dust, but worthy of a post of its own.
Full source code that can be dropped into any iOS project can be downloaded here. Source code is under Apache 2 license.

5 Comments

  1. Hi Fredrik,

    Thanks for sharing. This is so much easier than the documented way of localizing using ibtool etc. For some reason the awakeFromNib method was not called on my NSObject category, but changing it to a UIResponder category did the trick for me.

    Best
    Ole

  2. @Ole: One problem might be that not all classes calls [super awakeFromNib]. Something the documentation is very clear about, but I miss quite frequently myself.

  3. I have now used this method for a new project and I love it. I added a few lines of code to warn me when localization of a string is missing. My localizedValue: method now looks like:

    -(NSString*)localizedValue:(NSString*)value
    {
    if ([value hasPrefix:@”@”]) {
    NSString *stringsKey = [value substringFromIndex:1];
    value = NSLocalizedString(stringsKey, nil);
    if (stringsKey == value)
    NSLog(@”Possibly missing localization for key ‘%@'”, stringsKey);
    } else if ([value hasPrefix:@”@”]) {
    value = [value substringFromIndex:1];
    }
    return value;
    }

  4. Thanks, very nice tweak.

    I added another method for segmentedcontrol…

    -(void)localizeSegmentedControl;
    {
    UISegmentedControl* segment = (id)self;
    int itemCount = [segment numberOfSegments];
    for (int state = 0; state < itemCount; state++) {
    NSString* oldTitle = [segment titleForSegmentAtIndex:state];
    if (oldTitle != nil) {
    NSString* newTitle = [self localizedValue:oldTitle];
    if (oldTitle != newTitle) {
    [segment setTitle:newTitle forSegmentAtIndex:state];
    }
    }
    }
    }

  5. Ole Gammelgaard

    It seems this doesn’t work under iOS 5. Anyone has a solution yet?

Leave a Reply