📜 ⬆️ ⬇️

Updating strings on the fly in mobile apps: part 2



Hi, Habr!

In a recent article, our colleague Dmitry Marushchenko, yojick, talked about how to deliver dynamic translation updates from the server to mobile devices. In continuation of the topic today we will talk about how we use these updated translations in our applications.
')
Historically, all the major out-of-the-box mobile platforms have excellent support for localizing messages. In iOS, Android and Windows Phone, the application can be localized without difficulty. All the tools for this are already built into the IDE: just select the desired language in the list of supported localizations, enter text in this language - and the IDE will do the rest. Works like a clock. But this approach still has flaws.

Found an error in the text? Want to rephrase something? Do you like to experiment with different appeals to different target groups? In all cases, the answer is one: you have to rebuild the application, upload it to the store again, go through the test, get approval, publish the new version with all the changes and wait for users to update the application on their devices. Even if all the procedures go without a hitch, it will take days or weeks. And if users do not want to be updated? Or worse - can not do this for technical reasons, like an unsupported OS? Then the unwanted text in your application will live much longer than we would like.

This is pretty awkward. Fortunately, we managed to solve this problem on different platforms (taking into account the features of each of them). And we are happy to share our decision.

It's simple. We will use the existing platform localization tools, but add dynamic updates on demand to them. To do this, we introduce a localization versioning system. When a developer, technical writer, or anyone else changes the localization database, we will increase the localization version number. When building a mobile application, we will get the latest version of localization and put it in a bundle along with the version number.

If the client receives a signal from the server that a new version of localization appears, it requests an update, providing the server with the current version of localization. Having two different versions, the server creates a diff and transfers it to the mobile client. After receiving a diff, the client applies it to the current version, saves the latest version, and from that moment on uses only the updated package in the application.

The process can be repeated time after time. The client will always receive from the server the latest version of localization. The approach, which will be discussed later, we use on the client side in all mobile applications Badoo. Considering all the implemented server tools and assuming that the client code already has access to the updated localizations, we need only provide the user interface components with the correct messages. For the work!

iOS


A natural way to localize a message in iOS is to use one of the methods of the NSLocalizedString family. We have created a set of similar methods BPFLocalizedString (the BPF prefix means Badoo Platform Foundation) and use them throughout the application. "Under the hood" BPFLocalizedString uses a localization service that contains all the data and implements the basic functionality. We save all updates from the server in a separate bundle. When the client code requests a localized string, we look for the necessary message in this bundle and, if necessary, return to the default localization bundle.

 public func localizedStringForKey(_ key: String) - > String { let str = self.localizationsBundle.localizedString(forKey: key) return str == key ? Bundle.main.localizedString(forKey: key, value: nil, table: nil) : str } 

This approach greatly simplifies client code. In this case, you can use all the heavy localization iOS-machines, including support for languages ​​with right-sided and multiple (plural) localization. For this we only need to maintain valid data inside the additional package.

The BPFLocalizedString API for BPFLocalizedString looks like this:

 NSString * __nonnull BPFLocalizedString(NSString * __nonnull key, NSString * __nullable comment); public func BPFLocalizedString(_ key: String) - > String { return BPFGlobals.shared().localizedStringsService.localizedStringForKey( key) } 

It is easy to use from both Objective-C and Swift code.

There is one subtlety. Localization updates can come at any time during the life cycle of the client application. And at the same time, we can still show users some "old" localized messages. In order to maintain consistency of data, it is better not to mix “old” localizations with “new” ones. We solve this problem by applying updates only the next time the application is launched. Such a solution simplifies everything and allows the client code not to “think” about unexpected cases.

What are the limitations here? You need to be sure that we everywhere replaced NSLocalizedString with the corresponding BPFLocalizedString . Fortunately, this task can be easily solved using automatic scripts. Another limitation is the impossibility of applying BPFLocalizedString directly to statically packaged user interface elements (XIB and Storyboard). This is a completely natural limitation, since we replace the static localization with the dynamic one.

Android


In Android, localizations are packaged in an. Apk file and cannot be changed during execution. In this OS, the standard solution for localizing a message is to use Resources. Access to Resources is part of the Context interface. Resources provides the configuration of the current device (location, screen size, orientation, and so on). One solution is to replace all Resources.getString() with our own custom implementation, as in iOS. But we chose a more elegant way.

What if you could implement your implementation of Resources instead of the system one? Fortunately, this is possible! Take the Activity class, write its successor, and apply it everywhere:

 public abstract class BaseActivity extends Activity { private Resources mResources; public Resources getResources() { if (mResources == null) { Resources r = super.getResources(); mResources = new ResourceWrapper(this, r); } return mResources; } } 

And we will make a wrapper around the standard Resources to retrieve the updated values ​​of the tokens:

 public class ResourceWrapper extends Resources { private final Resources mResources; private final LexemeProvider mLexemeProvider; public ResourceWrapper(Context context, Resources r) { super(r.getAssets(), r.getDisplayMetrics(), r.getConfiguration()); mResources = resources; mLexemeProvider = new LexemeProvider(...); } @Override public String getString(@StringRes int id) throws NotFoundException{ String hotString = mLexemeProvider.getString(id); if (hotString == null) { return mResources.getString(id); } else { return hotString; } // Override each method and return corresponding value from mResources @Override public boolean getBoolean(int id) throws NotFoundException { return mResources.getBoolean(id); } } 

Obviously, you need to intercept all methods related to the text (getString, getText, getQuantityString, getQuantityText), and return the values ​​received from our own localization provider.

Usually, the system does not directly use the Resources class, which is in the Android source, but some of its successor, so you need to forward all calls to the ResourceWrapper to this particular implementation ( mResources in our case).

So far we have dealt with explicit recipients of tokens. What about representations that are “inflated” from XML layouts? When you declare the android: text attribute, after its “inflating”, the TextView calls context.getTheme (). ObtainStyledAttributes (...) .getText (...) to get the corresponding values, in which case our replacement for Resources no longer works.

We need to implement our LocalizationProvider here too.

 public class DynamicLexemeInflater { private static void applyDynamicLexems(View view, String name, Context context, AttributeSet attrs) { if (view instanceof TextView) { TextView textView = (TextView) view; TypedArray typedArray = context.obtainStyledAttributes(attrs, new int[] { android.R.attr.text, android.R.attr.hint }); int textResourceId = typedArray.getResourceId(0, -1); if (textResourceId != -1) { String dispatchedString = context.getString(textResourceId); textView.setText(dispatchedString); } int hintResourceId = typedArray.getResourceId(1, -1); if (hintResourceId != -1) { String dispatchedString = context.getString(hintResourceId); textView.setHint(dispatchedString); } typedArray.recycle(); } } 

Here we set up our factory InflaterFactory to add “post-processing” “inflated” views in which text values ​​are specified.

Windows phone


As in iOS with Android, in Windows Phone, localization resources are placed in the application package. It also cannot be changed during the execution of the program. Our approach to hot localization updates is based on the Windows Phone Silverlight 8.1 API.

In WP applications, we usually refer to localized messages through the toolkit-generated AppResources class (or you can give it any other name), which contains static getters for all strings used in the application. Here is what is inside these getters:

 public static string ApplicationTitle { get { return ResourceManager.GetString("ApplicationTitle", resourceCulture); } } 

Note that the ResourceManager property is used here, defined as

 public static global::System.Resources.ResourceManager ResourceManager { get { if (object.ReferenceEquals(resourceMan, null)) { global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("PhoneApp.Resources.AppResources", typeof (AppResources).Assembly); resourceMan = temp; } return resourceMan; } } 

System.Resources.ResourceManager is a central component of the localization API, it takes all the hard work of loading current string values. Fortunately, in the overloaded method it has an extension point:
public virtual string GetString(string name, CultureInfo culture) . That is what we need to implement our machinery and to supplement the means provided by the system. It is necessary only to inherit from this class and overload the GetString method:

 public class UpdateableResourceManager: ResourceManager { public override string GetString(string name, CultureInfo culture) { var lexemesHandler = _localizationService.GetLexemesHandler(culture); return lexemesHandler?.GetLexeme(name)?.Value?.Text ?? base.GetString(name, culture); } } 

Now we need to use our UpdateableResourceManager instead of what is used by default in the AppResources class. But since this class is automatically generated, you also need to gain control over the generation in order to add your data to the resulting file. This is usually done every time you open the AppResources file in Visual Studio, but you can do it manually (or automatically in the script) using the RESGen tool, as in this PowerShell example:

 $resgenPath = “C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.6 Tools\ResGen.exe” & $resgenPath AppResources.resx to_delete.txt “/str:cs,Badoo.Is.Ponies.Namespace,AppResources,AppResources.Designer.cs” / publicclass Remove — Item “to_delete.txt” 

You also need to replace the System.Resources.ResourceManager line in our Badoo.Next.Big.Thing.UpdateableResourceManager . The rest is processed in a separate LocalizationService, which is responsible for all the work with the network, data storage and retrieval.

Conclusion


This is a brief overview of our approach to implementing the process of dynamically updating localizations for our own mobile applications on all major platforms. All three cases have a lot in common. We have created a powerful and flexible tool to use at any time to apply localization updates. At the same time, we tried as seamlessly as possible to integrate it with our code base. This allowed us to embed the localization infrastructure into mobile applications with minimal effort from client developers. We use localization tools in our daily workflow. This is a time-tested, reliable and useful tool.

Peter Kolpashchikov, iOS developer
Victor Patrushev, Android developer
Stas Shusha, Windows Phone Developer

Source: https://habr.com/ru/post/348350/


All Articles