During the week before New Year’s Eve, I decided to localize the Entry Manager app into French. The iOS Entry Manager app wasn’t originally written with l10n/i18n in mind so the code and resource files (XIBs) are full of English strings and U.S.-specific date/time formats.
Here’s the approach I took in localizing the app and some of the tricks I learned along the way. We thought that some engineers and fans of the app might enjoy learning a bit more about how it was built.
Preparing Strings for Localization
I started by running an exhaustive search of all the strings in the code (i.e. the .m files, not the XIBs). A basic search for @” returns a lot of results, but XCode helps out by color-highlighting strings so you can scan through files pretty quickly. Most strings don’t need to be localized—you can skip debug messages, key-value names, database column names, etc. Most of the strings that you’ll need to localize in .m files tend to be found in View Controllers and in the strings shown in alerts.
Every string that you want to localize in Cocoa needs to be wrapped with a call to NSLocalizedString. This function takes two parameters: the English string and a text comment for the translator that provides some context about how that string will be used. During runtime, the app knows to source the appropriate localized string based on the user’s locale (if the localized string file is found in the app bundle).
Here are some examples:
aimLaserAtBarcodeLabel.text = NSLocalizedString(@"Aim laser at barcode",
@"text on Laser Scan button instructing the user to aim the laser at a barcode to scan
(the translated string needs to be short)");
NSLocalizedString(@"Duplicate Barcode",
@"Error message on scan view when attendee's ticket has already been scanned")
In the first example, I added the extra explanation in parentheses to let the translator know that the string needs to be as short as possible to fit inside the scan button.
The second example is a case where having a comment to explain the context resolves ambiguity and provides clarity to the translator. At first glance, “Duplicate” looks like a verb but in this case it’s actually an adjective.
Generating the Strings File
Once you’ve prepared a few strings in the app to be localized, run the following command in the project’s root directory:
find . -name '*.m' | xargs genstrings -o ./Resources/en.lproj/
GenStrings is a command-line app included with XCode that pulls out all the instances of NSLocalizedString in your code. Running the command above will create or overwrite a file called Localizable.strings in the Resources/en.proj directory. This file needs to be there for English strings to display correctly and it will act as the default if a language file is missing.
Take a look at the file; you’ll find lines like this:
/* message shown in table view when nothing matched the user's search query */ "No Results" = "No Results"; /* label below light icon when the iPhone's LED light is off */ "OFF" = "OFF";
The commented part is the text you provided as the second parameter to NSLocalizedString. Below the comment is the key-value pair: the key, which was the first parameter to NSLocalizedString, and the value, which is the actual string to display. For the English file, the key and value will always be the same. For other languages, the values will be translated so you end up with lines like this:
/* message shown in table view when nothing matched the user's search query */ "No Results" = "No hay resultados"; /* label below light icon when the iPhone's LED light is off */ "OFF" = "Apag.";
Text in Resource Files
There are strings in the views that make up an app that need to be localized: button labels, static text fields, etc.
In Android, localizing the strings in activity layout files is trivial because there should be no strings in these files. The Activity layout XML should always use string constants.
In iOS, there are a few different ways you can localize XIB files. Some people recommend making copies of the XIB file in each locale folder and having the translator update the strings directly in the XIB, but there are drawbacks to doing it this way. For one, it means that any changes to the UI need to be made across all the XIB files. The other reason is that passing a XIB file to a translator is not very practical. It’s a big XML file with a lot of overhead. The method I found worked best involved overriding the strings at runtime:
1. For each localizable string in a XIB, create an IBOutlet through Interface Builder.
2. While you’re in there, grab the string, translate it into German or French using Google Translate, and paste the translated string back into the field in Interface Builder. You’ll probably find the field needs to be enlarged to accommodate the longer string.
3. In the viewDidLoad for that view, set the label of each field dynamically using NSLocalizedString e.g. scanBarcodeLabel.text = NSLocalizedString(@”Scan barcode below”, @”text on Camera Scan view explaining that the barcode should be scanned in the reticle below”);
The advantage to doing it this way is that we end up with just a single file containing all the strings that need to be passed to the translator. This works well with the Transifex tool that we’re using to manage translations (www.transifex.net).
Accessibility Labels
Accessibility labels are there to help the sight-impaired know what text is accessible on the screen, so these should also be localized.
Avoid Plural/Singular Logic
In English, we have a rule that says that nouns are singular for exactly one item and plural for all other numbers. If we consider this rule we end up with (pseudo)code that looks something like this:
baseString = "You have %d ticket%s"; endString = ""; if (count(tickets) != 1) endString = "s"; printf(myStr, count(tickets), endString);
This code works for English, but the logic breaks down for other languages where noun endings are more complicated. In general, you should avoid embedding or concatenating strings. In Russian there are different forms for singular, dual and plural. And there may be other things influencing the ending besides the count, such as the gender (masculine/feminine) and the case (Nominative, Accusative, Genitive, etc.).
Instead of trying to format strings that look like this…
You have 1 ticket. You have 4 tickets.
… convert the string to show the number by itself after a colon:
Number of Tickets: 1
Dates
The recommended way to internationalize dates is to work with the set of predefined formats provided by the OS rather than forcing a custom format that may only be meaningful in our U.S. English locale. Cocoa provides a great class NSDateFormatter that includes a set of short, medium, long and full predefined date and time formats.
The Entry Manager code included the following custom date formatter to display a date like this: Mon. August 15, 2011 – 5:00 PM
[myDate setDateFormat:@"EEE, MMM d, yyyy – h:mm a"];
This custom format works fine in the United States, but breaks down in other countries. For example, most languages list the day before the month, not after it. Also, it’s normal in Europe to display 24h time and not tack on an AM/PM. Using the custom formatter above, we’d end up with the following in French:
mar., févr. 14, 2012 – 5:00 PM
Whereas French speakers would normally write:
mardi 14 février 2012 17:00
Note there is no punctuation (commas or dashes).
So please don’t use custom number formatters. Cocoa provides the following constants to use for various length strings (with examples in US English):
NSDateFormatterShortStyle: “11/23/37” or “3:30pm”
NSDateFormatterMediumStyle: “Nov 23, 1937”
NSDateFormatterLongStyle: “November 23, 1937” or “3:30:32pm”
NSDateFormatterFullStyle: “Tuesday, April 12, 1952” or “3:30:42pm PST”
Incorporating these into code we can generate a date and time string with a long-form date and a time in hours and minutes:
NSDateFormatter *dateFormat = [[[NSDateFormatter alloc] init] autorelease]; [dateFormat setDateStyle:NSDateFormatterFullStyle];
NSDateFormatter *timeFormat = [[[NSDateFormatter alloc] init] autorelease]; [timeFormat setTimeStyle:NSDateFormatterShortStyle];
NSString *dateAndTimeStringFormat = NSLocalizedString(@"When: %@ at %@", @"Date and time of event (e.g. Friday, December 21, 2011 at 5:51 PM)");
NSString *dateAndTimeString = [NSString stringWithFormat: dateAndTimeStringFormat, [dateFormat stringFromDate:eventDate], [timeFormat stringFromDate:eventDate]];
Conclusion
Localizing an app isn’t difficult but it can be made a lot easier if it’s considered upfront and not as an afterthought. There are a few simple things you can do when writing new code or refactoring:
- Use predefined date formats, not custom formats
- Avoid singular/plural issues in strings. Don’t embed strings or concatenate them—throw the number at the end after a colon.
- When adding new strings, use NSLocalizedString and provide a meaningful comment. This will help developers understand your code, not just the translators.
- Consider UI layouts where buttons and labels might need more space to display than they do in English.
Merci!
Are you an engineer looking to join an awesome team? Check out our jobs page for the latest positions. You could be just who we’re looking for!