Working with Localization

There’re a place where most users set English as their preferred system language, but demanded Chinese for local apps. We’re in Hong Kong.

James Tang
iOS Apprentice

--

Localization, or in other words, Internationalization, is something vital to improve user experience. Despite some of the applications may not be at worldwide scale, in Hong Kong, most local apps provides at least two languages.

Hong Kong is a relatively interesting region in language preferences. Users always set English as their system-wide default language, but demanded Traditional Chinese for local applications. As a result, applications has to prompt users to pick a language at very first launch.

We have released Carshare iOS app in English only, we almost got this language request immediately. This article is about tips and tricks on technical level that how we proceed with the execution. It could do the most if you’re an iOS developer and already familiar with what a .strings file is. If not, but you’re still interested, it could be a good starting point at Apple’s Internationalization and Localization guide.

This article comes in two parts: Part 1 will be focusing more on the basics productivity tips, and Part 2 is about how we cater on-the-fly language toggle.

Part 1 — 12 Tips to Be More Productive

Implementing isn’t necessary straight forward. There’re a number of ways that we can save time and refine our work flow, also making it in a more managable fashion when involving more parties to the translation process.

0. Adding a language in Xcode 5 into your project

The fundamental step to add a new language to a project. Every Xcode version does things slightly different, this explains how we do in Xcode 5.

1. Use QuickLocalization Xcode plugin

There isn’t really any magic to make your text display in different language, simply with this marco:

NSLocalizedString(@"Hello", nil);

With the help of QuickLocalization, it saves us a few keystrokes. Pressing ⌥+⇧+D will auto completes the marco for us.

2. Use genstrings

$ genstrings -o Base.lproj *.m

After we’ve add all the missing marcos, lets get our translation file ready. Apple already provides a command line tool for us. It scans the entire project and extract all strings marked by NSLocalizedString and export to a .strings file.

The genstrings command overrides previously generated version, so here we would want to make sure we’ve been version tracking our translation files.

3. Use comments to fix warnings and missing translations

Say here’s what’s in your ViewController.m:

- (void)reloadUI:(NSString *)orderBy {
self.orderByLabel.text = NSLocalizedString(orderBy, nil);
}

Variables in any forms will not be genstring-friendly. Moreover when you run the command it’s will also generate a warning like this:

Bad entry in file ViewController.m (line = 92): Argument is not a literal string.

The fix is easy. Listing the possible variations in the comments block can be both self-explaining, and eliminates the warning.

/* 
* @orderBy — possible variations
*
* NSLocalizedString(@"price", @"Order By");
* NSLocalizedString(@"name", @"Order By");
*
*/
- (void)reloadUI:(NSString *)orderBy {
// Use the bundle API directly to prevent the warning
NSBundle *bundle = [NSBundle mainBundle];
NSString *text = [bundle localizedStringForKey:orderBy
value:orderBy
table:nil];
self.orderByLabel.text = text;
}

4. Working with Plural

The Foundation Releases Notes explained that now we have a much smarter way to work with plural forms.

The configuration dictionary could contain keys for “zero”, “one”, “two”, “few”, “many”, and “others”.

By using a .stringsdict file, we eliminate ugly if-else cases for these conditions.

5. Split into multiple .strings files

Considering that more people will be involved in the translation process, we want to make it more manageable. We’ll use a slightly different marco to group our localizable strings:

NSLocalizedStringFromTable(@"Browse", @"BrowseCar", nil);

Re-run the genstrings command (mentioned in #2) and it automatically split into smaller files. Now you can distribute them to the team with your preferred channel.

6. Test different languages (and longer text) with Launch Arguments

You can also use ⌘+⇧+, to display the panel

Some of the languages may have very long text, you can easily make our current interface displaying ultra long text by specifying a “Launch Argument”.

-NSDoubleLocalizedStrings YES

Testing another language can also be done.

-AppleLanguages (zh-Hant)

There’re many more that Launch Arguments can do, which explained at HSHipster.

7. Use Base Internationalization for Storyboard/XIBs

Not only a .strings file can have be configured in multiple languages. Storyboard/XIBs (and images) can also be done in the exact same way.

Prior to Xcode 5, we’d have to make a new copy of the whole Storyboard/XIB. Say if you have 5 different languages, if there’s a new UI element added, you’ll have to add it for each localization. It might be useful if you really want a totally different layout for each language, but mostly you’ll just want to have a new string.

Thanks Apple, localizing Storyboard/XIBs are much more easier.

8. Use Auto Layout

Now we probably realize that our interface isn’t quite right when the content has various number of lines when displaying in different language. It’s one of the exact reason why Auto Layout is introduced.

Setting up proper “constrains” can make our UI elements responsive to the number of lines. If you’re not yet familiar with AutoLayout, watch this WWDC 2013 video: Taking Control of Auto Layout in Xcode 5.

9. Tidy up our Storyboards

Rearchitecturing our Storyboard an splitting it into smaller “flows”

Similar to #4, it’s also good to break up our Storyboards into smaller files and remove unused UI elements. It will help Xcode to generate smaller string files and eliminate noises. Our Interface Builder will be more performant as well.

Connecting across storyboards (bonus tips)

Created a custom Intention object to encapsulate segue details and configured to use in Interface Builder

BUT, since we have removed those connecting segues from our view controllers, now we need some way to connect them back. We can use the old way to present a view controller in code, or use a code-less magically way to do it.

I don’t like both and I recently came across an article about Intentions, it’s a great application for this.

10. Merging and solving conflicts with external tool

Kaleidoscope is one of the pretty handly file comparison tool. While comparing images is one of its unique selling point, I find the shortcuts are most effective on merging newly translated strings.

Make sure you don’t forget about its $ git difftool intergration.

11. Make your .strings file diff-friendly in git

If the above solution sounds a bit costly, $ git diff has always been here for free. But you may notice that it may not show the line difference properly since .strings files are recognized as binary files. A small configuration change is how you can fix it.

12. Debugging

Sometimes your translation just doesn’t show up, most probably when a new translation file is added or removed. Try the following procedures:

  1. Clean your project > Remove the app from simulator > Compile & Run.
  2. There may have syntax error in the .string files, use the plutil command to “lint” your strings file.
$ plutil Localizable.strings

scroll down to continue to Part 2…

Part 2 — On-the-fly translation

To conquer what Apple didn’t provide, our mission is to find out the best possible solution.

While having our translations ready, we can start considering the best user experience. We wanted to have our language toggle without requiring an application restart to take effect. The bad news is, there’s no official way provided by Apple for this purpose.

By default, using the system specified language is what iOS will automatically shown to our users. To force an app to use a specific language, is actually a world-wide problem.

Attempt #1

In fact, Apple did provide a way to specify application specific language, by updating the “AppleLanguages” key in NSUserDefaults.

[[NSUserDefaults standardUserDefaults] setObject:@[@"zh-Hant"]
forKey:@"AppleLanguages"];
[[NSUserDefaults standardUserDefaults] synchronize];

The problem is that, you’ll have to set it BEFORE UIKit initialized, most probably in main.m, which means that if you have changes, the app has to be relaunched to take effect.

Attempt #2

Other suggested to create a wrapper method instead of using NSLocalizedString marco.

The wrapper instances listen for changes in language settings, and use different bundles to return the appropriate strings.

This will only work for strings, but not the image resources, and not our interfaces that created in Storyboard and XIBs. We’ll also need to throw away all our code that has been using the NSLocalizedString marco, which will break our genstrings command to work properly.

Our idea solution

  1. A solution that works well with genstrings.
  2. Not just work for strings but also for Storyboard/XIBs, images and other resources.
  3. Doesn’t require an app relaunch to take effect.

It turns out the only solution is to swap the mainBundle of our application as soon as user changes their language preferences inside the app.

http://stackoverflow.com/a/20257557/1013897

We founded the above answer is a valid solution, and it deserves more up votes, try it yourself and vote it now. ☺

Updating element on active screens

What finally remains is to reload onscreen elements immediately when user switches the language. It turns out to be another big question.

For table views and collection views, we’ll just need to call `reloadData` once. But what about other static UI elements?

If we absolutely need to retain the UI states, it seems like there’s no quick way. So we’ve have to create IBOutlet references in Interface Builder for each of them, then our view controller can do the right thing at the right time.

What’s the right time then? Some suggested at `viewWillAppear`, or you can go using NSNotification, either method have its own pros and cons, you can decide that method that works for you.

To make things a little bit simpler, may be we can sacrifice a bit on the app states. Reloading our rootViewController from our application delegate will always work reliably.

// Reload our root view controller
AppDelegate *delegate = [UIApplication sharedApplication].delegate;
NSString *storyboardName = @"Main"; // Your storyboard name
UIStoryboard *storybaord = [UIStoryboard storyboardWithName:storyboardName bundle:nil];
delegate.window.rootViewController = [storybaord instantiateInitialViewController];

Conclusion

There are some third party solutions that aimed to make localization less painful. I’ve tried Linguan, and Localizable Strings Merge, but I don’t find it worked tool well in my workflow. There’re more out there but seemed most have not been actively maintained.

Apple’s Localization is certainly very powerful, but it certainly has it’s limitations and took us some time to understand and make things more pleasant to work with. We hope this article will help you and your project team in some ways, and we also hope that Apple can provide a more decent solution for on-the-fly language toggle in the future.

Special thanks to @simonpang and @aschndr for proof-reading the article and suggest improvements.

--

--

James Tang
iOS Apprentice

Sketch Plugins and iOS UX Engineer. Opensource projects contributor, share on Twitter. @jamztang