One of the greatest concerns of all developers is whether their applications will be accepted and used by a big audience. It’s a fact that the bigger that audience is, the more sales or downloads will be achieved, resulting to applications that sit at the top of the rankings and of course, applications that make a better profit. There are many factors that take part to the success of an application. Undoubtably, the most important one is the implemented features in it, but not only. Another factor that matters a lot, is whether an app is localized or not. A localized application will definitely meet a greater acceptance by the users who natively speak other languages, as it will be looking much more friendly and accessible than an application with the same features that supports just one localization, i.e. in English.
Deepening a bit more to what I just said, think that a French, a Greek, or a Chinese user won’t download easily an application that isn’t translated and localized to his language, unless there are great features in it. Many developers believe that by creating an app to a well known language that is spoken or learnt by the biggest part of the population, such as English, and by providing it worldwide, there is no need to do any localization, as the potentials users are too many, so it’s not a big deal to “lose” some of them because of the language. However that’s a wrong way to think. Making an application available to as many languages as possible, will result to a better user experience, while at the same time it will maximize the resonance by the users.
Localization is strongly related to another term, the internationalization. According to Apple:
Localization is the process of translating your app into multiple languages. But before you can localize your app, you internationalize it.
And:
Internationalization is the process of making your app able to adapt to different languages, regions, and cultures
The above two definitions are pretty explanatory, and they describe in the best possible way each term. Speaking of localization, it’s important to say that when doing it we don’t just translate any existing texts (strings) to another language. More steps are included than that, such as translating the subviews in the storyboard, and using the proper resources for each region. For example, an app could contain many copies of the same image, each one redesigned or redrawn and localized according to the supported languages and regions. The same should happen with sound files. Also, displaying numbers, currencies and dates in the proper format is part of the localization process. For instance, the decimal number 8.3 is represented just like that in US, but in Germany the same number should be represented as 8,3 (where the comma is used instead of the dot). Of course, a localized app can support right-to-left (RTL) languages as well.
The process of both internationalization and localization isn’t hard at all as you’ll see next thanks to the frameworks provided by the iOS SDK. When everything is done following the proper way, then the system automatically uses the correct version of each localized object and developers don’t need to hard code anything. The bottom line is that the internationalized applications appear to users as native apps, and nobody can really say if such an app was originally built to support only English, German, Hindi, or any other language.
Before closing this introduction, there’s one last thing I’d like to mention. It’s always a better idea to allow professional translators to translate any app texts for you than using any translating software, even if doing so costs in money. The final result worths it. This doesn’t mean that you shouldn’t try on your own of course, especially if you know people who can help you with the languages that you are about to localize to. It’s totally up to you how you’ll perform the required translations ultimately.
So, let’s get started and see what we’re going to do in this tutorial.
Demo App Overview
To keep things simple, my goal in this tutorial is to show you how to:
- Localize any strings existing in the code that are about to be displayed to the users.
- Localize the storyboard and the texts of the subviews existing to it.
- Localize image files.
- Use new Xcode 6 features for importing and exporting files for translation and performing localization preview in the Interface Builder.
In the sample application that we’ll implement, we’ll deal with movies. More specifically, we’ll display the details for five (preselected) movies. The source of our data is going to be the International Movie Database (IMDB). Particularly, we’ll display the following information for each one:
- Movie title
- Category
- Rating
- Release date
- Duration
- Director
- Stars
- Hyperlink for more info
All the above will be displayed into a tableview. Besides that tableview, in our interface will also exist a label to use as a title, an image view for displaying the flag of the region matching to the current locale, and a button for showing an action sheet to pick a movie. The next image illustrates a sample of all that. Notice that besides English which is the base language, we’ll add two more localizations in French and in German.
The Jumpstart Project
Because there is a lot of stuff to work with in this tutorial, I’ll save us some time by giving you a jumpstart project to start with. You can download this project here as a compressed file. You just have to unzip it to a directory of your choice in your computer, and then you’re ready to start working. In this project you’ll find the interface already created, and any necessary IBOutlet properties and IBAction methods already declared and connected to the proper subviews. So, we’re not going to create a project from the scratch, and we won’t do any work at all in the Interface Builder.
Besides that project, you can also download from this link a zipped file with some resources that we’ll need during the implementation of the sample app. In the package they are included two image files (2 flag images for the extra localizations we’ll add to the project), and a .plist file that contains all the sample data of the app. You can later either add the .plist file to the project, or copy-paste some XML code I’ll provide you directly into a new file.
So, before you keep reading, make sure that you’ve downloaded and unzipped both files. Take a look to the Interface Builder in the jumpstart project so you familiarize yourself with the interface, and when you are ready proceed to the next parts.
The Sample Data
As I already said earlier, the source of our demo data is going to be the International Movie Database (IMDB), because our goal is to show the details of some movies using various localizations. Normally, one should make such an app dynamic and flexible, so it supports searching capabilities (by name, category, etc), and other useful features. However, this is just a simple tutorial, so we’ll limit the amount of the movie information that we’ll be reviewing through the application.
There are various ways to store the movies data into the application and then display them. The one I chose though, and I believe that it’s a quite handy solution, is to store all the data into a .plist file. It’s really awesome and fast to load data from a .plist file to an array or a dictionary, as everything can be done simply using the existing iOS SDK frameworks.
Even though the .plist file with all the data already exists in the resources package that you downloaded, I would like to stick a bit here and explain how it is structured, and even more how you can create a new .plist file on your own and then fill it with data. Of course, there’s the easy path here too, where you can simple drag the MoviesData.plist file from the Finder in the Project Navigator in Xcode, and you’ll be fine. Also, if you are already familiar on how to add such a new file to the project, or if you want to dive into the action straight away, feel free to skip this part.
To add a new .plist file to the project simply follow the next steps:
- On Xcode, open the menu File > New > File….
- In the window that’s shown, in the iOS category select the Resource option (at the left).
- Select the Property List template and click Next.
- Name the file MoviesData and click on the Create button to let it be created and added to the project.
The root element of the .plist file is going to be an array. This array will contain five (5) objects, and each object is going to be a dictionary. Right next they are listed the details of the movies that we’ll display to the app, along with their data type:
- Movie Title: String
- Category: Array (a movie rarely belongs to just one category, so we’ll use an array for storing all the categories of a movie)
- Rating: Number (decimal)
- Release Date: Date
- Duration: Number (integer, just for the joy of the conversion as it could be a string)
- Director: String
- Stars: String
- Link: String
The next screenshot shows a sample of the .plist file:
Right next you can see the demo data of the file to XML format. To copy-paste it to the new file, simply go through the following steps:
- In the Project Navigator, Control – Click on the MoviesData.plist file.
- Open As > Source Code.
- Paste the following XML code.
<?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″>
<array>
<dict>
<key>Movie Title</key>
<string>Fury</string>
<key>Category</key>
<array>
<string>Action</string>
<string>Drama</string>
<string>War</string>
</array>
<key>Rating</key>
<real>8.300000000000001</real>
<key>Release Date</key>
<date>2014-10-16T21:00:00Z</date>
<key>Duration</key>
<integer>134</integer>
<key>Director</key>
<string>David Ayer</string>
<key>Stars</key>
<string>Brad Pitt, Shia LaBeouf, Logan Lerman</string>
<key>Link</key>
<string>http://www.imdb.com/title/tt2713180</string>
</dict>
<dict>
<key>Movie Title</key>
<string>Captain Phillips</string>
<key>Category</key>
<array>
<string>Biography</string>
<string>Drama</string>
<string>Thriller</string>
</array>
<key>Rating</key>
<real>7.9</real>
<key>Release Date</key>
<date>2013-10-10T21:00:00Z</date>
<key>Duration</key>
<integer>134</integer>
<key>Director</key>
<string>Paul Greengrass</string>
<key>Stars</key>
<string>Tom Hanks, Barkhad Abdi, Barkhad Abdirahman</string>
<key>Link</key>
<string>http://www.imdb.com/title/tt1535109</string>
</dict>
<dict>
<key>Movie Title</key>
<string>Iron Man 3</string>
<key>Category</key>
<array>
<string>Action</string>
<string>Adventure</string>
<string>Sci-Fi</string>
</array>
<key>Rating</key>
<real>7.3</real>
<key>Release Date</key>
<date>2013-05-02T21:00:00Z</date>
<key>Duration</key>
<integer>130</integer>
<key>Director</key>
<string>Shane Black</string>
<key>Stars</key>
<string>Robert Downey Jr., Guy Pearce, Gwyneth Paltrow</string>
<key>Link</key>
<string>http://www.imdb.com/title/tt1300854</string>
</dict>
<dict>
<key>Movie Title</key>
<string>Edge of Tomorrow</string>
<key>Category</key>
<array>
<string>Action</string>
<string>Sci-Fi</string>
</array>
<key>Rating</key>
<real>8.300000000000001</real>
<key>Release Date</key>
<date>2014-06-05T21:00:00Z</date>
<key>Duration</key>
<integer>113</integer>
<key>Director</key>
<string>Doug Liman</string>
<key>Stars</key>
<string>Tom Cruise, Emily Blunt, Bill Paxton</string>
<key>Link</key>
<string>http://www.imdb.com/title/tt1631867/</string>
</dict>
<dict>
<key>Movie Title</key>
<string>The Maze Runner</string>
<key>Category</key>
<array>
<string>Action</string>
<string>Mystery</string>
<string>Sci-Fi</string>
</array>
<key>Rating</key>
<real>7.4</real>
<key>Release Date</key>
<date>2014-09-18T21:00:00Z</date>
<key>Duration</key>
<integer>113</integer>
<key>Director</key>
<string>Wes Ball</string>
<key>Stars</key>
<string>Dylan O'Brien, Kaya Scodelario, Will Poulter</string>
<key>Link</key>
<string>http://www.imdb.com/title/tt1790864/</string>
</dict>
</array>
</plist>
Now, assuming that your project contains the sample data, we’re ready to carry on.
Preparing to Display Data
Before we perform any “heavy” localization tasks the project, it’s necessary the app to be fully working. Therefore, let’s begin by implementing the display of the movie details along with the rest of the visual elements in the base language (English), and then we’ll be able to proceed to localization. As we just saw, the data for every single movie in the .plist file is included into an array, so the first step we should do is to declare an array variable to which we’ll append the movie data from the file.
At first, make sure that you’ve selected the ViewController.swift file in the Project Navigator. Go at the top of the file where all the IBOutlet properties are already declared, and add the next line:
1 | var moviesData : NSArray? |
The moviesData array is declared as a NSArray, because we’ll use some of the features that this class contains. Also, declare the following variable as well:
1 | var selectedMovieIndex : Int? |
This will be used as the index of the currently shown movie in the moviesData array.
Now, we’ll implement a function in which we’ll perform one simple task: We’ll load the .plist file contents to the array. Here it is:
1 2 3 4 5 |
There’s nothing particularly difficult here. As you see, the way we load the file contents to the array is quite similar to the way of the Objective – C.
Time to do some initializations. Go to the viewDidLoad function, and in there add the next couple of rows:
1 2 3 4 5 6 7 8 9 | override func viewDidLoad ( ) { ... // Set the default selected movie index. selectedMovieIndex = 0 // Load the movies data from the .plist file. loadMoviesData ( ) } |
We make the selectedMovieIndex variable to indicate the first index of the moviesData array by default. Besides that, we make a call to the loadMoviesData function we just created, so the array gets filled with values once the view is loaded.
If you recall, one of the subviews existing in the storyboard (and maybe the most important one) is the tableview. This tableview contains eight rows, where each one displays a specific movie detail. Right next we are going to add the definitions of the minimum required tableview methods, so we can later display the loaded data. However, before doing so, it’s necessary to perform two preliminary steps:
- To conform the UITableViewDelegate and UITableViewDatasource protocols.
- To make our class the delegate and the datasource of the tableview.
To do the first one, simple go to the header line of the class and add the two protocols as shown below:
1 | class ViewController : UIViewController, UITableViewDelegate, UITableViewDataSource |
For the second, visit once again the viewDidLoad function, and this time copy-paste or write the following two lines:
1 2 3 4 5 6 7 | override func viewDidLoad ( ) { ... // Make self the delegate and datasource of the tableview. tblMovieInformation.delegate = self; tblMovieInformation.dataSource = self; } |
At this point, we’re ready to add the necessary tableview methods. Let’s get started by specifying the number of sections in the tableview:
1 2 3 | func numberOfSectionsInTableView (tableView : UITableView ) -> Int { return 1 } |
Next, we continue by returning the total number of rows in the one and only section, which, as we said, it’s 8:
1 2 3 | func tableView (tableView : UITableView, numberOfRowsInSection section : Int ) -> Int { return 8 } |
Next, let’s specify the height of each row:
1 2 3 | func tableView (tableView : UITableView, heightForRowAtIndexPath indexPath : NSIndexPath ) -> CGFloat { return 60.0 } |
The next one is quite important, but we’ll work with it in just a while:
1 2 3 | func tableView (tableView : UITableView, cellForRowAtIndexPath indexPath : NSIndexPath ) -> UITableViewCell { } |
Lastly, even though we’re not going to use the following method at the moment, we’ll simply define it for later usage:
1 2 3 |
The above is needed to handle the taps on the last row (on the link of each movie). When that row gets tapped, we’ll open the webpage pointed by the link URL to the Safari.
Don’t be alarmed if Xcode is showing an error right now. That’s simply because we haven’t implemented the tableView(tableView:, cellForRowAtIndexPath indexPath: ) method yet. But don’t worry, we’re about to fix that soon.
The NSLocalizedString Function
Before we proceed to the display of the movies data, it’s necessary first to talk a bit for a very important function named NSLocalizedString. This is strongly related to the internationalization process, as it’s the key for localizing text at any point of your applications. If you have been working with localization in the past, then you definitely know about it. If not, then you’ll merely learn how it works.
The NSLocalizedString function in it’s basic form accepts the following parameters:
1 | NSLocalizedString (key :, tableName :, bundle :, value :, comment : |
From the above parameters, we usually only care only for the first and last one. The key is a string value that we want to have translated versions for, while the comment is simply a description to the translator (a human translator), so he knows what exactly we want from him to translate. If you don’t use any assistance by other people for the translation of your app’s texts, then you can just pass the empty string as the last argument.
The other three arguments existing in the function are rarely used, they have default values, and they can be omitted. So, the above function can be simply used with two parameters only, and actually that’s how we’ll use it in this tutorial:
1 | NSLocalizedString (key :, comment : |
The function’s job is to look up in the localized string files (more about such files later) and return the translated version of the given key based on the current user’s locale. If no translation is found, then the matching term from the base language is returned. To make things as clear as possible, assume the following snippets:
1 2 | let goodMorning = "Good morning" println (goodMorning ) |
And:
1 2 | let goodMorning = NSLocalizedString ( "Good_Morning", "The good morning greeting" ) println (goodMorning ) |
In the first case, the “Good morning” message will appear to the output no matter what the user’s locale is. However, in the second case the message to the output will be translated according to the user’s locale (if any respective translation exists of course). So, the above will show on the output:
- Good morning in English
- Guten Morgen in German
- Καλημέρα in Greek
- Bonjour in French
… and so on.
We’re about to use the NSLocalizedString function several times during the implementation of the sample app, so I considered it as the best practice to present it first here. If there’s something above that isn’t clear to you, no worries! By the end of this tutorial you’ll have perfectly understood everything!
Number and Date Formatters
Numbers, dates and currencies consist of an important part in the localization process. Not all countries around the world use the same representation for any of them, and moreover there are cases where the representation in one country means something entirely different to another one. For example, the number 5.400 in US is the decimal number five point four (5.4), but in Europe the same number is the five thousand and four hundred (5400).
During the development of a localized application, it’s always important to take care of the proper number, date and number representation. No matter how each one of them is stored to the app, the way it is displayed must be in accordance to the user’s locale. Fortunately, no hard coding is required from the developers, as there are embedded mechanisms in the iOS SDK that allow to handle all the above in a quite straightforward and simple way. In this sample project we’ll use numbers and dates, therefore let’s see how we’ll handle them.
Let’s begin by numbers. There’s a special class provided by the iOS SDK, named NSNumberFormatter. This class allows to represent and convert numbers in various formats, and of course to automatically adjust any number to the user’s locale. For example, using a number formatter in the above example would result in having 5.4 in US locale, and 5,4 in some European locale. Also, and that’s really awesome, using a number formatter is possible to get string values properly formatted from numbers, and numbers in the proper format from strings. I won’t get into much details, as I leave that to you believing that you’ll take a look at the Apple’s documentation. However, I’m going to say that in our sample project we’ll need to perform number to string conversion, and more specifically we’ll convert a double number (the rating of a movie) to a string, so we can correctly display it later on. For that reason we will define a new, simple function:
1 2 3 4 5 | func getFormattedStringFromNumber (number : Double ) -> String { let numberFormatter = NSNumberFormatter ( ) numberFormatter.numberStyle = .DecimalStyle return numberFormatter.stringFromNumber (number ) ! } |
At first, we initialize a new NSNumberFormatter object. Next, we set its style to decimal, because the movie rating is a decimal number. Lastly, using the stringFromNumber method we convert the number given as a parameter to a string, which, at the same time, is returned from the function. Notice that the returned string value will be properly formatted according to the user’s locale when it will be displayed on-screen.
For managing dates, there’s the NSDateFormatter class. What I mentioned earlier applies here too. That means that we can convert dates to strings and back, using various representations, and always formatted in the most suitable way. Once again, it’s up to you to seek more info about this class. In the project we develop, we have just one date that must be displayed, and that is the release date of a movie. As we just did, we’ll add a new function here as well for doing the conversion to a string value. Let’s see it:
1 2 3 4 5 | func getFormattedStringFromDate (aDate : NSDate ) -> String { let dateFormatter = NSDateFormatter ( ) dateFormatter.dateStyle = .MediumStyle return dateFormatter.stringFromDate (aDate ) } |
Before doing any conversion it’s important to specify the style of the date. Here we set the medium style, which will return something like 28 Oct, 2014, or 28/10/2014, or 28.10.2014 (depending on the locale). However, the real work is done by the stringFromDate method, which returns a string value based on the given date and the preferences we set.
By having the above two functions already implemented, we can keep going in the data display part, where we’ll make some real use of them.
Displaying Data
There are eight prototype cells in the table view already added to the storyboard, and each one has been given with an identifier value. Using those identifier values we’ll dequeue the correct cell in the tableView(tableView:, cellForRowAtIndexPath indexPath: ) method, always based on the current index path’s row value. Moreover, because we have eight different cell cases, we’ll use a switch statement for specifying the details of each one.
However, before we start defining each case, it would be a good idea to assign to a local variable the dictionary matching to the currently selected movie (don’t forget that each object in the moviesData array is a dictionary object. Doing so will make the data access much easier as we’ll move forward. So, let’s begin the implementation with that:
1 2 3 4 5 6 7 | func tableView (tableView : UITableView, cellForRowAtIndexPath indexPath : NSIndexPath ) -> UITableViewCell { var cell : UITableViewCell ! let movieDataDictionary : NSDictionary = moviesData?.objectAtIndex (selectedMovieIndex ! ) as NSDictionary return cell } |
Notice that beyond the dictionary assignment to the movieDataDictionary constant, we also declare a table view cell variable that will be used next, which we also return from the function. With that, you’ll see that the error being shown by Xcode is vanishing at once.
Now, let’s create a switch statement.
1 2 3 4 5 6 7 8 9 10 11 | func tableView (tableView : UITableView, cellForRowAtIndexPath indexPath : NSIndexPath ) -> UITableViewCell { var cell : UITableViewCell ! let movieDataDictionary : NSDictionary = moviesData?.objectAtIndex (selectedMovieIndex ! ) as NSDictionary switch indexPath.row { } return cell } |
Our whole work will now take place in that statement. Because we have eight different cells to dequeue and set the proper values to the respective text labels, we’ll see everything one by one. Note that each code snippet that will be presented below must be added inside the body of the switch.
Let’s get started by the first cell where the title of each movie will be displayed. That’s easy enough, as the title is simply a string value. So, let’s dequeue the cell using the correct identifier value, and then let’s set the movie title to the text label:
1 2 3 4 5 6 | case 0 : // Dequeue the proper cell. cell = tableView.dequeueReusableCellWithIdentifier ( "idCellTitle", forIndexPath : indexPath ) as UITableViewCell // Set the cell's title label text. cell.textLabel.text = movieDataDictionary.objectForKey ( "Movie Title" ) as? String |
Notice that in Swift there’s no need to add a break command. The above is easy enough, so let’s move forward to the movie category (or categories). This is a bit more complex, because the categories that a movie belongs to are stored in the .plist file as an array. At first, we’ll assign that array to a local variable, and then using a loop we’ll build a string containing all categories separated by a space character. Let’s see the implementation and then there are more to talk about:
1 2 3 4 5 6 7 8 9 10 | case 1 : cell = tableView.dequeueReusableCellWithIdentifier ( "idCellCategory", forIndexPath : indexPath ) as UITableViewCell let categoriesArray = movieDataDictionary.objectForKey ( "Category" ) as [String ] var allCategories = String ( ) for aCategory in categoriesArray { allCategories += NSLocalizedString (aCategory, comment : "The category of the movie" ) + " " } cell.textLabel.text = allCategories |
Let’s take a more thorough look at the above. First, we get the array with the categories using this command:
1 | let categoriesArray = movieDataDictionary.objectForKey ( "Category" ) as [String ] |
Next, we initialize a string variable. The value of this string will be the result of the concatenation of all categories.
1 | var allCategories = String ( ) |
Finally, in the for loop we build step by step the custom string with the categories altogether. There’s a very important detail here: We don’t append directly each category value to the allCategories variable, but we use the NSLocalizedString method instead. We talked about it previously, and now we’re using it for first time. We want each single category of a movie to be displayed to the user translated to the correct language, so after we have read it from the array we provide it to the NSLocalizedString function with aim to get back the translated version of it. At the end of course, the concatenated categories are assigned to the text label of the dequeued cell.
Let’s keep going, and let’s dequeue the prototype cell regarding the movie rating. If you recall, the rating is a double number, therefore we must use the getFormattedStringFromNumber function we implemented to the previous part. By using that function, it’s easy to display the rating. As you’ll also see next, the formatted string representing the rating is suffixed with the “/10″ string.
1 2 3 4 | case 2 : cell = tableView.dequeueReusableCellWithIdentifier ( "idCellRating", forIndexPath : indexPath ) as UITableViewCell cell.textLabel.text = getFormattedStringFromNumber (movieDataDictionary.valueForKey ( "Rating" ) as Double ) + " / 10" |
The above cell will display something similar to: 8.5 / 10
Next, we have the release date of the movie. Once again, we’ll make use of the getFormattedStringFromDate function that we defined earlier. This case is quite similar to the previous one, so let’s see it:
1 2 3 4 | case 3 : cell = tableView.dequeueReusableCellWithIdentifier ( "idCellReleaseDate", forIndexPath : indexPath ) as UITableViewCell cell.textLabel.text = getFormattedStringFromDate (movieDataDictionary.objectForKey ( "Release Date" ) as NSDate ) |
If we wouldn’t have used the above two custom functions, then both the rating and the date values would appear as they are originally stored to the .plist file.
The next cell regards the duration of a movie. The duration could have been stored as a string value to the .plist file, however I intentionally saved it as an integer value, so we have the chance of doing a conversion from int to string:
1 2 3 4 5 | case 4 : cell = tableView.dequeueReusableCellWithIdentifier ( "idCellDuration", forIndexPath : indexPath ) as UITableViewCell let duration = movieDataDictionary.objectForKey ( "Duration" ) as Int cell.textLabel.text = String (duration ) + " " + NSLocalizedString ( "minutes", comment : "The minutes literal for the movie duration" ) |
The above case is also interesting because we use the NSLocalizedString function for displaying localized (translated) the word “minutes” once we have it appended to the movie duration value.
The following two cases are pretty easy, as they’re plain strings, and we have already seen such a case. First, we have the director of the movie:
1 2 3 4 | case 5 : cell = tableView.dequeueReusableCellWithIdentifier ( "idCellDirector", forIndexPath : indexPath ) as UITableViewCell cell.textLabel.text = movieDataDictionary.objectForKey ( "Director" ) as? String |
Then, the stars (actors):
1 2 3 4 | case 6 : cell = tableView.dequeueReusableCellWithIdentifier ( "idCellStars", forIndexPath : indexPath ) as UITableViewCell cell.textLabel.text = movieDataDictionary.objectForKey ( "Stars" ) as? String |
Lastly, there’s one more case regarding the hyperlink to the IMDB record of the shown movie. As this is the last one, we can use it as the default case:
1 2 3 4 | default : cell = tableView.dequeueReusableCellWithIdentifier ( "idCellLink", forIndexPath : indexPath ) as UITableViewCell cell.textLabel.text = movieDataDictionary.objectForKey ( "Link" ) as? String |
That’s it, finally! We’ve covered all the possible cases, and the movie details are now ready to be displayed.
However, we’re not yet completely ready. There’s one more subview that we must take care of; this is the image view, where the flag of the respective region should be shown.
Actually, we only need to add one line in the viewDidLoad method, as show below:
1 2 3 4 5 6 | override func viewDidLoad ( ) { ... // Set the flag image to the imageview. imgFlag.image = UIImage (named : "flag" ) } |
The image in the image view is set using the UIImage(named:) initializer method of the UIImage class. In it, we only have to specify the name of the image file we want to use. Later we’ll add two more flag images for localization reasons, and the method itself will load the proper file depending on the user region and the locale. No extra work for us, and that’s awesome, isn’t it?
Other App Features
We have already seen a few concepts regarding the internationalization and the localization by using the NSLocalizedString function and the number and date formatters. Before we see more on localization however, there are two last features of the app that should be implemented:
- The functionality of the button at the bottom of the view, which will be used to display an action sheet and let us choose another movie from a list.
- To launch Safari when the cell with the movie hyperlink is tapped.
Let’s get started with the first one. When tapping on the button, the showMoviesList IBAction method is being called. Therefore, in this one we’ll add the required code that will enable the feature that was just described above.
In iOS 8 a brand new class has been introduced, named UIAlertController. This class is used as a replacement to the older UIAlertView and UIActionSheet classes, and we’re going to use it for showing the action sheet. The logic we’ll apply is simple enough, but let’s take a look at it step by step:
- At first we’ll initialize a UIAlertController object, and we’ll set its preferred style to ActionSheet.
- Inside a loop we’ll extract all the movies titles one by one from the moviesData array.
- For each movie title we’ll create a new button for the action sheet.
- For all the buttons that we’ll be created in the step above we’ll perform the same action; we will set the proper value to the selectedMovieIndex variable and we will reload the table view, so the details of the proper movie to be displayed.
- We will also add a Close button to dismiss the action sheet without picking a movie. Its title will be localized.
- Finally, we’ll present the action sheet.
All the above steps are included to the code that follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | @IBAction func showMovieList (sender : AnyObject ) { let moviePicker = UIAlertController (title : "Movies List", message : "Pick a movie", preferredStyle : .ActionSheet ) for var i = 0; i<moviesData?.count; ++i { let movieDataDictionary : NSDictionary = moviesData?.objectAtIndex (i ) as NSDictionary let currentMovieIndex = i let movieTitle = movieDataDictionary.objectForKey ( "Movie Title" ) as String let normalAction = UIAlertAction (title : movieTitle, style : UIAlertActionStyle.Default, handler : { (action : UIAlertAction ! ) -> Void in self.selectedMovieIndex = currentMovieIndex; self.tblMovieInformation.reloadData ( ) } ) moviePicker.addAction (normalAction ) } let closeAction = UIAlertAction (title : NSLocalizedString ( "Close", comment : "The Close button title" ), style : UIAlertActionStyle.Cancel ) { (action : UIAlertAction ! ) -> Void in } moviePicker.addAction (closeAction ) self.presentViewController (moviePicker, animated : true, completion : nil ) } |
As you see, the buttons’ actions are defined in closures, which are quite similar to blocks in Objective – C. Also, notice that we use the NSLocalizedString function for setting the Close button’s title, so it will be properly translated in localizations other than English. Lastly, the alert controller is presented as any other usual view controller using the presentViewController method.
Further than implementing the functionality of the button, we also need to make the last row of the tableview to respond to our taps, and launch Safari for opening the IMDB website and viewing the selected movie’s information. This is quite simple, and it’s shown to the following code segment:
1 2 3 4 5 6 7 8 9 | func tableView (tableView : UITableView, didSelectRowAtIndexPath indexPath : NSIndexPath ) { if indexPath.row == 7 { let movieDataDictionary : NSDictionary = moviesData?.objectAtIndex (selectedMovieIndex ! ) as NSDictionary let link = movieDataDictionary.objectForKey ( "Link" ) as String UIApplication.sharedApplication ( ).openURL ( NSURL ( string : link ) ! ) } } |
As you can see, at first we check if the tapped row is the last one. In that case, we extract the link value from the movie’s dictionary, and then using the openURL method we launch Safari, by loading at the same time the URL specified by the link value.
At this point, our application is fully functional and it perfectly works, but only for the English localization. If you want to run it now and have a taste of it, then just do it. In the next parts we’ll focus on localizing all the needed assets, so our app becomes an international one.
Localizing the Storyboard
Currently, the base localization of the project is in English, but our goal is to also support French and German localizations as well. Now that our application is fully working, we can start working on internationalizing the application, and see how all that stuff actually work.
Note: Even though I chose the French and German as the extra localizations for the project, feel free to follow the procedure described here and in the next parts for adding any other localization that you desire.
To add a new localization, go to the Project Navigator pane and select the project’s group at the top. Click on the Project/Target pop-up menu and select the Project options as shown below:
For starters, let’s add the French localization. To do that, go to the menu Editor > Add Localization > French. Alternatively, click on the plus icon under the Localizations section, just as you see next, and then click on the French localization:
If a window for selecting files appears, then just leave checked the Main.storyboard file, and also make sure that the Localizable Strings is the selected option in the File Types column.
Under the Localizations section a new record with the French language will appear. Also, next to the Main.storyboard file at the Project Navigator you’ll notice that a disclosure indicator was added. If you click it, you’ll find a new file being there, named Main.strings (French). Click on that file, and you’ll see all the strings existing in the storyboard file:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | /* Class = "IBUILabel"; text = "Release Date"; ObjectID = "CX9-8e-cML"; */ "CX9-8e-cML.text" = "Release Date"; /* Class = "IBUILabel"; text = "Rating"; ObjectID = "FJd-n3-GOf"; */ "FJd-n3-GOf.text" = "Rating"; /* Class = "IBUILabel"; text = "Movie Title"; ObjectID = "NRq-Lz-Ex4"; */ "NRq-Lz-Ex4.text" = "Movie Title"; /* Class = "IBUILabel"; text = "Movie Information"; ObjectID = "SBh-a4-EJR"; */ "SBh-a4-EJR.text" = "Movie Information"; /* Class = "IBUILabel"; text = "Stars"; ObjectID = "bGs-Af-eL9"; */ "bGs-Af-eL9.text" = "Stars"; /* Class = "IBUIButton"; normalTitle = "Pick a Movie"; ObjectID = "dgk-XI-sgx"; */ "dgk-XI-sgx.normalTitle" = "Pick a Movie"; /* Class = "IBUILabel"; text = "Director"; ObjectID = "jnz-9e-ZT1"; */ "jnz-9e-ZT1.text" = "Director"; /* Class = "IBUILabel"; text = "Duration"; ObjectID = "uJE-ia-usa"; */ "uJE-ia-usa.text" = "Duration"; /* Class = "IBUILabel"; text = "Link"; ObjectID = "vgW-o8-2Rh"; */ "vgW-o8-2Rh.text" = "Link"; /* Class = "IBUILabel"; text = "Category"; ObjectID = "yOc-zp-Qs1"; */ "yOc-zp-Qs1.text" = "Category"; |
For every subview, Xcode adds a comment line that contains three things: The class of the subview, the normal title (text) that was assigned to the subview in the Interface Builder, and a unique object ID, which is used internally by Xcode for matching any localized strings to the proper subviews. When such a file is generated, the strings of the base language are assigned to each subview, and it’s our job (or the translator’s job) to replace any string in English with the respective one in the new language (in this case French).
Right next I give you the above file translated to French. If you translate to any other language, then make sure to replace just the words inside the double quotes with the respective translated words in the language you prefer.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | /* Class = "IBUILabel"; text = "Release Date"; ObjectID = "CX9-8e-cML"; */ "CX9-8e-cML.text" = "Date de sortie"; /* Class = "IBUILabel"; text = "Rating"; ObjectID = "FJd-n3-GOf"; */ "FJd-n3-GOf.text" = "Evaluation"; /* Class = "IBUILabel"; text = "Movie Title"; ObjectID = "NRq-Lz-Ex4"; */ "NRq-Lz-Ex4.text" = "Titre de Film"; /* Class = "IBUILabel"; text = "Movie Information"; ObjectID = "SBh-a4-EJR"; */ "SBh-a4-EJR.text" = "Informations Pour Le Film"; /* Class = "IBUILabel"; text = "Stars"; ObjectID = "bGs-Af-eL9"; */ "bGs-Af-eL9.text" = "Stars de Cinéma"; /* Class = "IBUIButton"; normalTitle = "Pick a Movie"; ObjectID = "dgk-XI-sgx"; */ "dgk-XI-sgx.normalTitle" = "Choisissez un film"; /* Class = "IBUILabel"; text = "Director"; ObjectID = "jnz-9e-ZT1"; */ "jnz-9e-ZT1.text" = "Directeur"; /* Class = "IBUILabel"; text = "Duration"; ObjectID = "uJE-ia-usa"; */ "uJE-ia-usa.text" = "Durée"; /* Class = "IBUILabel"; text = "Link"; ObjectID = "vgW-o8-2Rh"; */ "vgW-o8-2Rh.text" = "Lien hypertexte"; /* Class = "IBUILabel"; text = "Category"; ObjectID = "yOc-zp-Qs1"; */ "yOc-zp-Qs1.text" = "Catégorie"; |
Xcode will create ObjectID* values other than the above, so don’t just copy-paste the translation. Instead, copy-paste each translated word one by one.*
That’s all! By translating the above terms, any subviews existing to the storyboard will appear translated later when we’ll run the app, and no more work is required.
For the German localization, just follow all the steps as they were just described. When you add the German localization, you’ll see in the Project Navigator, under the Main.storyboard a new file named Main.strings (German). Click and edit it. Replace all the English words with the German ones given below, and the localization of the storyboard in German is over:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | /* Class = "IBUILabel"; text = "Release Date"; ObjectID = "CX9-8e-cML"; */ "CX9-8e-cML.text" = "Erscheinungsdatum"; /* Class = "IBUILabel"; text = "Rating"; ObjectID = "FJd-n3-GOf"; */ "FJd-n3-GOf.text" = "Wertung"; /* Class = "IBUILabel"; text = "Movie Title"; ObjectID = "NRq-Lz-Ex4"; */ "NRq-Lz-Ex4.text" = "Filmtitel"; /* Class = "IBUILabel"; text = "Movie Information"; ObjectID = "SBh-a4-EJR"; */ "SBh-a4-EJR.text" = "Filminformationen"; /* Class = "IBUILabel"; text = "Stars"; ObjectID = "bGs-Af-eL9"; */ "bGs-Af-eL9.text" = "Filmstars"; /* Class = "IBUIButton"; normalTitle = "Pick a Movie"; ObjectID = "dgk-XI-sgx"; */ "dgk-XI-sgx.normalTitle" = "Wählen Sie einen Film"; /* Class = "IBUILabel"; text = "Director"; ObjectID = "jnz-9e-ZT1"; */ "jnz-9e-ZT1.text" = "Direktor"; /* Class = "IBUILabel"; text = "Duration"; ObjectID = "uJE-ia-usa"; */ "uJE-ia-usa.text" = "Dauer"; /* Class = "IBUILabel"; text = "Link"; ObjectID = "vgW-o8-2Rh"; */ "vgW-o8-2Rh.text" = "Hyperlink"; /* Class = "IBUILabel"; text = "Category"; ObjectID = "yOc-zp-Qs1"; */ "yOc-zp-Qs1.text" = "Kategorie"; |
Localizing Strings
During the implementation in the previous parts we used the NSLocalizedString function a few times, expecting for the proper translation to be applied to the respective strings when we’ll run the app. However, no translation still exist for any of the two localizations we added to the project, and in this part we’re about to fix this. More particularly, in here we’ll create the files to which the NSLocalizedString function will look up for the translated versions of each string, and of course we’ll also do the translation.
Let’s get started to see how all this is achieved. Begin by going to the menu File > New > File…*. In the window that appears, select the Resource** category under the iOS section, and then select the Strings File template.
When you’re about to save the file in the second step, make sure to name it Localizable and then click on the Create button to let it be added to the project.
Check your Project Navigator now, where you can find the newly added file. If you select it, you’ll see that the file is empty, it just contains some information regarding the author, the creation date and the copyright. Let’s add some contents to that file, by entering all the string values we want to be translated later to other languages too. Here it is:
1 2 3 4 5 6 7 8 9 10 | minutes = "minutes"; Close = "Close"; Action = "Action"; Drama = "Drama"; War = "War"; Biography = "Biography"; Sci -Fi = "Sci-Fi"; Thriller = "Thriller"; Adventure = "Adventure"; Mystery = "Mystery"; |
The first two words regard visual elements of the application. The rest are movie categories that we want to be displayed translated as well.
Let’s proceed to the localization of that file now. Open the Utilities pane, and then display the File Inspector (make sure that the Localizable.strings file is selected). In there you’ll find a button titled Localize….
Click it, and select English as the first language for localization to the new popup window:
By clicking on the Localize button, you’ll see that in the File Inspector the Localize… button has been replaced by localization options. The English language is already selected, so you can go and select both the French and German now:
By doing the above selections, a disclosure button appears next to the Localizable.strings file. Open it, and you’ll find files regarding each added localization.
Each localized file has “inherited” the strings that should be translated, only the real translation is missing. So, let’s fix that. Click on the Localizable.strings (French) file, and replace strings values in English with the French ones:
1 2 3 4 5 6 7 8 9 10 | minutes = "minutes"; Close = "Fermer"; Action = "Action"; Drama = "Drame"; War = "Guerre"; Biography = "Biographie"; Sci -Fi = "Sci-Fi"; Thriller = "Thriller"; Adventure = "Aventure"; Mystery = "Mystère"; |
Next, open the Localizable.strings (German) file, and add the translation in German:
1 2 3 4 5 6 7 8 9 10 | minutes = "minuten"; Close = "Schließen"; Action = "Action"; Drama = "Drama"; War = "Krieg"; Biography = "Biographie"; Thriller = "Thriller"; Adventure = "Abenteuer"; Sci -Fi = "Sci-Fi"; Mystery = "Geheimnis"; |
Localizing Images
The internationalization and localization process doesn’t just include any text translation existing in the code or in the interface, but it also has to do with the localization of any other resources existing to the project. In our case, we have an image view in which we want to display a localized image (the flag of each country), depending on the region and the locale of the device that the app runs to. There are certain steps that should be performed, and we’ll see them all all one by one.
Everything begins by importing the image for the base localization to the project. In the jumpstart project you downloaded, there’s already an image named flag.png. Note that the image shouldn’t be added to the Images.xcassets group, instead it should be added directly to the Project Navigator, or at least in a sub-group in it. That’s why the My Images group exists over there. Sub-groups are good for keeping order to the files.
Now, select the flag.png file, and first open the Utilities pane, then the File Inspector. We are going to do exactly the same actions as we did while we were localizing the strings in the last part. In the Localization section, click on the Localize… big button, and in the window that appears select the English language and click on the Localize button. You’ll notice that the button is replaced by some checkboxes. The English localization is already selected, so we must select the French and German localizations as well.
Once you do so, you’ll notice that a disclosure indicator has appeared at the left side of the flag.png image. If you click it, you’ll see all the localized versions of the image that we just added to the project.
However, you’ll easily find out that the US flag appears in every version of the image file, and that’s something that should be fixed.
So, let’s begin with the French flag image. Control-Click on the flag.png (French) file, and in the context menu select the Show in Finder option. Or, in the File Inspector under the Identity and Type section, locate and click on a small arrow button pointing to the right and sits at the right side of the full path of the image file:
No matter which way you’ll choose, you’ll eventually see the image file appearing on the Finder. Now, at first simply delete the existing file (the flag.png) while being in the Finder window, and then copy – paste the flag_france.png file in this directory. Finally, rename the new file to flag.png and you’re ready.
If you return to the project and you click on the flag.png (French) file, the French flag will be displayed.
Following the same steps as before, set the proper flag image for the German localization as well.
Other Xcode 6 Features
The industry standard file format for exchanging and sharing data for translation is the XLIFF type. Files of such format are used worldwide by translators, translating companies, developers, even simple users. An XLIFF file is actually a XML file that is created based on standard specification, and there are programs and applications capable of reading and building such files. You can find more about it here, and if you’re curious about its specification you can take a look at this website.
Since version 6, Xcode is able to create and export XLIFF files, and it also provides the option to import them. This is an awesome addition to Xcode, because it consists of a tool for the developers who work in cooperation with translators or translating companies. Xcode has made the whole process of exporting any texts for translation a quite fast operation, and simple too. If you’re about to use that feature, then you’re just a few clicks away from having your text-to-be-translated already exported.
Let’s take a look at this process. At first, select the project’s group in the Project Navigator. Then go to the menu Editor > Export For Localization…. A window for exporting the XLIFF file(s) is appeared:
As you see, you can select the languages you want to be exported. Feel free to check or uncheck any language you want. At the end, find a location to do the export and click on the Save button. Open the Finder app, navigate to the directory you just saved, and you’ll find the exported .xliff files being there.
Besides than just exporting, you can also import such files in a quite easy manner. Make sure that the project’s group is selected in the Project Navigator, and then go to the Editor > Import Localizations… menu. A window to select the file will appear as shown next:
Just select the file you want, and in the next step click on the Import button, and you’re ready. The good news is that you can import .xliff files with translated data as many times as you want during the development stage, without any risk of losing any previously done translations.
Xcode 6 has also another new feature to show. This is a new localization previewing mode in the Interface Builder and allows you to see how the translated text on the subviews is shown without running the app. Let’s take a bit more detailed look at that:
Begin by opening the Main.storyboard file. Once the Interface Builder is appeared, show the Assistant Editor (menu View > Assistant Editor > Show Assistant Editor). Next, turn on the Preview mode as shown in the next image:
In the Preview mode you can see how your interface looks in a 4-inch device. Further than that however, at the bottom right side there’s a button, which, if you tap it, displays a list with all the available localizations that have been added to the project.
By selecting any language other than the default one, the interface in the Assistant Editor gets changed accordingly. The next image illustrates the interface with the German localization applied:
There’s also one more option appeared when tapping the above button, named Double-Length Pseudolanguage. When is tapped, then any visible text in the subviews is doubled. This is extremely useful in cases you want to see how long strings are displayed, especially if you’re planning to localize to languages with really big words in their dictionary. Here’s a sample of it:
Compile and Run the App
Now that we’ve seen how all the parts of an app are localized, and after having done so, it’s time to run the app and see it in real action. First of all, let’s see how it works in the development language (English), so just build and run. Watch how everything is displayed, and use the button at the bottom side to pick another movie.
To see all the other localizations, you should perform some steps first. Go back to Xcode, and then edit the active scheme:
In the modal window that appears, make sure that the Run option is the selected one at the left side. Next, click on the Options tab in the main area of the window. There are two drop down menus, named Application Language and Application Region. By clicking in the first one, you can select a localization from the existing ones. By clicking then in the second one, you select a region related to the selected localization. For example, you can see below the selections been made for the German localization:
The next image shows a demo of the app running in French localization:
And finally the German version:
Alternatively, you can change the localization in the Settings of the device instead of editing the scheme and changing the region options.
Summary
Our sample app is ready, and it’s time to do a quick recap. As you saw, certain steps are included in the process of internationalizing and localizing every part of the application, and none of them has something particularly difficult. If you’re working on big projects and collaborating with other people for translating your texts, then Xcode can become extremely useful, as it can now export and import XLIFF files. Also, being able to preview the interface while working in the Interface Builder is a time-saving feature, as you don’t have to run the app again and again so as you test out the results. As a last word, I would recommend to always start building your apps applying any necessary internationalization process, even if you don’t plan to localize them. It’s quite possible to do so in the future, and having the ground already prepared, it will be a piece of cake to add extra localizations at any time later. Anyway, I hope you’ve found this tutorial useful and if you want, just drop us a line at the comments!
For your reference, you can download the complete Xcode project from here.
Không có nhận xét nào:
Đăng nhận xét