I am not an Athenian or a Greek, I am a citizen of the world.
Socrates
Localizing your app is the process of taking the text in your application, both within the code and in the UI, and enabling it to be represented by the language the customer has chosen. It’s enabling your app to support this setting automatically. This chapter walks you through the steps, the process, and the requirements to make your application not an isolationist but a citizen of the world.
The chapter is broken into three general sections:
1. The first section gives the basic “10,000-foot level” description of the localization process with iOS applications. We’ll also cover some specifics of iOS localization, including base localization.
2. The second section takes our sample app from Chapter 4—I18nExerciser—and walks through with more detail in the process of localizing it for French. We’ll also walk through localizing images as well as the name of the app icon.
3. The last section covers other localization tools available, working with translation services, and iOS support for Unicode Common Locale Data Repository (CLDR).
This section walks through the localization process starting with what localization is, what gets localized, and how the process works. This will first be generalized, and then I’ll get into the specifics.
When we talk about localization, we’re talking about the translation of strings. But even more than that, we’re talking about context. This needs to be not just the “word for word” translation, but the context as well. Locales have grammar and linguistic rules that we need to understand and respect. For now, we want to set up our application to be able to display different strings when we change our system language. And we want this change to be automatic. We do not want to work with hard-coded strings and a series of if
statements that look for our locale’s current language. Thankfully, Apple has done the heavy lifting for us and given us a sweet suite of tools. Some of the tools are step-intensive; hopefully I can make those steps clear.
Of course, we’re talking about strings. To put a finer point on it, this includes hard-coded strings within our code and strings within our UI. So that includes any NSStrings
we might have in our code (e.g., @"Hello world"
) and any text we’ve included in our UI, XIBs, or Storyboards, be that text in labels, buttons, table views, and so on. These strings needs to be parsed out of their source files and stored in a .strings
file.
For our strings to get parsed out of the code source files, there are two requirements. The first is that our strings be wrapped in a macro, specifically the NSLocalizedString()
macro. This identifies the strings so the tools know what to parse out. The tool in question is a command-line utility called genstrings
. The genstrings
command parses the specified files, typically all the implementation or “.m
” files, and creates a text file, Localizable.strings
, in the project folder. Because this is a text file, this is something you can create manually and populate with the key-value pairs that are discussed in the section “The genstrings
Command.”
For strings in your XIBs and Storyboards, this is much friendlier and can be done through Interface builder, specifically the Localization section in the File Inspector tab.
Base Localization
Base localization allows you to work with XIBs and Storyboards in your native language. Its most powerful feature is that it generates .strings
files (Main.strings
) for your UI-facing text whenever you add a localization language to your project. Before Xcode 4.5, separate XIB files were required for each language you localized for. If your app supported ten languages, ten separate XIBs were required to support the UI for those languages. This was a lot of overhead and version control headaches dealing with changes to XIB files. If a UI tweak was needed, say a button moved eight pixels, that change would need to be made in all ten XIB files.
Now support comes through the .strings
file. Whenever you add a new language to localize for, you simply copy the Main.strings
file from the Base.lproj
folder to your newly added language resource folder, such as jp.lproj
.
The tools parse through either the code or the UI and generate .strings
files. These .strings
files can be thought of as dictionaries of sorts because they contain “keys” or “tokens” with values associated with them. A quick example from a .strings
file looks like this:
"First name" = "First name"
Here, the token “First name” has a value of "First name",
and running under an English language, every instance of “First name” would be replaced with "First name"
automatically. Each language you have localized for will have a .strings
file associated with it.
The following steps are just the general, shorthand steps to localize your app. As already mentioned, we’ll go into these with detailed steps and screen captures.
1. As you are writing your code, wrap any hard-coded strings presented to the user in the NSLocalizedString()
macro. You can go back and apply this macro to existing strings in your code as well.
2. Add the language you want to localize via the Project Info tab under the Localization section.
Note
When you add a language to localize for, a resource folder for that language is created in your project. Xcode’s naming convention for this folder is language_identifier.lproj
. So adding support for Spanish localization would result in a new folder being added with the name es.lproj
.
3. For strings in your UI elements, select the XIBs and Storyboards in Xcode’s Project Explorer and then enable any languages listed in the Localization section under the File Inspector tab. This will result in the localized Main.strings
files listed in the Project Explorer.
4. Launch the Terminal app and switch to the project’s folder.
5. Run the genstrings
command as follows: genstrings *.m
. This will create the Localizable.strings
file.
6. Switch to Finder and bring up a window showing the project’s folder.
7. Copy the Localizable.strings
file to the localized project folders (*.lproj
).
8. Move the Localizable.strings
file to the Base project folder (Base.lproj
).
9. Copy each of these Localizable.strings
files to Xcode’s Project Explorer. This creates localized entries for the .strings
files that are labeled with the language name, such as Localizable.strings (English)
.
10. Update the values in the .strings
files—both Localizable.strings
and Main.strings
—to the appropriate translations.
Simple? Ready to localize on your own? No, probably not. Let’s walk through localizing the “Quotes and Scripts” view controller in our I18nExerciser application from the preceding chapter.
We’ll take our project from the preceding chapter, the I18nExerciser, and focus on localizing one view controller, Quotes and Scripts. The detailed steps we’ll go through can be applied to any other code, any other view controller, and any other project.
We need to set up our project to support different languages, which is done by adding a resource folder for that language in our project. Modify the project by following these steps:
1. In Xcode, select the project and then the Info tab.
2. In the Localization section, click the plus button.
3. From the menu, choose French, as shown in Figure 5.1.
4. On the next screen (see Figure 5.2), choose the files to localize and click Finish.
We have options to localize strings in our code, and with Xcode the approach is to wrap those strings with the NSLocalizedString()
macro. Macros in general wrap up a larger snippet of code into a manageable size.
For its arguments key
and comment
, the key
argument is equivalent to a key entry with a dictionary, and the comment
is just that, a comment to add clarity and context to the translator. comment
is not a required argument, but including a meaningful comment is an excellent practice to follow. By default, our generated .strings
files that contain the key, the value, and the comment we’ve supplied and the name of the key will be the same as the value we have assigned. If, for our key
argument, we’ve supplied the string Tap to continue
, by default, the value for this key is the same, Tap to continue
.
Recall from the preceding chapter that we wrapped our strings in the QuotesAndScriptViewController.m
view controller implementation file in the NSLocalizedString()
macro. Here are the specific lines we changed:
NSString *quotedString = NSLocalizedString(@"Remember a panda eats shoots and leaves.", @"String used to demo quotation marks");
self.scriptCode.text = NSLocalizedString(@"No Script code for current locale.", @"Error message if there is no script code");
self.variantCode.text = NSLocalizedString(@"No Variant code for current locale.", @"Error message if there is no variant code");
Now we need to create our Localizable.strings
files. These files contain the keys, the localized values for those keys, and the comments for translators. At our app runtime, the NSLocalizedString()
macro will read in the values from the appropriate language’s .strings
file.
genstrings
is the command-line utility that parses specified files, typically all the *.m
files, and creates the Localizable.strings
files based on the NSLocalizedString()
macro entries.
Follow these steps:
1. Launch the Terminal app.
2. Make your project folder the current working directory.
3. We’re focused on the Quotes and Scripts view controller, so run the following command:
genstrings QuotesAndScriptViewController.m
You can find more information about the genstrings
command by looking at its man page in Terminal: man genstrings
.
Note
Here are some other samples for genstrings
:
Generate strings from *.m
files located in the current folder and place the resulting Localizable.strings
file in the en.lproj
subfolder:
genstrings -o en.lproj *.m
Generate strings from *.m
files located in the current folder plus its subfolders, and place the resulting Localizable.strings
file in the en.lproj
subfolder:
find . -name *.m | xargs genstrings -o en.lproj
Generate strings from *.m
files in the current folder plus its subfolders but exclude otherFolder
and place the resulting Localizable.strings
file in the en.lproj
subfolder:
find . -name *.m -not -path "./otherFolder/*" -print0 | xargs -0 genstrings -o
en.lproj -1
Now we switch to Finder and open a window for the project folder. You should see our generated Localizable.strings
file. We need to make this available to your applied localizations by copying it to our *.lproj
resource folders. Copy the file to the en.lproj
and fr.lpoj
folders.
Finally, move the Localizable.strings
file to the Base.lproj
folder.
Lastly, we need to bring these files into our Xcode project. Drag all three Localizable.strings
files to the Xcode Project Explorer. At the Choose Options for Adding sheet make sure to enable Copy Items and click Finish. Note that the localized Localizable.strings
files are listed in the Explorer, as shown in Figure 5.3.
Click the Localizable.strings (English)
entry, which is also listed here:
/* Error message if there is no script code */
"No Script code for current locale." = "No Script code for current locale.";
/* Error message if there is no variant code */
"No Variant code for current locale." = "No Variant code for current locale.";
/* String used to demo quotation marks */
"Remember a panda eats shoots and leaves." = "Remember a panda eats shoots and
leaves.";
Note how the argument for our comment is placed within comment characters, and by default the name of the key is the same as its value. At this point we can change the value to whatever is appropriate. If we change the name of the key, that change would need to occur within the code as well. Let’s add our localized text for the French .strings
file by clicking on its listing in the Xcode Project Explorer and add the following changes:
/* Error message if there is no script code */
"No Script code for current locale." = "Pas de code disponible.";
/* Error message if there is no variant code */
"No Variant code for current locale." = "Pas de code disponible.";
/* String used to demo quotation marks */
"Remember a panda eats shoots and leaves." = "Rappelez vous, un Panda mange des pousses
et des feuilles.";
That takes care of localizing the strings within our code for the Quotes and Scripts view controller. Now let’s take care of the strings within the UI. Recall with our I18nExerciser project we have both Storyboards and XIBs. I intentionally did this so we could see how to handle either one for localization.
Recall that because we are using base localization, we only need to work with .strings
files; otherwise, we would have to have separate XIBs for each language. Now we have only one XIB and separate .strings
files for each language.
Note
Before base localization, a separate XIB file would have to be maintained for each language you wanted to support. Each language’s .lproj
folder would have its own associated XIB file. Much like the genstrings
command-line tool, .strings
files could be created from the XIBs and XIBs populated via the ibtool
command. The ibtool
command also takes your translated .strings
file and creates new XIBs populated with those supplied strings.
Let’s say we have a view controller named SampleViewController that we want to be able to translate into German. First you run ibtool
to pull the strings from your development language XIB (say, English for argument’s sake). From Terminal in your project’s folder you run this:
ibtool --export-strings-file ~/Desktop/SampleViewController.strings
en.lproj/SampleViewController.xib
This creates a SampleViewController.strings
file on the desktop. You’ll take this file and have it translated into German. Now you want to run ibtool
again to use the translated .strings
file and create a German localized XIB file. If you don’t already have one, you will need to create a de.lproj
folder in your project folder. Run the following command in Terminal:
ibtool --import-strings-file ~/Desktop/SampleViewController.strings –write
de.lproj/SampleViewController.xib en.lproj/SampleViewController.xib
The command reads, “Create SampleViewController.xib
in the de.lproj
folder based on the SampleViewController.xib
in the en.lproj
folder, and replace all its English strings with my translated German strings.”
“Hey Siri...” Not today. Maybe someday!
Notice the change to the Main.storyboard
listing Xcode’s Project Explorer. Figure 5.4 shows that the file now has a disclosure triangle associated with it, as well as a listing for Base and French localization. This happened automatically when we added French localization to our project. But where is English, our “base” development language?
We just need to enable English localization with our Storyboard. Select Main.storyboard
in the Explorer and open the Utilities pane. Notice in the Localization section from the File Inspector tab that English is not enabled (see Figure 5.5). Enable it now.
To localize our QuotesAndScriptViewController.xib
, first select it. From the Utilities pane, File Inspector tab, click Localized. You’ll see what’s shown in Figure 5.6. This prompt displays to ask what folder the XIB file should be moved to. We want to go with the default and place it in the Base.lproj
folder. Click the Localize button.
This creates a QuotesAndScriptViewController.strings
file to hold our localization strings specific to this view controller. So you need to do this for every XIB, and every XIB gets a .strings
file. You might need to enable the language listings in the Localization section (see Figure 5.7).
Enable English and French. Then the XIB gets a disclosure triangle (see Figure 5.8).
Now let’s change the language in the Simulator via the Settings app. Choose General, then International. Change the Language to Français. Figure 5.9 shows the French language being applied.
Notice how some of our apps now have localized app names (see Figure 5.10): Réglages, Calendrier, Kiosque. We’ll be doing that ourselves later in this chapter.
Fire up the I18nExerciser app, and let’s look at our Quotes and Scripts view controller by tapping More and then Quotes and Scripts (see Figure 5.11).
But all the strings are still in English? We added the French localization and added it correctly. We’ve correctly changed our System’s language setting to French. Why are we not seeing French strings? I’ll tell you the next step. We do have a French localization file for our French strings, namely, Localizable.strings (French)
, as displayed in the Xcode Project Explorer, which contains all of our necessary keys and values. Remember, we wrapped our strings from the implementation file in the NSLocalizedString()
macro.
The NSLocalizedString()
macro uses the localizedStringForKey:value:table:
method (from the NSBundle
class) to look up the string specified by the key, and do this in the current System language. It passes nil
for the table name so as to use the default .strings
filename, Localizable.strings
.
Note
The NSLocalizedString
macro definition is as follows:
#define NSLocalizedString(key, comment) [[NSBundle mainBundle]
localizedStringForKey:(key) value:@"" table:nil]
One important thing to note is that the text supplied to the comment argument is not used in the code, and therefore there is no disadvantage to using long, descriptive comments.
At runtime, the NSLocalizedString
macro runs the following NSBundle
method:
method[[NSBundle mainBundle] localizedStringForKey:(key) value:@"" table:nil]
This method determines the System preferred language and uses the corresponding Localizable.strings
file in the app bundle. If the user’s preferred language is set to Polish, then NSLocalizedString
will look to the file pl.lproj/Localizable.strings
.
By default, it is going to use the language that we were developing in, our Base language. Go ahead and click on the .strings
file in the Xcode Project Explorer, and you’ll see just that.
/* Class = "IBUILabel"; text = "Script and Variant"; ObjectID = "4Lu-iM-OR0"; */
"4Lu-iM-OR0.text" = "Script and Variant";
/* Class = "IBUILabel"; text = "variantCode"; ObjectID = "8NW-X6-HOo"; */
"8NW-X6-HOo.text" = "variantCode";
/* Class = "IBUILabel"; text = "Quoted string to demo quotation marks."; ObjectID =
"F9g-YI-MRH"; */
"F9g-YI-MRH.text" = "Quoted string to demo quotation marks.";
/* Class = "IBUILabel"; text = "Variant: "; ObjectID = "SsI-gF-TRr"; */
"SsI-gF-TRr.text" = "Variant: ";
/* Class = "IBUILabel"; text = "scriptCode"; ObjectID = "oII-iT-30m"; */
"oII-iT-30m.text" = "scriptCode";
/* Class = "IBUILabel"; text = "multiLineLabel"; ObjectID = "pPk-UG-tme"; */
"pPk-UG-tme.text" = "multiLineLabel";
/* Class = "IBUILabel"; text = "Quotes & Scripts"; ObjectID = "pfR-sY-Bk2"; */
"pfR-sY-Bk2.text" = "Quotes & Scripts";
/* Class = "IBUILabel"; text = "Script:"; ObjectID = "stt-Fy-X4e"; */
"stt-Fy-X4e.text" = "Script:";
Before we update our key values to French translations, we should note some things that are different between this XIB .strings
file (QuotesAndScriptViewController.strings (French)
) and the Localizable.strings (French)
file. Both .strings
files have comments for the localizer, keys, and values for the keys. The “key” difference is that you cannot customize the names of the keys for XIB .strings
files. They are referencing the UI elements contained by the XIB by the object ID for that element. You can verify the IDs by Interface Builder’s Document section on the Identity Inspector tab. Figure 5.12 shows this for the “Script:” label. This is also the case for our Storyboard Main.strings
files.
Update the key values to the following for the QuotesAndScriptViewController.strings (French)
file. Note that I’ve also updated the default comments to more meaningful information.
/* Static text for section title */
"4Lu-iM-OR0.text" = "Script et Variante";
/* Placeholder label. Value is set via code DO NOT LOCALIZE */
"8NW-X6-HOo.text" = "variantCode";
/* Sample string of text to demo quotation marks */
"F9g-YI-MRH.text" = "Une phrase avec guillemets.";
/* Static label for locale variants */
"SsI-gF-TRr.text" = "Variante: ";
/* Placeholder label. Value is set via code DO NOT LOCALIZE */
"oII-iT-30m.text" = "scriptCode";
/* Placeholder label. Value is set via code DO NOT LOCALIZE */
"pPk-UG-tme.text" = "multiLineLabel";
/* Static text for the title of the view controller */
"pfR-sY-Bk2.text" = "Quotations et Scripts";
/* Static label for locale script */
"stt-Fy-X4e.text" = "d'ordinateur:";
Now let’s do the same for the Storyboard. Select the Main.strings (French)
listing and make the following changes in Xcode:
/* Static label for "Calendar:" */
"3gc-Lt-U2Y.text" = "Calendrier:";
/* Static label for "Language:" */
"6Ud-Gs-Obj.text" = "Langue:";
/* Static label for "Current system settings" */
"BN8-uM-1rU.text" = "Parametres courants du systeme";
/* Static label for the name of the app */
"Fvv-yF-k6c.text" = "Votre I18 outil";
/* Label for Nav Bar Button for returning to the Home Screen */
"Lzm-bV-lyZ.title" = "Site";
/* Static label for "Date:"; */
"MBe-Fd-lbK.text" = "Date:";
/* Placeholder label. Value is set via code DO NOT LOCALIZE */
"Srg-3T-q1Y.text" = "currCal";
/* Placeholder label. Value is set via code DO NOT LOCALIZE */
"fiJ-c1-ZID.text" = "currLoc";
/* Placeholder label. Value is set via code DO NOT LOCALIZE */
"gqN-co-QBb.text" = "currLang";
/* Class = "IBUINavigationItem"; title = "Navigation"; ObjectID = "pda-KE-rL6"; */
"pda-KE-rL6.title" = "Navigation";
/* Placeholder label. Value is set via code DO NOT LOCALIZE */
"r9W-cm-1d7.text" = "currDate";
/* Static label for "Locale:" */
"sxj-dL-W14.text" = "Locale:";
/* Label for the "More" button which displays list of View Controllers */
"vij-EV-xfg.normalTitle" = "plus >";
Note
To ensure that your localization changes are reflected in the locale’s XIB, first delete your app from the Simulator or device. This way, Xcode will respect your resource file changes. If you find that your localization changes are still not showing up in your app, do the following:
1. From your device or the Simulator, delete your app.
2. Open your project.
3. Choose Clean from the Project menu.
4. While holding the Option key, click Project and then Clean Build Folder.
This forces a series of “re’s” on your app—recompiled, rebundled, reinstalled.
Let’s build and run the app and bring up the Quotes and Scripts view controller (see Figure 5.13).
Voilà. Très bien!
I’m sure we’re both noticing now we have labels with “too much” text and the result is being truncated. We’ll want to adjust our layout to handle this automatically through Auto Layout and constraints. Please be patient! We’ll cover that in Chapter 6, “Adjusting the UI.”
To keep things interesting, here are the translations for the Localizable.strings (Japanese)
file:
/* Static text for section title */
"4Lu-iM-OR0.text" =
/* Placeholder label. Value is set via code DO NOT LOCALIZE */
"8NW-X6-HOo.text" = "variantCode";
/* Sample string of text to demo quotation marks */
"F9g-YI-MRH.text" =
/* Static label for locale variants */
"SsI-gF-TRr.text" =
/* Placeholder label. Value is set via code DO NOT LOCALIZE */
"oII-iT-30m.text" = "scriptCode";
/* Placeholder label. Value is set via code DO NOT LOCALIZE */
"pPk-UG-tme.text" = "multiLineLabel";
/* Static text for the title of the view controller */
"pfR-sY-Bk2.text" =
/* Static label for locale script */
"stt-Fy-X4e.text" =
Figure 5.14 shows our app now running with the System language setting set to Japanese.
Our app can be set up such that it handles localized images as well. This is internationalization at its best, giving the customers the feeling that this is their app, fully embracing their locale. So they have an app that not only supports their language and the formats of their locale, but any specific pictures or graphics as well. This could include their country’s flag or a region-specific image. We’ll modify our I18nExerciser project to replace the “globe” image on the home screen to a country map for the language we choose. The process is straightforward because the image is just a resource that’s read in. We need to ensure that the correct resource is in the correct .lproj
folder. These are the rules to follow:
Images for localization all have the same filename. This includes the file extension, so therefore you want to work with the same file format.
The appropriate localized files copied to their corresponding .lproj
folder.
Note
Right now, I have a perfect refactoring opportunity. The original graphic file I worked with is named globe.jpg
. Although this was appropriate at the time, now I want to localize this image. Localizing images requires that all images have the same filename. If I want to work with maps of countries, for example, having a map of Denmark named globe.jpg
makes no sense. At this point, I’m going to go to the Finder and rename my graphic to the more generic name HomeScreen.jpg
. I’ll make the same change in the code as well. Be sure to remove any references to globe.jpg
from within Xcode.
Follow these steps:
1. Copy or save a .jpg
graphics file to the I18nExerciser/I18nExerciser/fr.lproj
folder. Name it HomeScreen.jpg
. I’m choosing a Creative Commons license graphic off of Wikipedia for a map of France.
2. Do the same for the English.lproj
folder.
3. Move the original, now renamed HomeScreen.jpg
to the Base.lproj
folder.
4. Open the I18nExerciser project.
5. Switch back to Finder and drag all three images into the Xcode Project Explorer.
6. Set the Simulator language setting to French and launch the app.
Figure 5.15 shows the sample app, correctly displaying the graphic I chose for a map of France.
Display a Country Based on Language?
I’m making a callout here to avoid any confusion with displaying a country as a localized image. When we localize an image, it needs to be stored as a resource in the language-specific lproj
folder. When the System is set to a language we’ve localized our app to support, it will apply all the resources associated with that language. Even though with our sample project we are displaying a country, which is in actuality a “region” or “locale,” this image will change only when the System’s language is changed, not when the System’s region setting is changed. Chapter 6’s “UI Localization” section goes into detail about what you must consider when localizing images. For our sample app, I decided to display the image of a country where French is the official language.
A finishing touch you can give your application is to localize the app name that displays below its icon on the home screen. If your application’s name is unique, for example Apple’s “Safari,” you won’t want to translate it. However, this detail shows design savvy and will get your app noticed in the app store quicker than a nontranslated screen. Also, for any trademarked app name, you will want to leave in that trademarked language.
App names displayed on the home screen are limited by device and iOS version. The exact number of characters available varies because of this, and therefore you should set your expectations to no more than 12 characters. To localize your name requires changing the app’s Info.plist
file and InfoPlist.strings
files. The following steps assume you have an existing application that has more than one localization language setting:
1. Modify the application’s Info.plist
file. This is typically in the Supporting Files group in Xcode’s Project Explorer. (Its actual name is YOUR_PROJECT’S_NAME-Info.plist
.) Click the file listing to modify it.
2. Click the plus symbol next to Information Property List. From the drop-down shown in Figure 5.16, select Application Has Localized Display Name. Set the value to YES.
3. If the key named “Bundle display name” is present, delete it by clicking the minus sign.
4. Navigate to your localized InfoPlist.strings
files. Add the following two lines to the file, in which "
LOCALIZED_NAME"
would be replaced with your localized names:
CFBundleDisplayName = "LOCALIZED_NAME";
CFBundleName = "LOCALIZED_NAME";
5. Do this for each InfoPlist.strings
file.
Other macros are available to wrap code strings in. The list of these macros is shown here, followed by descriptions of their functionality:
NSLocalizedStringFromTable
NSLocalizedStringFromTableInBundle
NSLocalizedStringWithDefaultValue
This macro helps organize your project:
NSString *NSLocalizedStringFromTable(NSString *key, NSString *tableName, NSString
*comment)
This is nothing different from the NSLocalizedString
macro other than it includes a third argument to specify a custom table. By default, the NSLocalizedString
macro writes out to the Localizable.strings
file. NSLocalizedStringFromTable
allows you to specify a table to write out to, such as AlanParsonsProject.strings
. This allows you to break down the localization files based on individual projects within your app, for example, a table for any “contacts” in your project, another for “product details,” and so on. The syntax to write out to the suggested table would look like the following:
NSLocalizedStringFromTable(@"MainProject",@"AlanParsonsProject", @"Heading label
signifying the main project file")
The function signature for this macro is as follows:
NSString *NSLocalizedStringFromTableInBundle(NSString *key, NSString *tableName,
NSBundle *bundle, NSString *comment)
This macro is used to generate .strings
files named "tableName".strings
, which will be located in a custom bundle, like a framework bundle. Because your framework is separate from your main project, it will require its own localization resources. Also, as the .strings
files are in the framework’s bundle, that bundle will need to specifically be referenced as opposed to the main bundle.
The function signature for this macro is as follows:
NSString * NSLocalizedStringWithDefaultValue(NSString *key, NSString *tableName,
NSBundle *bundle, NSString *value, NSString *comment)
As with the other macros, genstrings
works well with this one. It’s a pretty self-explanatory function because whatever string is specified for value
will be used as the default value for the keys for the .strings
table. This can be useful if you set the default value to nonreadable text—something that isn’t an actual word. So instead of having a value of Tap Here
, which is completely readable and easy to overlook, if you have a default value of NOTLOC
you can easily identify strings that are missing translation.
When we’ve talked about wrapping our strings in the NSLocalizedString()
macro, we’ve been using the literal string as the first argument. This results in a key having a name that is the same as its value in the Base language. For example, if I have a literal string of No files found
in my code, the Localizable.strings
file would have this entry:
/* Message to display when no files are found */
"No files found." = "No files found."
An alternative approach is when you are implementing your NSLocalizedString()
macro, create a generic key for the first argument rather than the literal string. This makes for more readable Localizable.strings
files and quicker debugging to verifying that strings were localized. Looking at our preceding example, a better implementation would be to have the key named msgNoFile
:
NSLocalizedString(@"msgNoFile", @"Message to display when no files are found")
After we have run genstrings
, our .strings
file would have the following entry:
/* Message to display when no files are found */
"msgNoFile" = "msgNoFile"
This provides us a powerful debugging tool. Now as we run our app under the languages we’ve localized for, whenever we have a string that has not been localized, it will display, for our example, as msgNoFile
instead of the translated version of No files found
. The unfortunate tendency when verifying localized strings is recognizing strings that are in your Base language. It is easy to acknowledge that string, see it as correct, and not realize it’s not the correct string for the given language. That’s where using NSLocalizedString()
with a generic token term is most useful. In your UI, the generic token is displayed, which quickly gets recognized as something amiss. Where this approach becomes labor-intensive is with the Base language—you need to manually add the values for the Base language strings because you are not wrapping them in the NSLocalizedString()
macro. Look at resources, consider your costs, and then determine whether you can add this extra step to your base localization files.
Here are some solid GitHub projects related to genstrings
that enhance the genstrings
process. The most convenient option these projects have is searching the current folder and subfolders for implementation files to parse in creating the Localizable.strings
files.
Unlike the genstrings
utility, this project (https://github.com/iv-mexx/update_localization) will not overwrite any existing files but will append new items. This is valuable when generating your .strings
file that have been translated. When a new text element is added to your project, running this tool will append the new entries to the existing .string
files as opposed to replacing the entire file.
This project (https://github.com/dulaccc/pygenstrings) runs genstrings
on all *.m
files in your project, merges new results into the existing Localizable.strings
files, and reports whether everything ran correctly. The advantage this tool has is that it will search for *.m
files starting from your project’s root folder in all subfolders.
Doing your own translation of strings is possible if you plan ahead and try to limit the amount of text in your app. If you’re finding that your app needs more than a single word, maybe even a paragraph to initially explain functionality, you would be wise to invest in a translation service to ensure your description uses the correct terms and grammar for the given locale. The following sections discuss translation pitfalls and give details about translation services.
Mad Libs is a game in which a phrase is given with words missing that one needs to fill in. These missing words are represented by blank lines with a clue of what part of speech to fill in: noun, adverb, verb, and such. The fun of the game comes from the nonsensical replacement: “The pig drove the polka dot tree while sleeping.” A noun, an adjective, another noun, and a verb were added correctly, but contextually, the sentence makes no sense. Imagine how a reader will respond if you word-for-word translate a phrase without context. Do not simply translate words. The sentence is king.
When you’re working with iOS applications with such limited screen real estate, the more you can avoid long strings of text the better. Try to limit your UI to single words as much as possible: “Next,” “Sort,” and the like. That way context becomes easier to grasp. You do want to make sure not to be clever with what you plan on localizing. Avoid thinking you can translate a sentence by breaking it up—“noun” followed by a “verb” followed by another “noun”—and then having those individually translated. This will only lose context, verbs will not be conjugated properly, and grammatical constructions won’t exist or at the very least will be awkward.
It is tempting to use a service like Google Translate to localize the strings in your app. It’s free and only a reliable Internet connection away. What these services lack is the context that even two words can give.
When your project is large enough that you’re working with a number of languages and a large amount of text to translate, you will want a reputable firm to translate for you. Cost will vary, of course, but figure a cost of $0.20 (twenty cents U.S.) per word, per language. Some services have a minimum charge of $100.
You will submit your localized .strings
files, or, depending on the firm, XML or text files. Some firms will parse your file and determine the number of words that will be translated and give you an upfront cost. Some firms have on-demand sites, meaning you can submit your .strings
files at any time of the day. Do note what their site specifies for turnaround time.
Look for firms that have a long track record as opposed to a flashy Web site. Also be sure that the company works with native speakers from the regions and locales you want to translate to. These people are the most familiar with and will know the correct and current colloquial phrases.
The basic steps when working with a translation service are shown here:
1. Localize your app.
2. Review your .strings
files, especially to ensure that your comments make sense and will be understood by the localizer.
3. Sign in to your account with the translation service.
4. Upload your .strings
files.
5. Verify the costs.
6. Submit your translation job to the service.
Apple has a list of more than 20 available third-party vendors at the bottom of this site: https://developer.apple.com/internationalization/. Take advantage of any vendors that will also allow you to upload screenshots so as to have a complete context of how the feature works and how the customer should respond.
After the translation services have done their job and returned the .strings
files to you, take each translated .strings
file and copy it to the appropriate .lproj
folder. Delete any existing builds of your app, rebuild it, run it under the same language, and verify the strings.
When you’re working with nouns and adjectives, the adjective will be ordered first with English, but that’s not the general case for all languages. In French and Russian, for example, the adjective comes after the noun it is modifying or describing. We can still take this into consideration with our localization. How so, you ask? I can imagine your concern—would we need multiple configurations of .strings
files and would we need the tokens for combinations of nouns and adjectives? Something along the lines of "blueRectangle" = "blue rectangle"
for English strings, "blueRectangle" = "rectangle bleu"
for French strings? The answer to both of these questions is no, and our solution can be found in referencing the printf
command, of all places. When you’re formatting a string, it is possible to specify explicitly which argument is taken where by writing not just our usual %@
token but %
n$@
, in which n represents the arguments order.
Let’s look at an example. Part of your app’s functionality is order fulfillment for sporting goods. One of your customers orders a ball, and it’s red. We’ll want to localize the item, its color, and the order.
We can create our order string like this:
NSString *color = NSLocalizedString(@"colorKey", @"The color of the item");
NSString *item = NSLocalizedString(@"itemKey", @"The item the customer is
purchasing");
NSString *order = [NSString
localizedStringWithFormat:NSLocalizedString(@"orderKey", @"Formats the order of
the order"), color, item];
Within our English .strings
files we have this:
/* The color of the item */
"colorKey" = "red";
/* The item the customer is purchasing */
"itemKey" = "ball";
/* Formats the order of the order*/
"orderKey" = "%@ %@";
And then within our French .strings
files, notice we need to modify the "orderKey"
token to reflect the correct order for our noun and our adjective:
/* The color of the item */
"colorKey" = "rouge";
/* The item the customer is purchasing */
"itemKey" = "boule";
/* Formats the order of the order*/
"orderKey" = "%2$@ %1$@";
This has been a specialized example, but I hope it demonstrates the flexibility that’s available to us when localizing. And it’s certainly not limited to “adjectives” and “nouns” but whatever word order you need.
Languages have different rules for dealing with plurals. Some of these rules include how to represent text when you have different numbers of an item: zero, one, two, or other. The rules for plurals are included in the Unicode Common Locale Data Repository project. This project includes locale information for use in computer applications including but not limited to the following:
Translations for language names
Translations for territory and country names
Translations for currency names, including singular/plural modifications
Translations for time zones and sample cities (or similar) for time zones
The CLDR assists us in working with grammar rules and verb conjugation when dealing with the different number of objects. An example of verb conjugation with English would be a single example, “The horse runs across the field,” as opposed to a multiple or plural example, “The horses run across the field.” The iOS operating system has CLDR support built in to correctly handle these.
Zero, one, two, other—why are these called out? The quantity the text is referring to will affect the grammar and how the text is represented. For English, when we’re dealing with a single item, the text is represented as “horse,” for example. When that quantity changes, the string changes as well, to “horses.” The verb conjugation changes as well, from “The horse gallops across the field” to “The horses gallop across the field.” Apps that support CLDR categories can respond to differences in quantities and automatically reflect those changes. I’ll walk you through an example of setting up that support after explaining, in Table 5.1, how quantities within the CLDR are categorized.
Not all languages support every category. For example, English supports only “zero,” “one,” and “other,” meaning any quantity other than “zero” or “one” will display the text assigned to the “other” category. Other languages support many more categories, with Welsh supporting all five and Slovenian supporting three.
You can find more info at www.unicode.org/cldr/charts/latest/supplemental/language_plural_rules.html.
I’ve got an example that will look at an input value and then, depending on that value, return the correct plural form of the word “cactus.” In this example, an artist is painting a number of cacti on a mural. We need the following implemented:
Code using the NSString
class method localizedStringWithFormat
A Localizable.strings
file for the localized language
A Localizable.stringsdict
file for the localized language
The Localizable.stringsdict
dictionary we need is displayed in Listing 5.1. The same rules apply for the .stringsdict
files as with .strings
files. Each localized resource folder (*.lproj
) needs a copy of the file and that file referenced in the Xcode project. Unfortunately, there’s not an automatic way to generate the file. Here are some specifics to look at with this file:
The return value is from the NSStringLocalizedFormatKey
.
The spec type is NSStringPluralRuleType
.
The value to test against is our d
variable.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>cactusKey</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>The artist painted %#@num_of_cacti@ on the mural.</string>
<key>num_of_cacti</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>zero</key>
<string>no cacti</string>
<key>one</key>
<string>a cactus</string>
<key>other</key>
<string>%d cacti</string>
</dict>
</dict>
</dict>
</plist>
Figure 5.17 displays the XML code shown in Listing 5.1 opened as an ASCII Property List from within Xcode.
Listing 5.2 shows our sample code.
-(void)pluralTest{
NSInteger testInteger = [@"233" integerValue];
NSString *resultString = [NSString localizedStringWithFormat:NSLocalizedString(@"cactusKey", @"Number of
cacti an artist has painted"), (long)testInteger];
NSLog(@"%@", resultString);
}
The following is our Localizable.strings
file generated from running genstrings
from the Terminal app. Note that the value for the cactusKey
is what will be returned if the number/quantity of painted cacti is not “zero” or “one.”
/* Number of cacti an artist has painted */
"cactusKey" = "The artist painted %ld cactus on the mural.";
Finally, Table 5.2 displays the results from the different quantities.
It’s all correctly handled via CLDR support.
How a locale handles gender support—as in pronoun support—is defined with the CLDR as well. It follows a similar pattern to rules for plurals. Some examples of genders and the supported number for the locale are English with three (male, female, and neutral) and Polish with five (masculine personal [męski osobowy], masculine animate [męski nieosobowy żywotny], masculine inanimate [męski nieosobowy nieżywotny], feminine [żeński], and neuter [nijaki]). The CLDR assigns numbers for each gender. In the case of English, this would be male = 0, female = 1, neutral = 2. Supporting these gender pronouns in their languages follows a pattern similar to the plural rules.
The strings dictionary supports gender rules via NSStringGenderRuleType
. Unfortunately, the gender rule only returns null
from the strings dictionary as of iOS 7.1.2. For more detail about this, there is an existing Apple radar bug (rdar://16670931).
In this chapter we covered the basic and detailed steps for localizing your application. We walked through localizing a view controller from our sample app from the preceding chapter and localized both strings within the code and strings within the UI.
Hopefully, you saw that with a little planning, localizing your app doesn’t take that much more effort. Your app automatically responds to the international settings change.
In the next chapter, we’ll look at problems that can occur with layout and UI and the best ways to guard against them.