i18n on iOS – MeetMe Style

MeetMe Speaks Portuguese

MeetMe Speaks Portuguese

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.

The Why

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().

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,currentLocale, and autoUpdatingCurrentLocale. The systemLocale property is as straightforward as it sounds: provide the current system’s locale. The following two, currentLocale and autoUpdatingCurrentLocale, might be a tad confusing. It’s actually pretty straightforward.

Basically, 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.

Portuguese Main Menu

pt_BR? Or pt_pt_BR? Can you tell?

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.

Portuguese Registration

Advanced layouts can be tricky in foreign languages.

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).

Wrap Up

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.

How MeetMe Went International on Android

Our engineering team has been working hard to implement full internationalization (i18n), including localization (L10n) support. MeetMe sees international support as mutually beneficial for its members and the business. Through full i18n and L10n support, we provide a user experience that our current members want and some of our future members need. Internationalizing is great, and it makes the experience much better for native speakers, but it didn’t come without its own unique challenges in product, design, engineering, or quality assurance.

Preparing for internationalization

Stringification — ensuring text strings aren’t hard-coded — is one of the biggest pieces of prep-work for i18n in any project. Strings are used for user-facing text in UI components like buttons and labels. In Android, text strings are stored in a format known as string resources. Strings need to be extracted from any code and pushed into string resources. While we were working on i18n, we were fortunate enough to have the Android Lint tool which (among other things) identifies and reports hard-coded strings. If we did stringification early, that meant we could get the strings translated early (we used machine translation for development testing) and we could show our product and QA team just how badass we were. Our first pass was fairly simple: at least 90% of the application was already stringified. Supporting internationalization from the early stages saved us time in both testing and implementation. Having to refactor code is almost never a simple task, so having done it “the right way” from the beginning made it easy for us in the end.

As an example, in your application and within your layout code, let’s say you have an EditText view control with a hint. The hint may be a literal attribute value of the node. That value, android:hint="Email address" should be moved to a string resource to allow for different translations of that string. In your strings file (located in res/values/strings.xml, very likely) the node becomes <string name="email_hint">Email address</string> and your EditText’s hint attribute becomes android:hint="@string/email_hint" where @string/ denotes a string resource and email_hint is the value of the name attribute from your strings (strings.xml) file.


Translating all the strings

After we went through our initial stringification and machine translation, we needed to adopt a process for ingesting new and modified strings so they could be sent for translation without resending the already-translated strings. To do this, we endured a manual process for tracking strings changes. Each time a change was merged into our internal i18n branch, we reviewed the diff of our strings file, gathering new keys, and entering them into a file to be packaged for translation. Once translated, we had to import the new strings into localized strings files (under folders denoted with the appropriate qualifiers as per the “Configuration qualifier names” table. For Spanish (language code “es”), we placed our converted strings into /res/values-es/strings.xml. Once imported, code had to be committed, code reviewed, QA’d (to ensure different string lengths didn’t cause unexpected layout issues, which they sometimes did), and merged.

Using the aforementioned email_hint example, the original value in the strings file looks like
<string name="email_hint">Email address</string>
and then the Spanish strings (res/values-es/strings.xml) file has
<string name="email_hint">Dirección de correo electrónico</string>

Troubleshooting i18n layout issues

As previously mentioned, internationalized strings have different lengths and can sometimes cause unexpected issues related to the design and layout of an application. A Button or ListView item’s height or width may be adversely effected by a longer string, or a given layout may not implement fluidity to ensure proper layout display. Compensating for these issues typically works either through a fluid layout design or by rewording the string.

Fluid layout

The first way, introducing fluidity into a layout, can often help as it allows the view to expand or contract dependent upon the length of the string.

For example, a Button in English might be one line worth of text; in Spanish, the verbiage might be longer, creating the need to wrap the text to multiple lines. This type of layout is typically achieved by using non-static units for widths and heights, such as wrap_content (see Layout Params).


Rewording text

The second way, wording text for brevity, can be used to try to shorten the string to fit within the allotted space. Perhaps your layout has two buttons side-by-side with no additional vertical space to expand into. In that case, this technique is especially useful. For cases where we determined that this was the best solution, we marked our strings files with a custom attribute denoting that the length of the text should be as brief as possible and attempted to be fit into the existing character count of the English phrase.


Users outside of the US/UK see kilometers instead of miles

Localization refers to the process of adapting or converting for use in another language, especially the support of regional local features like timezones, currency, distance units (imperial versus metric) and formatting of dates, times, numbers (decimals, percentages, currencies) and distances. Most of this functionality is afforded to the developer on Android through classes like Calendar, Date, DateUtils, and NumberFormat (as well as its children). Distances were a special case. If we wanted to show 13 ft, 18 mi, 24 km (in cases like Feed and Locals), we needed to implement it ourselves. Our implementation was as follows: if the user’s Locale country is US or UK, use imperial units; otherwise, use metric units. This doesn’t account for the UK’s switching of imperial/metric based on type of measurement (short distances are in metric, long distances in imperial), but we didn’t feel that needed to be implemented at this juncture since we only use the units for distances traveled (as the crow flies).

Strings from API responses

Success and Errors

One of our biggest problems with our English implementation was that a number of our UX responses depended on success and error strings from our API. This was an obvious downfall to our implementation and was already being revamped to localize strings to the device prior to the internationalization process being kicked off. That revamp didn’t really do much for our existing endpoints, as most of the errors that we were localizing were for new features. To localize errors, we used a straightforward structure: errorType & errorCode. Error types are unique (Exception class names from the API side) and error codes are unique to each error type they’re sent with. This mapping allowed us to easily gather all of the exceptions being thrown by our API, and map them on the device side to have them made into user-facing strings. Stringification of errors consumed a big chunk of development time as there are a lot of possible error cases — especially when it comes to validating, sanitizing, and approving user-generated content.

On one side of the coin, this couple of APIs is a nicety for our platforms. The API gets to define a strict set of error cases and limit the data throughput which is beneficial to both the device and the server architecture. Additionally, we don’t have to count on a minor change to an error string causing internationalization problems on the device due to string comparison issues. On the flip side, coupling these values puts a constraint on the API that it must maintain both that error type and code until such a time that the device may be able to update to a newer type or code. The reason that is an issue for us is due to the nature of our code file architecture. Code is shared throughout our services in order to attempt to avoid code duplication. While that’s good, what we don’t have is that code shared with the native platform. So a change reflecting constant value on the API doesn’t automatically get reflected within the device.

Ultimately, if an API error code can’t be mapped to a known code on the device, the user will be presented with the ubiquitous, default error string: “Something went awry.”

Region names

Another hurdle in the API-to-device transition was the internationalization of regional names (countries, states, regions, counties, cities, etc). For example, in English, the display name of the country for US (ISO 3166-1) is “United States” and is “Estados Unidos” in Español. On top of that, the abbreviation for the country varies by region. For US in US it is “US” but for US in ES it is “EE. UU.” To be honest, we didn’t truly conquer this challenge yet. For regions, we chose only to translate country names. Furthermore, that translation is done dynamically through a service we’re using called “Smartling.” Smartling acts as a proxy between the client and the origin; Smartling retrieves the request from the client, mimics the request to origin (our API), parses the response, replaces English strings with the translated strings for the requested language, and responds to the client with full translated content.

Our response originally comes out as ["AFGHANISTAN","ALBANIA","ALGERIA", ...] and is translated (based on the requested language, es-es in this example) to ["AFGANISTÁN","ALBANIA","ARGELIA", ...].


Internationalizing our Android application certainly wasn’t an easy task given some of the roadblocks and caveats we ran into, but we’re confident we finalized with a solid product and implementation that will continue to give our members a quality experience from end to end.

Check out the internationalized version of the MeetMe app for Android on Google Play.

If this type of challenge interests you, consider applying to work at MeetMe.