Localization of applications in iOS. Part 1. What do we have?

IOS application localization

Part 1. What do we have?

Guide for working with localized string resources


A few years ago, I plunged into the magical world of iOS development, which, with all its essence, promised me a happy future in IT. However, delving into the particular platform and development environment, I have faced many difficulties and inconveniences in solving seemingly very trivial tasks: Apple's “innovative conservatism” sometimes makes developers greatly sophisticated in order to satisfy the unbridled customer “I WANT”.

One of these problems is the issue of localization of string resources of the application. I would like to devote some of my first publications to this problem in the open spaces of Habr.

Initially, I expected to fit my thoughts in one article, but the amount of information that I would like to explain was quite large. In this article, I will try to uncover the essence of the standard mechanisms for working with localized resources with an emphasis on some aspects that most guides and tutorials neglect. The material is focused primarily on novice developers (or those who have not encountered such problems). For experienced developers, this information may not be of particular value. But I will tell you about the inconveniences and shortcomings that can be encountered in practice in the future ...

From under the box. How storage of string resources in iOS applications is organized

To begin with, we note that the presence of localization mechanisms in the platform is already a huge plus, since It saves the programmer from additional development and sets a uniform format for working with data. And often, basic mechanisms are sufficient for the implementation of relatively small projects.

And so, what possibilities does Xcode provide us from under the box? To begin, let's understand the standard storage of string resources in a project.

In projects with static content, string data can be stored directly in the interface (markup files .storyboardand .xib, which in turn are XML files rendered using Interface Builder ) or in code. The first approach allows us to simplify and accelerate the process of marking screens and individual displays, because a developer can observe most changes without building an application. However, in this case it is not difficult to run into data redundancy (if the same text is used by several elements, mappings). The second approach just eliminates the problem of data redundancy, but leads to the need to fill the screens manually (by setting additionalIBOutlet-ows and assigning them appropriate text values), which in turn leads to code redundancy (of course, except in cases where the text must be set directly by the application code).

In addition, Apple provides a standard file with the extension .strings. This standard regulates the storage format of string data in the form of an associative array ( "ключ-значение"):

"key" = "value";

The key is case-sensitive, allows for spaces, underscores, punctuation, and special characters.

It is important to note that, despite its simple syntax, Strings files are regular sources of errors during the compilation, assembly, or operation of the application. There are several reasons for this.

First, syntax errors. Missed semicolons, equal signs, extra or unshielded quotes will inevitably lead to a compiler error. And Xcode will point to the file with an error, but will not highlight the line in which something is wrong. Finding such a typo can take a significant amount of time, especially if the file contains a significant amount of data.

Secondly, duplication of keys. The application because of it, of course, will not fall, but incorrect data may be displayed to the user. The thing is that when a string is accessed by a key, the value corresponding to the last occurrence of the key in the file is pulled.

As a result, a simple structure requires a programmer to be very thorough and careful when filling files with data.

Knowledgeabledevelopers can immediately exclaim: "But what about JSON and PLIST? Than they did not please?" Well, firstly, JSONand PLIST(in fact, ordinary XML) are universal standards that allow storing both strings and numerical, logical ( BOOL), binary data, time and date, as well as collections — indexed ( Array) and associative ( Dictionary) arrays. Accordingly, the syntax of these standards is richer, and therefore easier to put in them. Secondly, the processing speed of such files is slightly lower than the Strings files, again due to the more complex syntax. This is not to mention the fact that to work with them you need to carry out a number of manipulations in the code.

Localized, localized, but not dislocated. User Interface Localization

And so, with the standards figured out, let's figure out now how to use it all.

Let's go in order. To begin with, we will create a simple Single View Application and in the Main.storyboard on the ViewController we will add several text components.

Content in this case is stored directly in the interface. To localize it you must do the following:

1) Go to project settings

2) Then - from Target to Project

3) Open the Info tab

In the Localizations section , we immediately see that we already have the entry "English - Development language" . This means that English is set as a development language (or by default).

Let's add another language now. To do this, click " + " and select the desired language (for example, I chose Russian). Caring Xcode immediately invites us to choose which files need to be localized for the added language.

Click Finish , see what happened. In the project navigator, next to the selected files, there are buttons for displaying nestings. Clicking on them we see that the previously selected files contain created localization files.

For example, Main.storyboard (Base)this is the interface markup file created by default in the basic development language, and when forming localization to it in a pair, an associated Main.strings (Russian)file was created - a string file for Russian localization. Opening it you can see the following:

/* Class = "UILabel"; text = "Label"; ObjectID = "tQe-tG-eeo"; */
"tQe-tG-eeo.text" = "Label";
/* Class = "UITextField"; placeholder = "TextField"; ObjectID = "cpp-y2-Z0N"; */
"cpp-y2-Z0N.placeholder" = "TextField";
/* Class = "UIButton"; normalTitle = "Button"; ObjectID = "EKl-Rz-Dc2"; */
"EKl-Rz-Dc2.normalTitle" = "Button";

Here, in general, everything is simple, but for clarity we will take a closer look at, drawing attention to the comments generated by the caring Xcode:

/* Class = "UILabel"; text = "Label"; ObjectID = "tQe-tG-eeo"; */
"tQe-tG-eeo.text" = "Label";

Here is an instance of the class UILabelwith the value "Label"for the parameter text. ObjectID- object identifier in the markup file, is a unique string assigned to any component at the time it is placed on Storyboard/Xib. It is from the ObjectIDname of the object's parameter (in this case text) that the key is formed, and the record itself can be formally interpreted as:

Set the "text" parameter of the "tQe-tG-eeo" object to the value "Label".

In this record, only the " value " is subject to change . Replace " Label " with " Inscription ". We will do the same with other objects.

/* Class = "UILabel"; text = "Label"; ObjectID = "tQe-tG-eeo"; */
"tQe-tG-eeo.text" = "Надпись";
/* Class = "UITextField"; placeholder = "TextField"; ObjectID = "cpp-y2-Z0N"; */
"cpp-y2-Z0N.placeholder" = "Текстовое поле";
/* Class = "UIButton"; normalTitle = "Button"; ObjectID = "EKl-Rz-Dc2"; */
"EKl-Rz-Dc2.normalTitle" = "Кнопка";

Run our application.

But what do we see? The application uses basic localization. How to check if we made the translation correctly?

Here it is worth making a small digression and digging a little towards the features of the iOS platform and the structure of the application.

To begin, consider the change in the structure of the project in the process of adding localization. This is how the project directory looks like before adding Russian localization:

And so after:

As we can see, Xcode created a new directory ru.lprojin which it placed the created localized strings.

And here is the structure of the Xcode project to the finished iOS application? And despite the fact that it helps to better understand the features of the platform, as well as the principles of the distribution and storage of resources directly in the finished application. The bottom line is that when building the Xcode project, in addition to generating the executable file, the environment transfers resources ( Storyboard / Xib interface markup files , images, line files, etc.) into the finished application while maintaining the hierarchy specified at the design stage.

Apple provides a class Bundle(NSBundle)( free translation ) to work with this hierarchy :

Apple uses Bundleto provide access to applications, frameworks, plugins, and many other types of content. Bundle-y organize resources into well-defined subdirectories, and bundle structures differ depending on the platform and type. Using bundle, you can access the package resources without knowing its structure. BundleIt is a single interface for searching for items, taking into account the package structure, user needs, available localizations and other relevant factors.
Finding and opening a resource
Before you can start working with a resource, you must specify it bundle. The class Bundlehas many constructors, but most often is used main .Bundle.mainprovides paths to directories containing the current executable code. Thus, it Bundle.mainprovides access to resources used by the current application.

Consider the structure Bundle.mainusing the class FileManager:

Based on the foregoing, we can conclude that when the application is loaded, it is formed Bundle.main, the current device localization (system language), application localization and localized resources are analyzed. Then the application selects from all available localizations the one that matches the current language of the system and pulls the corresponding localized resources. If there is no match, resources from the default directory are used (in our case, the English localization, since English was defined as the development language, and the need for additional localization of resources can be neglected). If you change the device language to Russian and restart the application, then the interface will already correspond to the Russian localization.

But before you close the theme of localization of the user interface through Interface Builder , it is worth noting another remarkable way. When creating localization files (by adding a new language to the project or in the localized file inspector), it is easy to see that Xcode allows you to select the type of file to be created:

Instead of a string file, you can easily create a localized Storyboard/Xibone that will preserve all the markup of the base file. The great advantage of this approach is that the developer can immediately see how the content will be displayed in one language or another and immediately correct the screen layout, especially if the text volume varies, or another text direction is used (for example, in Arabic, Hebrew), etc. . But at the same time, the creation of additional Storyboard / Xib files significantly increases the size of the application itself (all the same, string files take up much less space).

Therefore, choosing one or another interface localization method should take into account which approach will be more expedient and practical in a specific situation.

Do It Yourself. Working with localized string resources in code

I hope everything is more or less clear with static content. But what about the text that is specified directly in the code?

The developers of the iOS operating system have taken care of this.

For working with localized text resources, the Foundation framework provides a family of methods NSLocalizedStringsin Swift

NSLocalizedString(_ key: String, comment: String)
NSLocalizedString(_ key: String, tableName: String?, bundle: Bundle, value: String, comment: String)

and macros in objective-c

NSLocalizedString(key, comment)
NSLocalizedStringFromTable(key, tbl, comment)
NSLocalizedStringFromTableInBundle(key, tbl, bundle, comment)
NSLocalizedStringWithDefaultValue(key, tbl, bundle, val, comment)

Let's start with the obvious. The parameter keyis the key of the string in the Strings file; val(default value) - default value, which is used in case of absence of the specified key in the file; comment- (less obvious) a brief description of the localized string (in fact, does not carry useful functionality and is intended to clarify the purpose of using a particular string).

As for the parameters tableName( tbl) and bunble, then they should be considered in more detail.

tableName( tbl) Is the name of the String-file (honestly, I don’t know why Apple calls it a table), in which the string we need is located by the specified key; when it is transmitted, the extension is .stringnot specified. The ability to navigate between tables allows you not to store string resources in a single file, but to distribute them at your own discretion. This allows you to get rid of file congestion, simplifies editing, minimizes the chance of errors.

This bundleoption extends the ability to navigate resources even more. As mentioned earlier, a bundle is a mechanism for accessing application resources, that is, we can independently determine the source of resources.

A little more. Let us go directly to the Foundation and consider the declaration of methods (macros) for a clearer picture, since the majority of tutorials simply ignore this point. The Swift framework is not very informative:

/// Returns a localized string, using the main bundle if one is not specified.publicfuncNSLocalizedString(_ key: String, tableName: String? = default, bundle: Bundle = default, value: String = default, comment: String) -> String

"The main bundle returns a localized string" - all we have. In the case of Objective-C is the case is a little different

#define NSLocalizedString(key, comment) \
          [NSBundle.mainBundle localizedStringForKey:(key) value:@"" table:nil]
#define NSLocalizedStringFromTable(key, tbl, comment) \
          [NSBundle.mainBundle localizedStringForKey:(key) value:@"" table:(tbl)]
#define NSLocalizedStringFromTableInBundle(key, tbl, bundle, comment) \
          [bundle localizedStringForKey:(key) value:@"" table:(tbl)]
#define NSLocalizedStringWithDefaultValue(key, tbl, bundle, val, comment) \
          [bundle localizedStringForKey:(key) value:(val) table:(tbl)]

Here you can clearly see that none other works with the string resource files bundle(in the first two cases mainBundle) - just like in the case of interface localization. Of course, I could immediately say about it, considering the class Bundle( NSBundle) in the previous paragraph, but at that time this information did not carry much practical value. But in the context of working with lines in the code, this can not be said. In fact, the global functions provided by the Foundation are just wrappers over standard bundle methods, the main task of which is to make the code more concise and safe. Nobody forbids initializationbundle manually and directly from his name, access resources, but in this way there appears (albeit very, very small) the probability of generating cyclic references and memory leaks.

Further examples will describe the work with global functions and macros.

Consider how it all works.
First, create a String file that will contain our string resources. Call it Localizable.strings * and add it to it.

"testKey" = "testValue";

( Localization of String files is done exactly the same way as Storyboard / Xib , so I will not describe this process. Replace the Russian test file " testValue " with " test value *".)

Important! In iOS, a file with this name is the default string resource file, i.e. if you do not specify the table name tableName( tbl), the application will automatically knock on Localizable.strings.

Add the following code to our project

print("String for 'testKey': " + NSLocalizedString("testKey", comment: ""))

//Objective-CNSLog(@"String for 'testKey': %@", NSLocalizedString(@"testKey", @""));

and run the project. After executing the code, the line will appear in the console

Stringfor'testKey': testValue

Everything works right!

Similarly, with an example of localization of the interface, change the localization and run the application. The result of the code will be

Stringfor'testKey': тестовое значение

Now we will try to get the value by a key that is not in the file Localizable.strings:

print("String for 'unknownKey': " + NSLocalizedString("unknownKey", comment: ""))

//Objective-CNSLog(@"String for 'unknownKey': %@", NSLocalizedString(@"unknownKey", @""));

The result of this code will be

Stringfor'unknownKey': unknownKey

Since there is no key in the file, the method returns the key itself as a result. If this result is unacceptable, it is better to use the method

print("String for 'testKey': " + NSLocalizedString("unknownKey", tableName: nil, bundle: Bundle.main, value: "noValue", comment: ""))

//Objective-CNSLog(@"String for 'testKey': %@", NSLocalizedStringWithDefaultValue(@"unknownKey", nil, NSBundle.mainBundle, @"noValue", @""));

where is the parameter value( default value ). But in this case, be sure to specify the source of resources - bundle.

Localized strings support the interpolation mechanism, similar to standard iOS strings. To do this, you need to add lines of recording using string literal ( %@, %li, %fetc.), for example:

"stringWithArgs" = "String with %@: %li, %f";

To display such a line, you need to add a code like

print(String(format: NSLocalizedString("stringWithArgs", comment: ""), "some", 123, 123.098 ))

//Objective-CNSLog(@"%@", [NSString stringWithFormat: NSLocalizedString(@"stringWithArgs", @""), @"some", 123, 123.098]);

But when using such constructions you need to be very careful! The fact is that iOS strictly keeps track of the number, the order of the arguments, the correspondence of their types to the specified literals. So, for example, if you substitute the string as the second argument instead of the integer value

print(String(format: NSLocalizedString("stringWithArgs", comment: ""), "some", "123", 123.098 ))

//Objective-CNSLog(@"%@", [NSString stringWithFormat: NSLocalizedString(@"stringWithArgs", @""), @"some", @"123", 123.098]);

then the application substitutes the integer code of the string "123" in the place of inconsistency

"String withsome: 4307341664, 123.089000"

If you skip it, you get

"String withsome: 0, 123.089000"

But if you omit the corresponding object in the list of arguments %@

print(String(format: NSLocalizedString("stringWithArgs", comment: ""), "123", 123.098 ))

//Objective-CNSLog(@"%@", [NSString stringWithFormat: NSLocalizedString(@"stringWithArgs", @""),  @"123", 123.098]);

then the application simply falls when the code is executed.

Push me baby! Localization of notifications

Another important task in the issue of working with localized string resources, which I would like to briefly describe, is the task of localizing notifications. The bottom line is that most tutorials (both in Push Notificationsand out Localizable Strings) often neglect this problem, and such tasks are not such a rarity. Therefore, when faced with a similar for the first time, the developer may have a reasonable question: is it possible in principle? Apple Push Notification ServiceI will not consider the mechanism of work here, especially since starting with iOS 10.0, Push and local notifications are implemented through the same framework UserNotifications.

We have to deal with a similar task when developing multi-language client-server applications. When such a problem first arose before me, the first thing that came to my mind was to throw off the problem of localizing messages to the server side. The idea was very simple: when the application starts, it sends the current localization to the backend , and when the server is sent, push-and selects the appropriate message. But there was a problem right away: if the device localization changed, and the application was not restarted (did not update the data in the database), the server sent the text corresponding to the last "registered" localization. And if the application is installed on several devices with different system languages ​​at once, then the whole implementation would work like the devil knows what. Since such a solution immediately seemed to me the wildest crutch, I immediately began to look for adequate solutions (funny, but in many forums the "developers" advised to localize the fuses on the backend- e).

The correct decision turned out to be terribly simple, although not entirely obvious. Instead of standard JSON sent by the server to APNS

"aps" : {
            "alert" : {
                      "body" : "some message";

it is necessary to send JSON of a type

"aps" : {
            "alert" : {
                      "loc-key" : "message localized key";

where the key loc-keyis the key of the localized string from the file Localizable.strings. Accordingly, the push message is displayed in accordance with the current localization of the device.

The interpolation mechanism for localized strings in Push notifications works in the same way :

"aps" : {
          "alert" : {
                    "loc-key" : "message localized key";
                    "loc-args" : [ "First argument", "Second argument" ];

The key loc-argsis an array of arguments that must be embedded in the localized notification text.

Let's sum up ...

And so, what we have in the end:

  • the standard for storing string data in specialized files .stringwith simple and accessible syntax;
  • the ability to localize the interface without additional code manipulations;
  • quick access to localized resources from code;
  • automatic generation of localization files and structuring the resources of the project (application) directory using Xcode;
  • the ability to localize text notifications.

In general, Xcode provides developers with a fairly simple and flexible mechanism for localizing application string resources, sufficient to implement simple localization tasks in relatively small projects.

I will try to describe the pitfalls of the described mechanism and the methods of their circumvention in the next article.

Also popular now: