Chapter 17: Internationalization and Localization

Localization is a key concern for any application with a global market. Users want to interact in their own languages, with their familiar formatting. Supporting this in your application is called internationalization (sometimes abbreviated “i18n” for the 18 characters between the “i” and the “n”) and localization (“L10n”). The differences between i18n and L10n aren’t really important or consistently agreed upon. Apple says, “Internationalization is the process of designing and building an application to facilitate localization. Localization, in turn, is the cultural and linguistic adaptation of an internationalized application to two or more culturally-distinct markets.” (See “Internationalization Programming Topics” at developer.apple.com.) This chapter uses the terms interchangeably.

After reading this chapter, you will have a solid understanding of what localization is and how to approach it. Even if you’re not ready to localize your application yet, this chapter provides easy steps to dramatically simplify localization later. You find out how to localize strings, numbers, dates, and nib files, and how to regularly audit your project to make sure it stays localizable.

What Is Localization?

Localization is more than just translating strings. Localization means making your application culturally appropriate for your target audience. That includes translating language in strings, images, and audio, and formatting numbers and dates. iOS 6 adds auto layout, which makes localization much simpler. This is discussed in Chapter 7.

Here are some general things you can do to improve your iOS localizations:

Keep nib files simple. This isn’t difficult on iOS because there aren’t as many complicated things you can do in a nib as you can on a Mac. But just remember that every IBOutlet and IBAction connection you make has to be made in every localized nib file.

Separate nib files that require localization from ones that don’t. Many iOS nib files have no strings or localized images. You don’t need to localize these. If you just need a localized title, then make it an IBOutlet and set the localized value at runtime rather than localizing the nib file. String localization is much easier to maintain than nib file localization.

Remember right-to-left languages. This is one of the hardest things to fix later, especially if you have custom text views.

Don’t assume that a comma is the thousands separator or a dot is the decimal point. These are different in different languages, so build your regular expressions using NSLocale.

Glyphs (drawn symbols) and characters do not always map one-to-one. If you’re doing custom text layout, this can be particularly surprising. Apple’s frameworks generally handle this automatically, but don’t try to circumvent systems like Core Text when they force you to calculate the number of glyphs in a string rather than using length. This issue is particularly common in Thai, but exists in many languages (even occasionally in English, as we discuss in Chapter 26).

In my experience, it is best to do all of your development up to the point of release and then translate rather than try to translate as you go. The cost of localization is best absorbed at fixed points during development, generally at the end. It’s expensive to retranslate things every time you tweak the UI.

Although translation is best done near the time of release, you should line up your localization provider fairly early in the development cycle and prepare for localization throughout the process. A good localization provider does more than just translate a bunch of strings. Ideally, your localization provider will provide testing services to make sure your application “makes sense” in the target culture. Getting the provider involved early in the process can save expensive rework later if your interface is hard to localize.

An example of a “hard-to-localize” application is one that includes large blocks of text. Translating large blocks of text can play havoc with layout, even when using auto-layout. Remembering that you will often pay by the word for translation may help you focus on reducing the number of words you use. Redesign your UI so it doesn’t need so much text to let the user know what to do. Rely on Apple’s UI elements and icons as much as possible. Apple’s done the hard and expensive work for you to make them internationally appropriate. Don’t waste that. For example, when using a UIToolBarItem, use a system item whenever appropriate rather than drawing your own icons. If the icon’s meaning matches your intent, always try to use the system icon, even if you believe you could create a better one. In our opinion, the “action” icon (an arrow coming out of a box) is incomprehensible, but users are used to it. Apple has trained them in what it means, so you should use it. Never use a system icon for something other than its intended meaning, however. For example, do not use UIBarButtonSystemItemReply to mean “go left” or “go back.”

Another frequent localization problem is icons that assume a cultural background, such as a decorated tree to indicate “winter.” Check marks can also cause problems, because they are not used in all cultures (French for instance), and in some cultures a check mark means “incorrect” (Finnish, for instance). Involving a good localization provider before producing your final artwork can save you a lot of money re-creating your assets.

Localizing Strings

The most common tool for localizing strings is NSLocalizedString. This function looks up the given key in Localizeable.strings and returns the value found there, or the key itself if no value is found. Localizeable.strings is a localized file, so there is a different version for each language, and NSLocalizedString automatically selects the correct one based on the current locale. A command-line tool called genstrings automatically searches your files for calls to NSLocalizedString and writes your initial Localizeable.strings file for you.

The easiest approach is to use the string as its own key (the second parameter is a comment to the localizer):

NSString *string =

    NSLocalizedString(@”Welcome to the show.”,

                      @”Welcome message”);

To run genstrings, you open a terminal, change to your source code directory, and run it as shown here (assuming an English localization):

genstrings -o en.lproj *.m

This creates a file called en.lproj/Localizeable.string that contains the following:

/* Welcome message */

“Welcome to the show.” = “Welcome to the show.”;

Even if you don’t run genstrings, this works in the developer’s language because it automatically returns the key as the localized string.

In most cases, I recommend using the string as its own key and automatically generating the Localizeable.strings file when you’re ready to hand the project off to localizers. This approach simplifies development and helps keep the Localizeable.strings file from accumulating keys that are no longer used.

Auditing for Nonlocalized Strings

During development, be sure to periodically audit your program to make sure that you’re using NSLocalizedString as you should. I recommend a script like this:

find_nonlocalized

#!/usr/bin/perl -w

# Usage:

#     find_nonlocalized [<directory> ...]

#

# Scans .m and .mm files for potentially nonlocalized

#   strings that should be.

# Lines marked with DNL (Do Not Localize) are ignored.

# String constant assignments of this form are ignored if

#   they have no spaces in the value:

#   NSString * const <...> = @”...”;

# Strings on the same line as NSLocalizedString are

#   ignored.

# Certain common methods that take nonlocalized strings are

#   ignored

# URLs are ignored

#

# Exits with 1 if there were strings found

use File::Basename;

use File::Find;

use strict;

# Include the basenames of any files to ignore

my @EXCLUDE_FILENAMES = qw();

# Regular expressions to ignore

my @EXCLUDE_REGEXES = (

    qr/DNL/,

    qr/NSLocalizedString/,

    qr/NSStrings**s*consts[^@]*@”[^ ]*”;/,

    qr/NSLog(/,

    qr/@”http/, qr/@”mailto/, qr/@”ldap/,

    qr/predicateWithFormat:@”/,

    qr/Key(?:[pP]ath)?:@”/,

    qr/setDateFormat:@”/,

    qr/NSAssert/,

    qr/imageNamed:@”/,

    qr/NibNamed?:@”/,

    qr/pathForResource:@”/,

    qr/fileURLWithPath:@”/,

    qr/fontWithName:@“/,

    qr/stringByAppendingPathComponent:@“/,

);

my $FoundNonLocalized = 0;

sub find_nonlocalized {

    return unless $File::Find::name =~ /.mm?$/;

    return if grep($_, @EXCLUDE_FILENAMES);

    

    open(FILE, $_);

    LINE:   

    while (<FILE>) {

        if (/@“[^“]*[a-z]{3,}/) {

            foreach my $regex (@EXCLUDE_REGEXES) {

                next LINE if $_ =~ $regex;

            }

            print „$File::Find::name:$.:$_“;

            $FoundNonLocalized = 1;

        }

    }

    close(FILE);

}

my @dirs = scalar @ARGV ? @ARGV : („.“);

find(&find_nonlocalized, @dirs);

exit $FoundNonLocalized ? 1 : 0;

Periodically run this script over your source to make sure that there are no nonlocalized strings. If you use Jenkins at (jenkins-ci.org) or another continuous-integration tool, you can make this script part of the build process, or you can add it as a script step in your Xcode build. Whenever it returns a new string, you can decide whether to fix it, update the regular expressions to ignore it, or mark the line with DNL (Do Not Localize).

Formatting Numbers and Dates

Numbers and dates are displayed differently in different locales. This is generally straightforward using NSDateFormatter and NSNumberFormatter, which you are likely already familiar with.

For an introduction to NSDateFormatter and NSNumberFormatter, see the “Data Formatting Guide” in Apple’s documentation at developer.apple.com.

There are a few things to keep in mind, however. First, formatters are needed for input as well as output. Most developers remember to use a formatter for date input, but may forget to use one for numeric input. The decimal point is not universally used to separate whole from fractional digits on input. Some countries use a comma or an apostrophe. It’s best to validate number input using an NSDateFormatter rather than custom logic.

Digit groupings have a bewildering variety. Some countries split thousands groups with space, comma, or apostrophe. China sometimes groups ten thousands (four digits). Don’t guess. Use a formatter. Remember that this can impact the length of your string. If you leave room for only seven characters for one hundred thousand (“100,000”) you may overflow in India, which uses eight (“1,00,000” or one lakh).

Percentages are another place where you need to be careful because different cultures place the percent sign at the beginning or end of the number, and some use a slightly different symbol. Using NSNumberFormatterPercentStyle will behave correctly.

Be especially careful with currency. Don’t store currency as a float because that can lead to rounding errors as you convert between binary and decimal. Always store currency as an NSDecimalNumber, which does its math in decimal. Keep track of the currency you’re working in. If your user switches locale from the U.S. to France, don’t switch his $1 purchase to @@eu1. Generally, you need to persist in using the currency in which a given value is expressed. The RNMoney class is an example of how to do this. First, the following code demonstrates how to use the class to store Rubles and Euros.

main.m (Money)

  NSLocale *russiaLocale = [[NSLocale alloc]

                        initWithLocaleIdentifier:@”ru_RU”];

  

  RNMoney *money = [[RNMoney alloc]

                    initWithIntegerAmount:100];

  NSLog(@”Local display of local currency: %@”, money);

  NSLog(@”Russian display of local currency: %@”,

        [money localizedStringForLocale:russiaLocale]);

  

  RNMoney *euro =[[RNMoney alloc] initWithIntegerAmount:200

                                      currencyCode:@”EUR”];

  NSLog(@”Local display of Euro: %@”, euro);

  NSLog(@”Russian display of Euro: %@”,

        [euro localizedStringForLocale:russiaLocale]);

RNMoney is an immutable object that stores an amount and a currency code. If you do not provide a currency code, it defaults to the current locale’s currency. It is a very simple data class designed to be easy to initialize, serialize, and format. Here is the code.

RNMoney.h (Money)

#import <Foundation/Foundation.h>

@interface RNMoney : NSObject <NSCoding>

@property (nonatomic, readonly, strong)

                                   NSDecimalNumber *amount;

@property (nonatomic, readonly, strong)

                                    NSString *currencyCode;

- (RNMoney *)initWithAmount:(NSDecimalNumber *)anAmount

         currencyCode:(NSString *)aCode;

- (RNMoney *)initWithAmount:(NSDecimalNumber *)anAmount;

- (RNMoney *)initWithIntegerAmount:(NSInteger)anAmount

                      currencyCode:(NSString *)aCode;

- (RNMoney *)initWithIntegerAmount:(NSInteger)anAmount;

- (NSString *)localizedStringForLocale:(NSLocale *)aLocale;

- (NSString *)localizedString;

@end

RNMoney.m (Money)

#import “RNMoney.h”

@implementation RNMoney

static NSString * const kRNMoneyAmountKey = @”amount”;

static NSString * const kRNMoneyCurrencyCodeKey =

                                           @”currencyCode”;

- (RNMoney *)initWithAmount:(NSDecimalNumber *)anAmount

               currencyCode:(NSString *)aCode {

  if ((self = [super init])) {

    _amount = anAmount;

    if (aCode == nil) {

      NSNumberFormatter *formatter = [[NSNumberFormatter alloc] init];

      _currencyCode = [formatter currencyCode];

    }

    else {

      _currencyCode = aCode;

    }

  }

  return self;

}

- (RNMoney *)initWithAmount:(NSDecimalNumber *)anAmount {

  return [self initWithAmount:anAmount

                 currencyCode:nil];

}

- (RNMoney *)initWithIntegerAmount:(NSInteger)anAmount

                      currencyCode:(NSString *)aCode {

    return [self initWithAmount:

            [NSDecimalNumber decimalNumberWithDecimal:

             [[NSNumber numberWithInteger:anAmount]

              decimalValue]]

                   currencyCode:aCode];

}

- (RNMoney *)initWithIntegerAmount:(NSInteger)anAmount {

  return [self initWithIntegerAmount:anAmount

                        currencyCode:nil];

}

- (id)init {

  return [self initWithAmount:[NSDecimalNumber zero]];

}

- (id)initWithCoder:(NSCoder *)coder {

  

  NSDecimalNumber *amount = [coder decodeObjectForKey:

                             kRNMoneyAmountKey];

  NSString *currencyCode = [coder decodeObjectForKey:

                            kRNMoneyCurrencyCodeKey];

  return [self initWithAmount:amount

                 currencyCode:currencyCode];

}

- (void)encodeWithCoder:(NSCoder *)aCoder {

  [aCoder encodeObject:amount_ forKey:kRNMoneyAmountKey];

  [aCoder encodeObject:currencyCode_

                forKey:kRNMoneyCurrencyCodeKey];

}

- (NSString *)localizedStringForLocale:(NSLocale *)aLocale

{

  NSNumberFormatter *formatter = [[NSNumberFormatter alloc]

                                  init];

  [formatter setLocale:aLocale];

  [formatter setCurrencyCode:self.currencyCode];

  [formatter setNumberStyle:NSNumberFormatterCurrencyStyle];

  return [formatter stringFromNumber:self.amount];

}

- (NSString *)localizedString {

  return [self localizedStringForLocale:

          [NSLocale currentLocale]];

}

- (NSString *)description {

  return [self localizedString];

}

@end

Nib Files and Base Internationalization

iOS 6 adds a new feature called “Base Internationalization.” In the Project Info panel, you can select Use Base Internationalization, and Xcode will convert your project to the new system. Prior to Base Internationalization, you needed a copy of all of your nib files for every locale. With Base Internationalization, there is an unlocalized version of your nib and storyboard files, and there is a strings file for every localization. iOS takes care of inserting all of the strings into the nib files for you at runtime, greatly simplifying nib localization. You may still want to create individually localized nib files in some cases.

Some languages require radically different layout. For example, visually “large” languages like Russian and dense languages like Chinese may not fit well in the layout used for English or French. Right-to-left languages may need some special handling as well. Luckily, you can still create per-language nib files while using Base Internationalization.

Localizing Complex Strings

Sentence structure is radically different among languages. This means that you can almost never safely compose a string from parts like this:

  NSString *intro = @”There was an error deleting”;

  NSString *num = [NSString stringWithFormat:@”%d”, 5];

  NSString *tail = @”objects.”;

  NSString *str = [NSString stringWithFormat:@”%@ %@ %@”,

                      intro, num, tail]; // Wrong

The problem with this code is that when you translate “There was an error deleting” and “objects” into other languages, you may not be able to glue them together in the same order. Instead, you need to localize the entire string together like this:

  NSString *format = NSLocalizedString(

                 @”There was an error deleting %d objects”,

                 @”Error when deleting objects.”);

  NSString *str = [NSString stringWithFormat:format, 5];

Some languages have more complex plurals than English. For instance, there may be special word forms for describing two of something versus more than two. Don’t assume you can check for greater-than-one and easily determine linguistic plurals. Solving this well can be very difficult, so try to avoid it instead. Don’t have special code that tries to add an s to the end of plurals because this is almost impossible to translate. A good translator will help you word your messages in ways that translate better in your target languages.

Talk with your localization provider early on to understand its process and how to adjust your development practice to facilitate working with it. Figure 17-1 demonstrates a good approach.

9781118449974-fg1701.eps

Figure 17-1 Localization workflow

1. Pseudo-Localize—During development, it’s a good idea to start doing experimental localization to work out any localization bugs early in the process. Pseudo-localization is the process of localizing into a nonsense language. A common nonsense language is one that substitutes all vowels with the letter x. For example, “Press here to continue” would become “Prxss hxrx tx cxntxnxx.” This kind of “translation” can be done by developers, generally with a simple script, and will make it more obvious where you have used nonlocalized strings. This won’t find every problem. In particular, it is not good at discovering strings that are pieced together from other strings, but it can find many simple problems before you pay for real translation services. You will need a language code for this localization. Pick a language that you do not plan to localize your application for. If you’re an American English speaker and don’t plan to localize for British English, it is particularly useful to use the British English slot for this purpose because you’ll still be able to easily read the rest of the iPhone’s interface.

2. UI Freeze—There should be a clear point in the development cycle at which you freeze the UI. After that point, firmly avoid any changes that affect localizable resources. Many teams ship a monolingual version of their product at this point and then ship a localization update. That’s the easiest approach if your market is tolerant of the delay.

3. Localize—You will send your resource files to your localizers, and they will send you back localized files. Nib files can be locked in Xcode to prevent changing localizable, nonlocalizable, or all properties. Before sending nib files to a localizer, lock nonlocalizable properties to protect your nib files against changes to connections, class names, and other invisible attributes of the nib file. Figure 17-2 shows the lock option in Interface Builder.

9781118449974-fg1702.eps

Figure 17-2 Interface Builder localization locking option

4. Version Control—As you make changes to your nib files, you will need to keep track of the original files your localizer sent to you. Lock the localizable properties in the nib files (unlocking the nonlocalizable properties). Then put these into a version control system or save them in a separate directory.

5. Testing—You’ll need to do extensive testing to make sure that everything is correct. Ideally, you will have native speakers of each of your localized languages test all your UI elements to ensure that they make sense and that there aren’t any leftover nonlocalized strings. A good localizer can assist in this.

6a. Merge Logic Changes—Certain nib file changes do not affect localization. Changes to connections or class names don’t change the layout or the resources. These are logic changes rather than localization (L10n) changes. You can merge the localized nib files like this:

ibtool --previous-file ${OLD}/en.lproj/MyNib.nib

       --incremental-file ${OLD}/fr.lproj/MyNib.nib

       --strings-file ${NEW}/fr.lproj/Localizeable.strings

       --localize-incremental

       --write ${NEW}/fr.lproj/MyNib.nib

       ${NEW}/en.lproj/MyNib.nib

This computes the nonlocalization changes between the old and new English MyNib.nib. It then applies these changes to the old French MyNib.nib and writes it as the new French nib file. As long as you keep track of the original files you were sent by the localizer, this works quite well for nonlayout changes, and can be scripted fairly easily.

6b. L10n Changes—If you make localization changes such as changing the layout of a localized nib file or changing a string, you’ll need to start the process over and send the changes to the localizer. You can reuse the previous string translations, which makes things more efficient, but it is still a lot of work, so avoid making these changes late in the development cycle.

Summary

Localization is never an easy subject, but if you work with a good localization partner early and follow the best practices detailed here, you can greatly expand the market for your applications.

Further Reading

Apple Documentation

The following documents are available in the iOS Developer Library at developer.apple.com or through the Xcode Documentation and API Reference.

Data Formatting Guide

Internationalization Programming Topics

Locales Programming Guide

WWDC Sessions

WWDC 2012, “Session 244: Internationalization Tips and Tricks”

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.220.9.237