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 ...
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 .storyboard
and .xib
, which in turn are XML files rendered by 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 eliminates the problem of data redundancy, but leads to the need to fill the screens manually (by setting additional IBOutlet
and assigning corresponding text values ​​to them), which in turn leads to code redundancy (of course, except when the text should installed 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 as 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.
Knowledgeable developers can immediately exclaim: "But what about JSON and PLIST? Than they did not please?" Well, firstly, JSON
and 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.
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)
is the interface markup file created by default in the basic development language, and when localizing it, the associated Main.strings (Russian)
file was created for the 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, let us consider in more detail, 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 UILabel
class with the value "Label"
for the text
parameter. ObjectID
- the object identifier in the markup file is a unique string assigned to any component at the time of its placement on the Storyboard/Xib
. It is from ObjectID
and the name of the object's parameter (in this case, text
) that the key is generated, 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 dig 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.lproj
, in 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 the Bundle(NSBundle)
class ( free translation ) for working with this hierarchy:
Apple uses theBundle
to provide access to applications, frameworks, plugins, and many other types of content. Bundles organize resources into well-defined subdirectories, and bundle structures vary by platform and type. Usingbundle
, you can access the package resources without knowing its structure.Bundle
is a single interface for searching for items, taking into account the package structure, user needs, available localizations and other relevant factors.
Search and open a resource
Before you start working with a resource, you must specify itsbundle
. TheBundle
class has many constructors, but the most commonly used is main .Bundle.main
provides the path to directories containing the current executable code. Thus,Bundle.main
provides access to resources used by the current application.
Consider the Bundle.main
structure using the FileManager
class:
Based on the foregoing, we can conclude that when the application is loaded, its Bundle.main
is formed, 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/Xib
, which 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.
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 the NSLocalizedStrings
family of methods in 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 key
parameter is the string key 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 (to be honest, I don’t know why Apple calls it a table), which contains the string we need by the specified key; during its transfer the .string
extension .string
not 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.
The bundle
option 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. public func NSLocalizedString(_ key: String, tableName: String? = default, bundle: Bundle = default, value: String = default, comment: String) -> String
"The main bundle returns a localized string" - all we have. Objective-C 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 the string resources work with none other than the bundle
(in the first two cases the 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 initializing the bundle
manually and directly accessing resources on its behalf, but in this way there appears (albeit very, very small) the probability of generating circular 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
//Swift print("String for 'testKey': " + NSLocalizedString("testKey", comment: ""))
//Objective-C NSLog(@"String for 'testKey': %@", NSLocalizedString(@"testKey", @""));
and run the project. After executing the code, the line will appear in the console
String for '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
String for 'testKey':
Now let's try to get the value by the key, which is not in the Localizable.strings
file:
//Swift print("String for 'unknownKey': " + NSLocalizedString("unknownKey", comment: ""))
//Objective-C NSLog(@"String for 'unknownKey': %@", NSLocalizedString(@"unknownKey", @""));
The result of this code will be
String for '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
//Swift print("String for 'testKey': " + NSLocalizedString("unknownKey", tableName: nil, bundle: Bundle.main, value: "noValue", comment: ""))
//Objective-C NSLog(@"String for 'testKey': %@", NSLocalizedStringWithDefaultValue(@"unknownKey", nil, NSBundle.mainBundle, @"noValue", @""));
where is the value
parameter ( 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 an entry to the string file using string literals ( %@
, %li
, %f
, etc.), for example:
"stringWithArgs" = "String with %@: %li, %f";
To display such a line, you need to add a code like
//Swift print(String(format: NSLocalizedString("stringWithArgs", comment: ""), "some", 123, 123.098 ))
//Objective-C NSLog(@"%@", [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
//Swift print(String(format: NSLocalizedString("stringWithArgs", comment: ""), "some", "123", 123.098 ))
//Objective-C NSLog(@"%@", [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 with some: 4307341664, 123.089000"
If you skip it, you get
"String with some: 0, 123.089000"
But if you omit the object corresponding to %@
in the list of arguments
//Swift print(String(format: NSLocalizedString("stringWithArgs", comment: ""), "123", 123.098 ))
//Objective-C NSLog(@"%@", [NSString stringWithFormat: NSLocalizedString(@"stringWithArgs", @""), @"123", 123.098]);
then the application simply falls when the code is executed.
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 on Push Notifications
and on Localizable Strings
) often neglect this problem, and such tasks are not so rare. Therefore, when faced with a similar for the first time, the developer may have a reasonable question: is it possible in principle? I will not consider the mechanism of the Apple Push Notification Service
operation 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 the server, when sending a push, 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, 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 loc-key
is passed to the loc-key
key from the Localizable.strings
file. 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-args
is an array of arguments that must be embedded in the localized notification text.
And so, what we have in the end:
.string
files with a simple and accessible syntax;, Xcode , .
.
Source: https://habr.com/ru/post/419077/
All Articles