iOS Localization

The Problem

Problem Statement

You need to build an iOS application localized in several languages.

Simple as that.

This article will focus on the localization of strings.

The Apple Recommended Way?

First of all, Apple did a great job while engineering the internationalization support into their iOS platform.

Not as great as Microsoft, to be honest, however the SDK provides most of the internationalization related functionality you’ll ever need:

  • Localized resources (images and NIB files) are easy to use: they require no source code changes.
  • UI controls are fully internationalization aware.
  • Various formatters provide almost everything you need to represent numbers, dates and times (I wrote “almost” because I was unable to find how to convert an NSDate to ISO-8601 date string, the rest of the functionality is here).
  • Localized string tables.

So, what’s the problem, you might ask? The problem is that the proposed way to use string tables is far from perfect.

Basically, every time you need a localized string, you’re expected to call NSLocalizedString function, passing another string, which is a key in the string table. So, the key for the localized string is itself a string, too.

There’re two problems with the proposed NSLocalizedString(@”A string key”, nil ) approach.

  1. The compiler doesn’t check the correctness of the string keys.
  2. The IDE doesn’t autocompletes the string keys.

For example, take a look at how to use a string from a localized string table while developing native Windows application in C++:

You see the point?

  1. Every string in your string table has a symbolic name. If you’ll accidentally type IDS_SPP_TITLE instead of IDS_APP_TITLE, your software just won’t build.
  2. The IDE’s autocompletion feature (called “Intellisense“ in Microsoft world) helps you typing all those string IDs.

The Solution

Here's the link to download the sample sources.

Overview

Here’s the proposed workflow after everything’s set up:

  1. You add new strings to your project’s default English string table, namely to the file called "en.lproj\Localizable.strings".
  2. After you modified the string table, you run a python script that does some magic.
  3. Anywhere in your project, you can now type NSString* appTitle = STR.ids_app_title;
    The compiler will check the correctness of all your string IDs.
    The auto-completion feature of the IDE (since we’re now in Apple world, it’s called “Code Sense”) will happily assist you in typing that ID.

Under the Hood

GenerateStringsClass Python Script

The Python script parses the Localizable.strings, looking for suitable key names. Since not every sequence of characters can be a valid identified, not every string table key can become a property.

Then, it generates two files:

Strings.generated.h — will contain "@property(nonatomic, readonly) NSString* string_id;" for every "string_id" key in the string table.

Strings.generated.m — will contain "@dynamic string_id;" for every "string_id" key in the string table.

That’s all.

You might ask why the Python? Well, I tried AppleScript first, hoping it'll be something like PowerShell or at least perl. However, after I spent 2 hours on that, I failed, concluding I never gonna use AppleScript again. Then I discovered Python is bundled with Mac OS X as well so I switched to Python. I never coded any of them before; however, with python, I was able to complete the task in about an hour.

Then you might ask “Fine, but where’re the implementations of that dynamic properties”?
The answer’s in the next section.

“Strings” Class

Here’s the header:

// This class provides access to the localizable strings used in the application.
@interface Strings : NSObject

+(void)_initInstance;

// Include the auto-generated part, with a bunch of readonly properties.
#include "../../Strings.generated.h"

@end

extern Strings* STR;

And here’s the implementation:

#import "Strings.h"
#import <objc/runtime.h>

Strings* STR = nil;

@implementation Strings

+(void)_initInstance
{
	if( STR )
		[ STR release ];
	STR = [ Strings new ];
}

// Include the autogenerated implementation, with bunch of @dynamics
#include "../../Strings.generated.m"

// This trivial static function implements getter instance methods for all the dynamic properties.
static NSString* getLocalizedStringImpl( id self, SEL _cmd )
{
	NSString* methodName = NSStringFromSelector( _cmd );
	return NSLocalizedString( methodName, nil );
}

// The real magic happens here: this class methods generates property getters in runtime.
+(BOOL)resolveInstanceMethod:(SEL)sel
{
	if( [ super resolveInstanceMethod:sel ] )
		return YES;
	return class_addMethod( [ self class ], sel, (IMP)getLocalizedStringImpl, "@@:" );
}

@end

Sorry for the lack of colorization. Visual Studio doesn't colorize obj-c source, and I dunno how to copy formatted text out of xCode.

As you can see, the ".generated" files created by the python script are just included into the header, and implementation of the Strings class.

The property getters instance methods are added to this class dynamically.

And "STR" is just a global variable, of class "Strings".

The Result

The result is: both compiler and IDE are now fully aware of your string table keys.

Just as planned.

Setting Everything Up

I’m too lazy to write a complete step-by-step guide.

Instead, I’ve just shared the source code of everything, hoping you'll figure it out. It's just a few dozens lines of code after all.

And, I'd like to outline some key points.

  1. When typing a string IDs in your localization file, make sure they’re valid C identifiers, i.e. don’t use spaces, don’t start with digits, etc..
  2. Save your Localizable.strings in UTF16 encoding. I just don’t know how to open an arbitrary-encoded text file in python.
  3. Save the python script to the root folder of your xcode project. The python scripts tries to read the input file called "./en.lproj/Localizable.strings", where the "." is the folder where the python script is located. Keep in mind that unlike Windows, file names in Mac OS are case-sensitive.
  4. If you’re adding the autogenerated files to your xcode project, make sure they don’t compile. To do that, open the "Strings.generated.m" file, click “View / Utilities / Show file inspector” menu item, on the newly appeared panel look for the “Target membership” section, and make sure all checkboxes in that section are cleared.
  5. In your application delegate class, don’t forget to call "[ Strings _initInstance ];" at the beginning of your “application:didFinishLaunchingWithOptions:” method, to initialize the STR global variable.
  6. MacOS X ships with outdated Python, namely the 2.6. Because of that, the script is incompatible with the latest 3.x versions of Python.

Bonus Chapter

Since the software I was developing should be localized to Russian as well, as a bonus, my sample code contains the port of my old “Russian Plurals” snippet from C# to objective-C. When using it, don’t forget to include 3 plural forms of each word into your English localization file: for English, only the first 2 are used, however all 3 are required for my python script to generate the dynamic properties. And of course, you should specify all 3 forms in the Russian version of string table.

Thank you for your attention, and good luck with all your localizationz!

The permission to publish this article with the attached source code is magnanimously granted by my current employer, iDa Mobile company.

Thanks for that, boss!