Localization - iOS Programming: The Big Nerd Ranch Guide (2014)

iOS Programming: The Big Nerd Ranch Guide (2014)

25. Localization

The appeal of iOS is global – iOS users live in many different countries and speak many different languages. You can ensure that your application is ready for this global audience through the processes of internationalization and localization. Internationalization is making sure your native cultural information is not hard-coded into your application. (By cultural information, we mean language, currency, date formats, number formats, and more.)

Localization, on the other hand, is the process of providing the appropriate data in your application based on the user’s Language and Region Format settings. You can find these settings in the Settings application. Select the General row and then the International row.

Figure 25.1 International settings

International settings

Apple makes these processes relatively simple. An application that takes advantage of the localization APIs does not even need to be recompiled to be distributed in other languages or regions. In this chapter, you are going to localize the item detail view of Homepwner. (By the way,“internationalization” and “localization” are long words. You will sometimes see people abbreviate them to i18n and l10n, respectively.)

Internationalization Using NSNumberFormat

In this first section, you will use the class NSNumberFormat to internationalize the number format and currency symbol for the value of an item.

Did you know that Homepwner is already partially internationalized? Launch the application and add a new item. The date label in the BNRDetailViewController is formatted according to the current regional settings. In the US, the dates are displayed as Month Day, Year. Cancel adding a new item.

Now open the Settings application and change Region Format to United Kingdom (General → International → Region Format). Switch back to Homepwner and add a new item again. This time, the date is displayed as Day Month Year. The text for the date label has already been internationalized. When did this happen?

Figure 25.2 Date format: US vs UK

Date format: US vs UK

In Chapter 10, you used an instance of NSDateFormatter to set the text of the date label of BNRDetailViewController. NSDateFormatter has a locale property, which is set to the device’s current locale. Whenever you use an NSDateFormatter to create a date, it checks its locale property and sets the format accordingly. So the text of the date label has been internationalized from the start.

NSLocale knows how different regions display symbols, dates, and decimals and whether they use the metric system. An instance of NSLocale represents one region’s settings for these variables. In the Settings application, the user can choose a region, like United States or United Kingdom. (Why does Apple use “region” instead of “country?” Some countries have more than one region with different settings. Scroll through the options in Region Format to see for yourself.)

When you send the message currentLocale to NSLocale, the instance of NSLocale that represents the user’s region setting is returned. Once you have that instance of NSLocale, you can ask it questions like, “What is the currency symbol for this region?” or “Does this region use the metric system?”

To ask one of these questions, you send the NSLocale instance the message objectForKey: with one of the NSLocale constants as an argument. (You can find all of these constants in the NSLocale documentation page.)

NSLocale *locale = [NSLocale currentLocale];

BOOL isMetric = [[locale objectForKey:NSLocaleUsesMetricSystem] boolValue];

NSString *currencySymbol = [locale objectForKey:NSLocaleCurrencySymbol];

Let’s internationalize the value amount displayed in each BNRItemCell. Open Homepwner.xcodeproj.

While NSLocale is extremely powerful and useful, always using it directly would make the process of localizing apps very tedious. That's why you used NSDateFormatter earlier. There is another class, NSNumberFormatter that does for numbers what NSDateFormatter does for dates. For example, to format a number appropriately for the current locale, use the stringFromNumber: method. Depending on the locale, the numberAsString may be 123,456.789 or 123 456,789 or some other value.

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

NSString *numberAsString = [numberFormatter stringFromNumber:@123456.789];

What makes NSNumberFormatter even more useful is its capability to format currency amounts. If the number formatter's numberStyle property is set to NSNumberFormatterCurrencyStyle, it will start producing the numbers formatted not only with the appropriate group and decimal separators, but also with the currency symbol. (In some countries, numbers may be formatted differently for currency and non-currency purposes.)

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

currencyFormatter.numberStyle = NSNumberFormatterCurrencyStyle;

NSString *numberAsString = [currencyFormatter stringFromNumber:@123456.789];

In BNRItemsViewController.m, locate the method tableView:cellForRowAtIndexPath:. Add the static variable currencyFormatter and set its numberStyle to NSNumberFormatterCurrencyStyle.

cell.serialNumberLabel.text = item.serialNumber;

// Create a number formatter for currency

static NSNumberFormatter *currencyFormatter = nil;

if (currencyFormatter == nil) {

currencyFormatter = [[NSNumberFormatter alloc] init];

currencyFormatter.numberStyle = NSNumberFormatterCurrencyStyle;

}

cell.valueLabel.text = [NSString stringWithFormat:@"$%d",

item.valueInDollars];

When the text of the cell’s valueLabel is set in this method, the string "$%d" is used, which makes the currency symbol always a dollar sign. Use the currencyFormatter to format the amount correctly.

// Create a number formatter for currency

static NSNumberFormatter *currencyFormatter = nil;

if (currencyFormatter == nil) {

currencyFormatter = [[NSNumberFormatter alloc] init];

currencyFormatter.numberStyle = NSNumberFormatterCurrencyStyle;

}

cell.valueLabel.text = [NSString stringWithFormat:@"$%d",

item.valueInDollars];

cell.valueLabel.text = [currencyFormatter

stringFromNumber:@(item.valueInDollars)];

cell.thumbnailView.image = item.thumbnail;

These changes will display the value formatted appropriately for the user’s region, with both the number format and currency symbol.

Build and run the application. You will see the value amount formatted according to the currently selected region, which should be United Kingdom if you followed the instructions at the beginning of this section. In the Settings application, change Region Format back to United States (General → International→ Region Format). Return to Homepwner.

You were probably expecting to see values displayed in dollars ($). However, it did not happen. To trigger the update to the table view, start adding a new item and immediately cancel. Now, you will see the values formatted correctly, because viewWillAppear: was called and it reloaded the table view. (Note that this is not a currency conversion; you are just replacing the symbol.)

To make Homepwner update when the region settings change, you need to use NSNotificationCenter. In BNRItemsViewController’s init method, register for locale change notifications:

NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];

[nc addObserver:self

selector:@selector(updateTableViewForDynamicTypeSize)

name:UIContentSizeCategoryDidChangeNotification

object:nil];

// Register for locale change notifications

[nc addObserver:self

selector:@selector(localeChanged:)

name:NSCurrentLocaleDidChangeNotification

object:nil];

}

return self;

Add the method localeChanged.

- (void)localeChanged:(NSNotification *)note

{

[self.tableView reloadData];

}

Build and run the application. Now, when you change the regional settings and return to Homepwner, the table view will be reloaded and the value label will display the amount properly formatted. To see why you used the number formatter instead of just retrieving the currency symbol, change the region to Germany. Not only did the currency symbol change, but also a few other things: the position of the currency symbol (after the number, instead of before), spacing (one space between the amount and the currency symbol, instead of no spaces), decimal mark (a comma instead of a dot), and thousands separator (a dot instead of a comma).

Figure 25.3 Number format: US vs UK vs Germany

Number format: US vs UK vs Germany

Localizing Resources

When internationalizing, you ask the instance of NSLocale questions. But the NSLocale only has a few region-specific variables. This is where localization comes into play: Localization is the process by which application-specific substitutions are created for different region and language settings.

Localization usually means one of two things:

· generating multiple copies of resources like images, sounds, and NIB files for different regions and languages

· creating and accessing strings tables to translate text into different languages

Any resource, whether it is an image or a XIB file, can be localized. Localizing a resource puts another copy of the resource in the application bundle. These resources are organized into language-specific directories, known as lproj directories. Each one of these directories is the name of the localization suffixed with lproj. For example, the American English localization is en_US: where en is the English language code and US is the United States of America region code. (The region can be omitted if you do not need to make regional distinctions in your resource files.) These language and region codes are standard on all platforms, not just iOS.

When a bundle is asked for the path of a resource file, it first looks at the root level of the bundle for a file of that name. If it does not find one, it looks at the locale and language settings of the device, finds the appropriate lproj directory, and looks for the file there. Thus, just by localizing resource files, your application will automatically load the correct file.

One option is to create separate XIB files and to manually edit each string in this XIB file in Xcode. However, this approach does not scale well if you are planning multiple localizations. What happens when you add a new label or button to your localized XIB? You have to add this view to the XIB for every language. This is not fun.

To simplify the process of localizing XIB files, Xcode has a feature called Base internationalization. When it is enabled for the project, Base internationalization creates the Base.lproj directory which contains the main XIB files. Localizing individual XIB files can then be done by creating just the Localizable.strings files. It is still possible to create the full XIB files, in case localization cannot be done by changing strings alone. However, with the help of Auto Layout, strings replacement may be sufficient for most localization needs.

In this section, you are going to localize one of Homepwner’s interfaces: the BNRDetailViewController.xib file. You will create English and Spanish localizations, which will create two lproj directories, in addition to the base one. Normally, you would first enable Base Internationalization in the project Info settings. However, as of this writing, there is a bug in Xcode that will not let you enable that option until at least one XIB file is localized.

So, start by localizing a XIB file. Select BNRDetailViewController.xib in the project navigator. Then, show the utility area.

Click the Localizing Resources tab in the inspector selector to open the file inspector. Find the section in this inspector named Localization and click the Localize... button (Figure 25.4).

Figure 25.4 Localizing BNRDetailViewController.xib, beginning

Localizing BNRDetailViewController.xib, beginning

Select English. This signifies to Xcode that this file can be localized, automatically creates en.lproj, and moves the BNRDetailViewController.xib file to it.

Now you need to enable Base Internationalization. Select the project file as shown in Figure 25.5. Make sure you select the project Homepwner, and not the target Homepwner.

Figure 25.5 Selecting Project Info

Selecting Project Info

In the bottom section of the Info tab of the project, locate the Use Base Internationalization checkbox in the Localizations section and check it. You will see the prompt to select which files will be used to create the Base localization; the table will consist of just BNRDetailViewController.xib and English will be listed as the reference language. Click Finish.

This will create the Base.lproj directory and move BNRDetailViewController.xib to it.

Click the + button under the list of languages and select Spanish. In the dialog, you can uncheck the InfoPlist.strings files and only keep the BNRDetailViewController.xib file checked. Make sure that the reference language is Base and the file type is Localizable Strings. Click Finish. This creates anes.lproj folder and generates the BNRDetailViewController.strings in it that contains all the strings from the base XIB file. The Localizations configuration should look like Figure 25.6.

Figure 25.6 Localizations

Localizations

Look in the project navigator. Click the disclosure button next to BNRDetailViewController.xib (Figure 25.7). Xcode moved the BNRDetailViewController.xib file to the Base.lproj directory and created the BNRDetailViewController.strings file in the es.lproj directory.

Figure 25.7 Localized XIB in the project navigator

Localized XIB in the project navigator

Select the BNRDetailViewController.xib. It does not matter if you select the top level one or the one marked Base. The file inspector should look like Figure 25.8.

Figure 25.8 Localizing BNRDetailViewController.xib, result

Localizing BNRDetailViewController.xib, result

In the project navigator, click the Spanish version of BNRDetailViewController.strings. When this file opens, the text is not in Spanish. You have to translate localized files yourself; Xcode is not that smart.

Edit this file according to the following text. The numbers and order may be different in your file, but you can use the text field in the comment to match up the translations.

/* Class = "IBUILabel"; text = "Serial"; ObjectID = "JkL-nP-h3R"; */

"JkL-nP-h3R.text" = "Numéro de serie";

/* Class = "IBUILabel"; text = "Label"; ObjectID = "Q5n-Bc-7IH"; */

"Q5n-Bc-7IH.text" = "Label";

/* Class = "IBUILabel"; text = "Name"; ObjectID = "qzL-Fn-qch"; */

"qzL-Fn-qch.text" = "Nombre";

/* Class = "IBUILabel"; text = "Value"; ObjectID = "rhE-7e-oTE"; */

"rhE-7e-oTE.text" = "Valor";

/* Class = "IBUIBarButtonItem"; title = "Item"; ObjectID = "uNg-wM-Zcr"; */

"uNg-wM-Zcr.title" = "Item";

Notice that you did not change the Label and Item text because those strings will be replaced programmatically at runtime. Save this file.

Now that you have finished localizing this XIB file, let’s test it out. First, there is a little Xcode glitch to be aware of: sometimes Xcode just ignores a resource file’s changes when you build an application. To ensure your application is being built from scratch, first delete it from your device or simulator. (Press and hold its icon in the launcher. When it starts to wiggle, tap the delete badge.) Relaunch Xcode (yes, exit and start it again). Then, choose Clean from the Product menu. Finally, to be absolutely sure, press and hold the alt/option button while opening the Product menu and chooseClean Build Folder.... This will force the application to be entirely re-compiled, re-bundled, and re-installed.

Homepwner’s detail view will not appear in Spanish until you change the language settings on the device or simulator. In Settings, change the language settings to Español (General → International → Language) and then relaunch your application. Select an item row, and you will see the interface in Spanish. However, the labels are being cut off (Figure 25.9).

Figure 25.9 Spanish BNRDetailViewController.xib, before and after layout fix

Spanish BNRDetailViewController.xib, before and after layout fix

Fortunately, thanks to Auto Layout, this is an easy one to fix. Open the BNRDetailViewController.xib file and select the name label. In the utility area, select the size inspector (the Spanish BNRDetailViewController.xib, before and after layout fix tab) and locate the width constraint. Currently, the width is set to be equal to 55, which is too short for the longer Spanish labels. Click on the drop-down next to the gear button of the width constraint and choose Select and Edit.... Change the relation from Equal to Greater Than or Equal.

Now, you will fix the widths of the serial number and value labels. Select the serial number label and locate its width constraint. Instead of editing it, just delete it. You want the text fields to be of the same size and the way to achieve this is to make the labels the same widths. After you deleted the width constraint, control-click on the serial number label and drag to the name label. In the menu, select the Equal widths item. Repeat the same steps for the value label. Build and run the application. This time, the text fields will resize to make space for the labels, so that they will not be cut off.

NSLocalizedString() and Strings Tables

In many places in your applications, you create NSString instances dynamically or display string literals to the user. To display translated versions of these strings, you must create a strings table. A strings table is a file containing a list of key-value pairs for all of the strings that your application uses and their associated translations. It is a resource file that you add to your application, but you do not need to do a lot of work to get data from it.

You might use a string in your code like this:

NSString *greeting = @"Hello!"

To internationalize the string in your code, you replace literal strings with the function NSLocalizedString.

NSString *greeting = NSLocalizedString(@"Hello!", @"The greeting for the user");

This function takes two arguments: a key and a comment that describes the string’s use. The key is the lookup value in a strings table. At runtime, NSLocalizedString() will look through the strings tables bundled with your application for a table that matches the user’s language settings. Then, in that table, the function gets the translated string that matches the key.

Now you are going to internationalize the string “Homepwner” that is displayed in the navigation bar. In BNRItemsViewController.m, locate the init method and change the line of code that sets the title of the navigationItem.

- (instancetype)init

{

// Call the superclass's designated initializer

self = [super initWithStyle:UITableViewStylePlain];

if (self) {

UINavigationItem *navItem = [self navigationItem];

navItem.title = @"Homepwner";

navItem.title = NSLocalizedString(@"Homepwner", @"Name of application");

Two more view controllers contain hard-coded strings that can be internationalized. The toolbar in the BNRDetailViewController shows the asset type. The title of the BNRAssetTypeViewController needs to be updated just like the title of the BNRItemsViewController.

In BNRDetailViewController.m, update the viewWillAppear: method:

NSString *typeLabel = [self.item.assetType valueForKey:@"label"];

if (!typeLabel) {

typeLabel = @"None";

typeLabel = NSLocalizedString(@"None", @"Type label None");

}

self.assetTypeButton.title = [NSString stringWithFormat:@"Type: %@", typeLabel];

self.assetTypeButton.title = [NSString stringWithFormat:

NSLocalizedString(@"Type: %@", @"Asset type button"), typeLabel];

[self updateFonts];

}

In BNRAssetTypeViewController.m, update the init method:

if (self) {

self.navigationItem.title = @"Asset Type";

self.navigationItem.title =

NSLocalizedString(@"Asset Type", @"BNRAssetTypeViewController title");

}

return self;

}

Once you have files that have been internationalized with the NSLocalizedString function, you can generate strings tables with a command-line application.

Open the Terminal app. If you have never used it before, this is a Unix terminal; it is used to run command-line tools. You want to navigate to the location of BNRItemsViewController.m. If you have never used the Terminal app before, here is a handy trick. In Terminal, type the following:

cd

followed by a space. (Do not press Enter yet.)

Next, open Finder and locate BNRItemsViewController.m and the folder that contains it. Drag the icon of that folder onto the Terminal window. Terminal will fill out the path for you. Press Enter.

The current working directory of Terminal is now this directory. For example, my terminal command looks like this:

cd /Users/aaron/Homepwner/Homepwner/

Use the terminal command ls to print out the contents of the working directory and confirm that BNRItemsViewController.m is in that list.

To generate the strings table, enter the following into Terminal and press Enter:

genstrings BNRItemsViewController.m

This creates a file named Localizable.strings in the same directory as BNRItemsViewController.m. Now you need to generate strings from the other two view controllers. Since the file Localizable.strings already exists, you will want to append to it, rather than create it from scratch. To do so, enter the following commands in the Terminal (do not forget the -a command line option) and press Enter after each line:

genstrings -a BNRDetailViewController.m

genstrings -a BNRAssetTypeViewController.m

The resulting file Localizable.strings now contains the strings from all three view controllers. Drag from the Finder into the project navigator (or use the Add Files to "Homepwner"... menu item). When the application is compiled, this resource will be copied into the main bundle.

Oddly enough, Xcode sometimes has a problem with strings tables. Open the Localizable.strings file in the editor area. If you see a bunch of upside-down question marks, you need to reinterpret this file as Unicode (UTF-16). Show the utility area and select the file inspector. Locate the area named Text Settings and change the pop-up menu next to Text Encoding to Unicode (UTF-16) (Figure 25.10). It will ask if you want to reinterpret or convert. Choose Reinterpret.

Figure 25.10 Changing encoding of a file

Changing encoding of a file

The file should look like this:

/* Name of application */

"Homepwner" = "Homepwner";

/* BNRAssetTypeViewController title */

"Asset Type" = "Asset Type";

/* Type label None */

"None" = "None";

/* Asset type button */

"Type: %@" = "Type: %@";

Notice that the comment above your string is the second argument you supplied to the NSLocalizedString function. Even though the function does not require the comment argument, including it will make your localizing life easier.

Now that you have created Localizable.strings, localize it in Xcode the same way you did the XIB file. Select the file in the project navigator and click the Localize... button in the utility area. Add the Spanish localization and then open the Spanish version of Localizable.strings. The string on the lefthand side is the key that is passed to the NSLocalizedString function, and the string on the righthand side is what is returned. Change the text on the righthand side to the Spanish translation shown below. (To type ñ, press Option-n and then “n”.)

/* Name of application */

"Homepwner" = "Dueño de casa"

/* AssetTypePicker title */

"Asset Type" = "Tipo de activo";

/* Type label None */

"None" = "Nada";

/* Asset type button */

"Type: %@" = "Tipo: %@";

Build and run the application again. Now all these strings, including the title of the navigation bar, will appear in Spanish. If they do not, you might need to delete the application, clean your project, and rebuild. (Or check your user language setting.)

Bronze Challenge: Another Localization

Practice makes perfect. Localize Homepwner for another language. Use Google Translate if you need help with the language.

For the More Curious: NSBundle’s Role in Internationalization

The real work of adding a localization is done for you by the class NSBundle. For example, when a UIViewController is initialized, it is given two arguments: the name of a XIB file and an NSBundle object. The bundle argument is typically nil, which is interpreted as the application’s main bundle. (The main bundle is another name for the application bundle – all of the resources and the executable for the application. When an application is built, all of the lproj directories are copied into this bundle.)

When the view controller loads its view, it asks the bundle for the XIB file. The bundle, being very smart, checks the current language settings of the device and looks in the appropriate lproj directory. The path for the XIB file in the lproj directory is returned to the view controller and loaded.

NSBundle knows how to search through localization directories for every type of resource using the instance method pathForResource:ofType:. When you want a path to a resource bundled with your application, you send this message to the main bundle. Here is an example using the resource file myImage.png:

NSString *path = [[NSBundle mainBundle] pathForResource:@"myImage"

ofType:@"png"];

The bundle first checks to see if there is a myImage.png file in the top level of the application bundle. If so, it returns the full path to that file. If not, the bundle gets the device’s language settings and looks in the appropriate lproj directory to construct the path. If no file is found, it returns nil.

This is why you must delete and clean an application when you localize a file. The previous un-localized file will still be in the root level of the application bundle because Xcode will not delete a file from the bundle when you re-install. Even though there are lproj folders in the application bundle, the bundle finds the top-level file first and returns its path.

For the More Curious: Localizing XIB files without Base Internationalization

Before Xcode had the Base internationalization feature, the localizable strings option was not available. Instead, you would maintain a XIB file for every locale that you wanted to support. So, you would have an en.lproj/BNRDetailViewController.xib and an es.lproj/BNRDetailViewController.xib. As you can imagine, maintaining every XIB in every language you wanted to support was a hassle.

To help with the creation and maintenance of localized XIB files, you could use a command-line tool named ibtool to suck the strings from your native language XIB file into a strings file. Then, you would translate these strings and create a new XIB file for each language.

To give it a try, open Terminal and navigate to the en.lproj directory. For example, my terminal command looks like this:

cd /Users/aaron/Homepwner/Homepwner/en.lproj

Next, use ibtool to suck the strings from this XIB file. Enter the following terminal command (all on one line) and enter it. (We only broke it up so that it would fit on the page.)

ibtool --export-strings-file ~/Desktop/BNRDetailViewController.strings

BNRDetailViewController.xib

This will create a BNRDetailViewController.strings file on your desktop that contains all of the strings in your XIB file. Open the Spanish BNRDetailViewController.strings. This is the same file as the one that Xcode created using localizable strings. Edit this file like before.

Now you will use ibtool to create a new Spanish XIB file. This file will be based on the English version of BNRDetailViewController.xib but will replace all of the strings with the values from BNRDetailViewController.strings. To pull this off, you need to know the path of your English XIB file and the path of your Spanish directory in this project’s directory. Remember, you opened these windows in Finder earlier.

In Terminal.app, enter the following command, followed by a space after write. (But do not press Enter yet!)

ibtool --import-strings-file ~/Desktop/BNRDetailViewController.strings --write

Next, find BNRDetailViewController.xib in es.lproj and drag it onto the terminal window. Then, find BNRDetailViewController.xib in the en.lproj folder and drag it onto the terminal window. Your command should look similar to this:

ibtool --import-strings-file ~/Desktop/BNRDetailViewController.strings --write

/iphone/Homepwner/Homepwner/es.lproj/BNRDetailViewController.xib

/iphone/Homepwner/Homepwner/en.lproj/BNRDetailViewController.xib

This command says, “Create BNRDetailViewController.xib in es.lproj from the BNRDetailViewController.xib in en.lproj, and then replace all of the strings with the values from BNRDetailViewController.strings.”

Press Enter. (You might see some sort of warning where ibtool complains about GSCapabilities; you can ignore it.)

Open BNRDetailViewController.xib (Spanish) in Xcode. This XIB file is now localized to Spanish. To finish things off, resize the label and text field for the serial number, as shown in Figure 25.11.

Figure 25.11 Spanish BNRDetailViewController.xib

Spanish BNRDetailViewController.xib