Internationalizing an existing iOS codebase, especially like the one we have for the MeetMe app, can be a big process. This post sums up the major points of the process. Some of the points here are particularly important, especially if you are going to be supporting specific language dialects in an application.
A quick shout out to my colleague, Dallas Gutauckis, who provided the lovely photography of the application. Dallas wrote a similar post describing the i18n process he and his team went through on the Android application.
I’d like to start out with some basic explanation of the i18n process when approached from an iOS developer’s point of view.
Lets get this out of the way to start – you want to be doing i18n/l10n (localization). iOS started in the US, much like many other environments, and like many other environments, iOS has spread world-wide. Apple has extolled the virtues of i18n for the past two WWDCs, so it’s fairly clear that’s where they want to take the ecosystem at large.
Thankfully, Apple has made internationalization easy for new projects. In the latest versions of Xcode, a new project is created with a Localizable.strings file and some basic usage of the
NSLocalizedString() macro (more to come on that in a bit). Let’s take a look at the first major tripping point: i18n vs l10n.
I18n vs. l10n
It’s almost hard to imagine one without the other, as internationalization and localization are really two sides of the same coin. If you’re going to do one, you ultimately have to deal with the other. So what’s the major difference between the two?
Internationalization is best defined as the process of making your application accessible to an international audience. In shorter terms, this means supporting multiple languages. All strings must be centralized so they can be easily switched out when a new language is loaded. This is the mainstay of the GNU’s gettext application, and is how i18n has been done for decades.
However, you quickly start running into l10n, and the nightmares it can cause if it’s not properly considered. Did you get swept up in the location craze of the mid-2000s and build some neat proximity-oriented apps? Well, all your apps that display miles must now be converted to kilometers (and vice-versa, for you international readers). But wait, the UK uses metric for small distance increments, and miles for long distances.
Time and timezones are another great example of l10n. If you interact at all with the iOS calendar app, make sure to get familiar with
NSCalendar, as you’ll have to wash any dates you create through NSCalendar’s functions to make sure you’re properly setting date and time based on timezone.
But we’re getting ahead of ourselves a bit. Lets take a look at the basic tools for making an app “i18n’d”.
Basic iOS i18n
Whenever you say “internationalize” to someone who’s worked in Unix & Linux for any amount of time, they immediately associate the program “gettext”. In fact, gettext is so well respected that most utilities that perform similar functions are called “gettext utilities.” Apple is no exception — and while they don’t provide gettext in its entirety, you get a similar set of functions based around
NSLocalizedString() takes in a key, and a comment. At runtime, your application will then go hunting in a Localizable.strings file for an appropriate value associated with this key. Simple, elegant, and just enough functionality to get going.
NSLocale is one of those classes that you really need to get familiar with, as even though it’s function might seem simple (provide access to the system’s locale), it’s got a lot of moving parts.
The first oddity is the class-level methods available:
systemLocale property is as straightforward as it sounds: provide the current system’s locale. The following two,
autoUpdatingCurrentLocale, might be a tad confusing. It’s actually pretty straightforward.
currentLocale will return a cached NSLocale object specific to your app, meaning it won’t change as the user changes locale (at least not very often).
autoUpdatingCurrentLocale means if your user opens your app, backgrounds it, changes their locale in settings, and then comes back, your app will start getting the new locale changes immediately. Both have their uses, and depend upon the purpose and design of your app.
NSLocale though has some quirks. MeetMe currently supports both Portuguese and Spanish. Portuguese has two variants: Brazilian Portuguese and Portugal Portuguese. However the language has diverged so much between the two countries that iOS & OS X have a specific selection for Portugal Portuguese. When this language selection is chosen (with Brazil as the selected region), NSLocale’s full string representation of the locale is outputted as
pt_pt_BR, which is a lovely, non-standard locale definition. Normally, one would use
NSLocaleCountryCode to pull the country code for the locale.
Unfortunately, for pt_pt_BR, NSLocaleCountryCode returns
pt, as that is just the second element from the locale: pt. When we had this problem, all we could do was check the other field,
NSLocaleVariantCode, to see if that was the country code we were looking for. Unfortunately, as long as the NSLocale class produces non-standard and inconsistent formats for the locale, there isn’t much that can be done to fix this problem.
Thanks to the above issue, my general recommendation is to build your own function that handles that locale problem and returns the proper locale code for a given dialect. The easiest way to accomplish this is by pulling the language from the
preferredLanguages array (index 0 for currently selected language), and pulling the country code from the
componentsFromLocaleIdentifier: method (making sure to do the dance described above by checking to make sure
NSLocaleVariantCode isn’t what you actually want).
The main issue with this approach is NSLocale gets used by other system components, like
UIDatePicker, so rolling your own locale will require re-converting back into an NSLocale object using
initWithLocaleIdentifier:. Thankfully, in most of those system classes, you usually won’t have to override its internal locale mechanism (specifically with
UIDatePicker, locale overrides only work in iOS 6. You can’t override the locale in iOS5, and it doesn’t really work in iOS4). The upsides tend to outweigh those occasional inconveniences, especially if you have to connect to a web-based API, which generally require hyphens instead of underscores that POSIX systems use for locales.
When developing inside Cocoa & Cocoa Touch, many times developers have a tendency to take for granted what the system does for you. With i18n, this includes things like DatePickers just working out of the box. Unfortunately, this is about the only example of it working correctly.
iOS’s biggest drawback in internationalizing is the lack of proper pluralization rule support in
NSLocalizedString(). This means having to write your own rule engine, and effectively build your own version of the GNU’s gettext. The recommendations range from completely abandoning
NSLocalizedString() and writing your own implementation using structured data files, to switching to iconography and avoiding the issue entirely.
At MeetMe we decided to to take a hybrid approach where we utilized a “template” engine that would take in a number, evaluate a series of rules for a particular language, and append a “form number” onto the end of the string key to pull. Thus, for the first plural form of a word, the key might look like “myString_1”, whereas the second plural form would be “mystring_2”.
We did this because abandoning Apple’s built-in tools is not an option if you want to translate push notification text with on-device strings files. Plus, making use of Apple’s default implementation under the covers means anyone who has to do maintenance on the code will have an easier time understanding what’s happening.
Layout, and it’s big brother, Design, is what many people considered the reason for creating an iOS Application. Apple creates beautiful products, and thus everyone wants to try to at least equal, if not surpass, Apple’s design on iOS.
Unless you build for i18n in advance though, layout on iOS is problematic. As a company our layouts have always been geared towards English, which creates problems when Spanish & Portuguese (our two launch languages) are on average 30% longer than English. This means switching to a fluid layout, something that iOS isn’t terribly good at.
While that will most likely get fixed in iOS 6 (update: Has been “fixed” with Auto Layout), as it’s already been addressed in OS X 10.7/10.8, that won’t help all of us who have to support iOS4 & 5 devices. Thus, I recommend becoming very familiar with the Apple-provided NSString category
sizeWithFont:forWidth:lineBreakMode:, and it’s cousins.
However, it’s important to note that that particular function is only good for a single line of text, per Apple’s documentation. More likely than not, you’ll want to be using
sizeWithFont:constrainedToSize:lineBreakMode: which is what we ended up using 95% of the time. When defining the CGSize, you’ll want to use the system constant
CGFLOAT_MAX for the height, although we have run into instances where that does not work right, and you’ll just want to define some absurdly huge float height (my goto number is 10000.0). This way you’ll receive back a CGSize that will limit your width, word wrap correctly, and give you the width and height for your bounding box. Just don’t forget to actually
set the linebreak mode on display text, as the NSString categories just use that for doing calculations; they won’t set it as you never reference the Label/TextField in the first place.
Strings File Management
One of our biggest hurdles that we’re still trying to overcome is the management of string files in a distributed development environment. There seems to be very little documentation, opinions, or approaches on the Internet for how to approach strings management. Obviously every organization is different, but there surely must be some standard best practices?
Well, we continue to work on our own, as we try to canonicalize strings between our Android and iOS Platforms. Our initial attempts have begun by integrating lint checks for duplicates on iOS. This has proven useful as iOS’s string system will simply pick the first version of the key it finds, even though several might be scattered throughout the file. Android doesn’t have this issue as it will fail to compile / crash if duplicates are found. (Update: This is dependent on IDE in use on Android. IntelliJ IDEA won’t compile, but Eclipse will just warn on duplicates.)
The next step is bringing all the strings together into a centralized database. I’ve been working on such a system, and will hopefully be able to produce something that can be open sourced for the rest of the community. Ideally it will have git integration, and be able to scale out to handle other mobile OSes as they come up (along with desktop development).
Going through i18n at MeetMe also made me once again realize the benefits of having a good team. Countless numbers of hours have been spent between translating our backend, our client apps, redesigning them to accomodate, and then finally QAing them to make sure functionality still works as expected. Our iOS Team at MeetMe is a fantastic group of individuals, and I want to thank each and every one of them for the great job we did bringing this project together.
Wrapping up, i18n/l10n are giant topics, and I’ve glossed over a bunch on how iOS handles them. I would definitely recommend looking at Apple’s Documentation for NSLocale, and the Introduction to Internationalization Programming Topics. If there’s any interest I’ll go into more depth in a particular topic. Just remember that i18n is a good thing in the long run, even if it does add more process time.
If this type of challenge interests you, consider applying to work at MeetMe.