How I converted Floating Apps to Localazy

The integration of Localazy took me 5 minutes. Then I dived deep into advanced features of the Localazy Android library.

It could be so simple…

Floating Apps is the reason why we’ve created Localazy, and it was the first app to benefit from what we’ve built.

The integration was a complex task as, from historical reasons, we allow users to change the language of the app independently from the current system locale. There was a lot of code scattered around the app’s codebase to provide this feature.

Except for the simple integration, nothing of what’s described in this post would be necessary, but I didn’t want to break anything for existing users, and it was a great test of what Localazy can do 😉.

Uploading strings & basic integration

The initial integration was simple and straightforward. Floating Apps was already translated to 30 languages, and so all strings were already externalized into XML resources. Also, a long time ago, I split strings between several files:

  • strings_main.xml - strings for the main app
  • strings_apps.xml - strings for the floating mini apps
  • strings_buddy.xml - strings for the floating buddy feature
  • strings_freeform.xml - strings related to the freeform feature
  • strings_help.xml - strings for help
  • strings_private.xml - private strings such as configuration, ad IDs, FB app ID, etc.
  • strings_universal.xml - strings that I don’t want to translate for some reason

I followed the integration guide, add Localazy Gradle plugin to the root build.gradle, and append the code below to the end of the app module build script.

apply plugin: 'com.localazy.gradle'

localazy {
    readKey "---"
    writeKey "---"
    
    // Used for testing that everything is translated correctly
    addPrefix false

    upload {      
        files {
            include "main:values/strings_apps.xml"
            include "main:values/strings_buddy.xml"
            include "main:values/strings_freeform.xml"
            include "main:values/strings_help.xml"
            include "main:values/strings_main.xml"
        }
    }
}

And that was it! I uploaded strings to Localazy, waited for them to be published, and then cleaned and rebuilt the app. Purposely, I used addPrefix true to see if everything was working well. And as you can see in the screenshot below, it was!

Floating Apps - first test after the integration

It worked like magic. I made no changes to the source code and just tuned my build script a bit, and my whole app with around 100k LOC was fully translated with over-the-air updates.

If I was not allowing users to switch languages in the app’s settings, I was done at this moment. Nothing else was needed to do to translate the app with Localazy.

Removing my stupid code

I’m not proud of it, but some parts of Floating Apps were developed under pressure, and while they work and are tested well, the code could be written better.

So, I removed my old method for changing the locale that was scattered around the whole app and followed the errors to get all the occurrences removed.

It was a lot of occurrences and long sleepless hours to get all this done correctly. But it’s no longer necessary, and having Localazy a few years ago, I wouldn’t need to write that code at all.

Stupid code for smart features

You may wonder why I allow users to change the language in the app itself. There is a good reason for it. It attracts more users to help with translating the app. They can see that their language is in the app; I can communicate that the translation is not complete and ask for their help, etc. It’s a way how to work together with Floating Apps users to make the app great!

However, it added a lot of burden to my shoulders. I manually managed the list of supported languages and whether they are translated fully or not.

I hated to release new major versions as it forced me to check all new translations and update the list of languages.

Good for us all, I dreamed about a tool like Localazy for such a long time that the only option was to go and create it 😃.

Fixing the language resource contamination

If you are not sure what the language resource contamination is, please read more about it in our documentation.

I never cared about this before because it would be even more work for me with every release. With Localazy, it’s simple, and so I decided to implement it. It took me just 1 minute.

It’s not necessary with OTA updates, but it can decrease app size a bit, and you can be sure that your app is going to work correctly in every situation. Good for users; worth to do.

All my strings were already uploaded to Localazy before I started the implementation. Be sure to introduce this change after you successfully uploaded your strings.

The app module build script looked like this after the change:

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'com.localazy.gradle.data'

localazy {

    readKey "---"
    writeKey "---"
  
    upload {
        files {
            include "main:values/strings_apps.xml"
            include "main:values/strings_buddy.xml"
            include "main:values/strings_freeform.xml"
            include "main:values/strings_help.xml"
            include "main:values/strings_main.xml"
        }
    }

}

android {

    // ...

    defaultConfig {    
        // ...
        resConfigs localazy.getResConfigs()
    }

    // ...

}

// ...

apply plugin: 'com.google.gms.google-services'
apply plugin: 'com.localazy.gradle'

I cleaned and rebuilt the project, and with the Analyze APK feature of Android Studio, I checked that only the locales managed by Localazy are included in the app’s APK.

Converting data from the old version

I used to store the selected language as a string in format lang-REGION in the shared preferences under the name general_language. It was a very old decision from the time I knew nothing about languages and translating mobile apps, and over time, there was no motivation to change it. It just worked, and changing the working code without reason is usually not the best idea, you know 😃.

In fact, general_language is used by PreferenceFragment (more on this in the next chapter), and with Localazy, it’s no longer needed at all. Localazy has its own mechanism for persisting the forced locale.

As Floating Apps is used by hundreds of thousands of users on a daily basis, I’m reluctant to make breaking changes if not necessary. Therefore, I want to convert the value of general_language and make Localazy to use the same locale as the user selected in the previous versions. Users hate unpredictable changes. Believe me.

So, I added this simple code to convert general_language:

boolean generalLanguageConverted = sp.getBoolean(SP_KEY_GENERAL_LANGUAGE_CONVERTED, false);

// Convert generalLanguage to Localazy if it wasn't converted yet.
if (!generalLanguageConverted) {

  // Mark the language as converted.
  sp.edit().putBoolean(SP_KEY_GENERAL_LANGUAGE_CONVERTED, true).apply();

  // generalLanguage is empty => keep automatic detection, don't force locale for Localazy
  // generalLanguage is not empty => run the conversion
  String generalLanguage = sp.getString(SP_KEY_GENERAL_LANGUAGE, "");
  if (generalLanguage.length() != 0) {
    try {
      String[] pair = generalLanguage.split("-");
      String code = pair[0];
      String country = "";
      if (pair.length > 1) country = pair[1];
      Locale locale = new Locale(code, country);
      Localazy.forceLocale(locale, true);
    } catch (Exception ignored) {
      // Just for case there is a problem with the stored locale.
      // Ignoring the exception fallbacks to the automatic locale detection.
    }
  }

}

From that moment on, everything related to locales is handled by Localazy.

Rewriting the language selection

The language selection is a part of the main settings screen, which is still the old good PreferenceFragment. Yep, Floating Apps is under development for about 7 years, and the technology is evolving. It’s not easy to keep up. Maybe, it’s not even necessary.

The definition XML for the fragment looks like:

<ListPreference
  android:entries="@array/languageValue"
  android:entryValues="@array/languageKey"
  android:icon="@null"
  android:key="general_language"
  android:persistent="true"
  android:layout="@layout/pref_normal"
  android:title="@string/settings_general_language" />

<Preference
  android:key="general_language_help"
  android:layout="@layout/pref_normal"
  android:summary=""
  android:title="" />

I removed android:entries and android:entryValues and added the code to populate the preference from Localazy. The code is below.

Persisting the state with android:persistent="true" for general_language is also no longer necessary as Localazy automatically stores the selected locale, but I have a watcher for shared preferences that automatically notifies other processes (see more in the next chapter), and therefore I keep it.

Now, let’s get the preferences connected with Localazy. While other code snippets are in Java, this one is written in Kotlin. I love Kotlin, and Floating Apps codebase is, at the moment, mixed. There are around 100k lines of code, partly in Java, partly in Kotlin. Migrating to all code to Kotlin would be an enormous task, but Kotlin is used for all new parts.

// Show the language selection only if the locales are correctly loaded by Localazy.
val locales: List<LocalazyLocale>? = Localazy.getLocales()
if (locales != null) {

    // Get the currently selected locale from Localazy.
    val selected = Localazy.getCurrentLocalazyLocale()

    (findPreference(PK_GENERAL_LANGUAGE) as ListPreference).apply {

        // Populate the preference with locales from Localazy.
        val sortedLocales = locales.sortedBy { it.localizedName }
        entries = sortedLocales.map { it.localizedName }.toTypedArray()
        entryValues = sortedLocales.map { it.localazyId.toString() }.toTypedArray()
        summary = if (selected != null) selected.localizedName else ""

        // React to the change of language.
        bindString { preference, value ->

            val newSelection = locales.find { it.localazyId.toString() == value }
            if (newSelection != null) {
                preference.summary = newSelection.localizedName

                // Force the locale and apply changes to the current activity.
                Localazy.forceLocale(newSelection.locale, true)
                refreshActivity()
            } else {
                preference.summary = ""
            }

        }
    }


    findPreference(PK_GENERAL_LANGUAGE_HELP).apply {

        // Change the motivation message under the language selection according to
        // whether the language is fully translate or not.
        if (selected != null) {
            if (!selected.isFullyTranslated) {
                setTitle(R.string.lang_help_title)
                setSummary(R.string.lang_help_text)
                setIcon(R.drawable.menu_translate)
                icon = icon.mutate()
                icon.setColorFilter(COLOR_WARNING, PorterDuff.Mode.SRC_IN)
            } else {
                setTitle(R.string.menu_translate)
                setSummary(R.string.translate_title)
            }

        } else {
            (findPreference(PK_CATEGORY_GENERAL) as PreferenceGroup).removePreference(this)
        }

        // Show the screen for new translators on click. 
        onPreferenceClickListener = Preference.OnPreferenceClickListener {
            (activity as ActivityMain).showFragmentWithAnimation(FragmentTranslate::class.java)
            true
        }
    }

} else {

    // Hide the language selection if the locales are not loaded.
    (findPreference("general_category") as PreferenceGroup).apply {
        removePreference(findPreference(PK_GENERAL_LANGUAGE))
        removePreference(findPreference(PK_GENERAL_LANGUAGE_HELP))
    }
    
}

The code snippet above is a bit longer, but it does more than just changing locale. It also shows a motivational message that can attract more contributors/volunteers to help with the translating of the app.

In action, it looks like:

Preference screen with motivational message

Synchronizing across processes

Not enough complications with Floating Apps? Well, okay. Let’s dive into another issue. The app uses more processes; one for the main configuration app and second for the floating mini-apps. It eliminates a problem with the background service being killed on some devices.

Localazy can be used in multi-process apps, but when you allow users to change the language, you have to introduce some kind of synchronization. I use Handler and Messenger to communicate between processes. When the configuration is changed in the main configuration app, the message is sent to the background service. It applies to all changes in the configuration, not only languages.

Everything I needed to do was to invoke the line below when the message was received:

Localazy.forceReload();

That’s it! The simple solution for the multi-process issue. Method forceReload also reloads cached data, and therefore it’s the perfect solution for our situation.

Not collecting stats for development

By default, Localazy collects anonymous stats and optimizes the translation and review process. It’s a fantastic feature! My previous translation platform just offered strings for translating in random order (or something that looks like a random order). I don’t want the text for an error message that is rarely shown to be among the first things for translating.

There is a lot of incredible people that helped me with translating Floating Apps to many different languages, but they usually don’t translate the whole app at once. So, it’s better if they focus on the more important strings first.

This issue is solved transparently by Localazy for me. Perfect!

I added this to my Application class to be sure that I don’t send stats during the development:

@Override
public void onCreate() {
  super.onCreate();
   
  if (BuildConfig.DEBUG) {
    Localazy.setStatsEnabled(false);
  }

  // ... more init code ...
}

If you have enough users, it’s not necessary to disable stats collecting as you are probably not going to affect them significantly anyway.

Planning for the future

The Localazy Android library allows listening to different events. One of them is missingTextFound, which is fired when the translation is missing for the requested key.

I want to introduce some kind of pro-active notification that is shown when the user is presented with the untranslated string. It could be a great way to attract more users to help me with translating the app.

However, this is under review at the moment as I need to find the best way how to do it. In no way, I want to bother users or distract them from what they are doing at the moment when the event occurs.

There must be some kind of aggregation of events and finding the right timing to show the notification — a lot of to think about before actual implementation.

However, if you know the right way how to implement this feature, it’s quite simple to do so. When the missingTextFound is called, you get instance of LocalazyId and all you need to do is to point the user to LocalazyId.getPhraseUrl() where she can instantly start translating the given missing phrase. Don’t forget to let me know your solution 😉.

No problem at all!

As you can see, converting Floating Apps with keeping all its features was a complex task. There were several situations in which the app had a non-standard behavior that I wasn’t willing to change because of historical reasons and because of existing users.

However, every single situation was easily solved with Localazy. As a side effect, it improved the app and simplified my codebase. And most importantly, it removes a lot of manual work I have to care about with every release.