Part 2: Hot localisations update

Badoo Tech
Bumble Tech
Published in
7 min readNov 20, 2017

Historically, all major mobile platforms have a great level of support in the form of localised messages out of the box. On iOS, Android and Windows Platform it’s rather easy to make an application localised. All the facilities are embedded right into IDEs. Just mark the necessary language in the supported localisations list, type a proper localised text and IDE will manage all the rest for you. It works unfailingly well. However, this approach has a number of drawbacks.

Have you found a mistake in some localised text? Would you like to change some of the wording? Would you like to experiment with different messages for some parts of your audience? The answer is always the same. You have to rebuild the application, resubmit your app to the application store, pass the review process, get the approval, publish the new version with all your changes and wait for users to update the app on their devices. Even in cases where this all goes well, it takes days or weeks. But what if the user doesn’t want to update the application? Or even worse, what if they can’t do it due to some technical reason, like having an unsupported OS version? You’re going to be stuck with undesirable messages for much longer than you wanted.

This is not at all convenient. Fortunately, there is a solution for this problem.

The idea is simple. We are going to reuse existing platform localisation facilities, but augment them with dynamic updates on demand. In order to do that, we introduce a versioning system for our localisation snapshots. Whenever a developer, copywriter or anyone else modifies the localisation database any way, we increase the localisation version number. When we build a mobile application, we get the latest version of the localisation and we bundle it into the application, together with the localisation version. If at some point the client receives a signal from the server that there is a new localisation version, then it requests for and update providing the current localisation version. Having two distinct localisation versions, the server creates a diff and passes it to the mobile client. Upon receiving the diff, the client applies it to the current version, stores the latest localisation version and from now on, uses only this updated localisation bundle across the app.

This process may be repeated over and over. The key point is that the client always gets the latest localisation from the server side. There is a more detailed description of protocol layer and server-side implementation of the whole process in our previous article.

In this article we’re going to shed light on to the approach that we use on the client side across all our Badoo mobile applications. Given all the server side facilities that are implemented and assuming that the client code already has access to updated localisations, all we need is to make sure that we supply the proper messages to the UI components. Let’s get our hands dirty!

iOS

The natural way to get a localised message in iOS is to use one of the methods in the NSLocalizedString family. We’ve created a set of similar methods BPFLocalizedString (where the BPF prefix stands for Badoo Platform Foundation) and use them all around the app. Internally, BPFLocalizedString uses a localisation service, which maintains all the data and implements the core functionality. We store all the updates coming from server side into a separate bundle. We look for a message in this bundle whenever client code asks for a localised string and we fallback to the default localisation bundle when it’s necessary.

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 significantly simplifies the client code. All the heavy iOS localisation machinery, including supporting of right-to-left languages and plural localisations, is still in use. The only thing that we need to do, is to keep the data valid inside our additional bundle.

The top-level 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’s easy to use it from both Objective-C and Swift code.

Incidentally, there is a subtle point that needs to be taken into account. Localisation updates may come at any point in the client’s lifetime. By this moment, we could already be displaying some ‘old’ localised messages to a user. To keep things consistent, it’s better not to mix the ‘old’ localisations with the ‘new’ ones. We address this issue by applying the updates only on next application launch. This simplifies things and allows the client code not to worry about unexpected cases.

What are the limitations of this approach? We need to make sure, that all the usages of NSLocalizedString are replaced with corresponding BPFLocalizedString. Fortunately, this task can be easily scripted and automated. Another limitation is that BPFLocalizedString cannot be applied directly to statically bundled UI elements (XIBs and StoryBoards). This limitation is quite natural since essentially we are replacing static localisation with a dynamic one.

Android

On Android, localisations are bundled into an apk file and there is no way to change them in runtime. The common pattern to get a localised message on Android is to use Resources reference. This reference is a part of the Context interface. Android’s Resources implementation provides the specific localised messages dependent on the current device configuration (locale, screen size, orientation etc.).

One of the approaches could be replacing all the usages of Resources.getString() with our own custom implementation, just like we did on iOS. We’ve chosen a more elegant way.

What if we were able to inject our own Resources implementation instead of the system default one? And fortunately, we can do it! We are going to subclass the Activity class and make use of 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 wrapper around default Resources to fetch updated lexeme values:

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);
}
}

We obviously should override all the methods related to text (getString, getText, getQuantityString, getQuantityText) and pass them through our own localisation provider.

Usually the system provides the instance of custom Resources subclass and we should use this instance as a fallback in our ResourcesWrapper class.

So far, we have dealt with explicit lexeme getters, but what about views, inflated from xml layouts?

When you declare android:text attribute, upon its inflation TextView calls context.getTheme().obtainStyledAttributes(…).getText(…) to get the corresponding value, and our Resources substitution doesn’t work in this case.

We have to inject our LocalizationProvider here too. Let’s do it.

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 our own InflaterFactory to add a kind of “post-processing” to inflated Views, where we set text values.

Windows Phone

Much like for iOS and Android localisation resources on Windows Phone are bundled into the application package. And this bundle cannot be changed at runtime as well. Our approach to hot localisations updating on Windows Phone is based on Windows Phone 8.1 Silverlight API.

On Windows Phone applications, we usually access localised messages via a tool-generated class AppResources (or whatever name you like) that basically has a static getter for each string that the app uses. Let’s see what is inside these getters:

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

Notice the use of ResourceManager property which is 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 the centerpiece of the localisation system API and it does all the heavy lifting of loading the actual strings. Fortunately, it has its extensibility point in the overloaded method:

public virtual string GetString(string name, CultureInfo culture)

This is exactly what we need to inject our own machinery and to augment the system-provided facilities. All we need is just to subtype this class and override 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);
}
}

The last part is to actually use our UpdateableResourceManager instead of a default one in AppResources class. But since this class is autogenerated, we also have to take control of generating it in order to add some custom content to the resulting file. Usually it is done whenever we open a Visual Studio designer for an AppResources file, but we can actually do it manually (or automate it in a script) by using a ResGen tool like this in PowerShell:

$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” / publicclassRemove — Item “to_delete.txt”

And we must also replace the string “System.Resources.ResourceManager” in our “Badoo.Next.Big.Thing.UpdateableResourceManager”. The rest is handled in the _localizationService, that is responsible for all networking, persisting and searching stuff.

Conclusion

This is a simplified overview of our approach to have a dynamically updateable localisation process for our own mobile apps across all the major platforms. As you can see, all the platforms share a lot in their approaches. We’ve built a robust and flexible tool that allows us to apply localisation updates whenever we want. On the other hand, we also tried hard to incorporate our own implementation facilities as seamlessly as possible. This approach allows us to incorporate our own localisation infrastructure into our mobile apps with minimal required knowledge from the client mobile developer. We’re using our localisation facilities on a daily basis and it is proving to be a reliable and useful instrument.

Peter Kolpashchikov, iOS developer.
Viktor Patrushev, Android developer.
Stas Shusha, Windows Phone developer.

--

--

Badoo Tech
Bumble Tech

Github: https://github.com/badoo - https://badootech.badoo.com/ - This is a Badoo/Bumble Tech Team blog focused on technology and technology issues.