Kotlin: Change application language on fly (in runtime)


Topics interesting only for Kotlin / Android / SQLite programmers

Link to this posting

Postby Ursego » 29 Dec 2019, 20:59

The standard Android behaviour is this: if an application has translations to different languages, the correct translation is picked up when the app starts - according to the language of the phone. But sometimes we want to allow user to change language when the application is running. The provided solution does it with help of a trivial Preference which is stored in SharedPreferences. That:

1. Switches to another language immediately when user selects it in the Settings screen.

2. Causes the app to be open in that language next time.

You will create the Settings (Preferences) screen with the Language Preference, which will be looking something like that (of course, you can add more Preferences to that screen):

Image Image

Sounds good? So, you need to

DO THE FOLLOWING STEPS:

@ Create application Settings (Preferences) screen. For that, do all the steps, described here.

@ Add to your values\strings.xml:

Code: Select all
    <string name="word__language">Language</string>
    <string name="lang__english">English</string>
    <string name="lang__russian">Russian (Русский)</string>

@ Add similar entries to strings.xml of each language. I will use Russian as an example. So, add the following fragment to ru\strings.xml (you need to translate also the word "Settings", added to values\strings.xml when you were creating the Preferences screen in the first step):

Code: Select all
    <string name="word__settings">Настройки</string>
    <string name="word__language">Язык</string>
    <string name="lang__english">Английский (English)</string>
    <string name="lang__russian">Русский</string>

Pay attention, that each entry in the languages drop down list contains the language twice:

1. In the current language of the application.

2. In that language itself (so, if user switches to a language with absolutely different alphabet (or hieroglyphs), he/she will be always able easily restore her/his language).

The only exception - the current language of the application: it will appear only once. It doesn't make sense to display "English (English)" or "Русский (Русский)". :lol:

Image Image

@ In the "util" package, create object AppPrefs. Copy-paste its source code from here.

@ In the "util" package, create interface ConstantsSet<T>. Copy-paste its source code from here.

@ Create "constants" package, if you don't have it yet - it's a package to store constants (standalone or packed into objects or enums).

@ In the "constants" package, create object Lang with the following code:

Code: Select all
import <YOUR BASE PACKAGE>.R
import <YOUR BASE PACKAGE>.util.ConstantsSet

// ----------------------------------------------------------------------------------------------------------------------
// This object is used to change your application locale on the fly.
// http://code.intfast.ca/viewtopic.php?t=823
// ----------------------------------------------------------------------------------------------------------------------

object Lang : ConstantsSet<String> {
    const val ENGLISH = "en" // mentioned in PrefFragment & LocaleHelper; if you delete this constant, change them accordingly
    const val RUSSIAN = "ru"

    override fun toArray(): Array<String> = arrayOf(
        ENGLISH,
        RUSSIAN
    )

    override fun getResourceId(constantValue: String): Int {
        return when (constantValue) {
            ENGLISH -> R.string.lang__english
            RUSSIAN -> R.string.lang__russian
            else -> throw Exception("$constantValue is not a valid value of ${this.javaClass.name}.")
        }
    }
}

@ In your "util" package, create the LocaleHelper object:

Code: Select all
import android.annotation.TargetApi
import android.content.Context
import android.os.Build
import ca.intfast.nocarbs.pref.PrefKey
import java.util.*

// ----------------------------------------------------------------------------------------------------------------------
// This class is used to change your application locale on the fly.
// http://code.intfast.ca/viewtopic.php?t=823
// ----------------------------------------------------------------------------------------------------------------------

object LocaleHelper {
    fun setLocaleByPrefLang(context: Context): Context {
        val appPrefsLang = AppPrefs.getString(PrefKey.LANG, context)
        if (appPrefsLang == "") appPrefsLang = Lang.ENGLISH
        return this.setLocale(context = context, newLang = appPrefsLang)
    }

    private fun setLocale(context: Context, newLang: String): Context {
        AppPrefs.put(PrefKey.LANG, newLang, context)
        val newLocale = Locale(newLang)
        Locale.setDefault(newLocale)

        return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) // 25, 24
            updateResources(context, newLocale)
        else
            updateResourcesLegacy(context, newLocale)
    }

    @TargetApi(Build.VERSION_CODES.N)
    private fun updateResources(context: Context, newLocale: Locale): Context {
        val config = context.resources.configuration
        config.setLocale(newLocale)
        config.setLayoutDirection(newLocale)
        return context.createConfigurationContext(config)
    }

    @SuppressWarnings("deprecation")
    private fun updateResourcesLegacy(context: Context, newLocale: Locale): Context {
        val resources = context.resources
        val configuration = resources.configuration
        configuration.locale = newLocale
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
            configuration.setLayoutDirection(newLocale)
        }
        resources.updateConfiguration(configuration, resources.displayMetrics)
        return context
    }
}

@ In your "util" package, create the AppCompatActivityWithLangByPref class. It will be the direct ancestor of all the Activities in your app (instead of AppCompatActivity):

Code: Select all
import android.content.Context
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import <YOUR BASE PACKAGE>.R

// ----------------------------------------------------------------------------------------------------------------------
// This class is used to change your application locale on the fly.
// http://code.intfast.ca/viewtopic.php?t=823
// ----------------------------------------------------------------------------------------------------------------------

abstract class AppCompatActivityWithLangByPref: AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        LocaleHelper.setLocaleByPrefLang(context = this)
        setTitle(R.string.app_name)
    }

    override fun onRestart() {
        super.onRestart()
        LocaleHelper.setLocaleByPrefLang(context = this)
        recreate()
    }

    override fun attachBaseContext(newBase: Context?) {
        val baseContext = LocaleHelper.setLocaleByPrefLang(context = newBase!!)
        super.attachBaseContext(baseContext)
    }
}

@ Open class PrefActivity (created in the first step) and make it inherited from AppCompatActivityWithLangByPref (instead of AppCompatActivity). Don't close it - go to the next step.

@ Add the globe icon. It will be shown in the language selection Preference. If the Settings screen has many Preferences, and user selected a language with a not-understandable writing, that icon will give a clue that it's the "Language" setting.

Steps to add the globe icon: right click the res/drawable folder > New > Image Asset > In "Icon type" select "Action Bar and Tabs Icons > In "Name" type "icon_globe" > Click "Clip Art" > In the search bar type "lang" > Click the Language icon > OK > In "Theme" choose "HOLO_LIGHT" > Next > Finish.

@ Go to PrefFragment (created in the first step) and change its whole code to the code provided below. If you reused your old PreferenceFragment (rather than created PrefFragment as the first step suggested), than add to it all the stuff related to languages switch (the DropDownPreference for language and the processing in onSharedPreferenceChanged):

Code: Select all
import android.content.SharedPreferences
import android.os.Bundle
import androidx.core.content.ContextCompat
import androidx.preference.DropDownPreference
import <YOUR BASE PACKAGE>.constants.Lang
import <YOUR BASE PACKAGE>.R
import <YOUR BASE PACKAGE>.constants.PrefKey
import <YOUR BASE PACKAGE>.util.LocaleHelper
import <YOUR BASE PACKAGE>.util.PreferenceFragmentAutomaticSummary

class PrefFragment : PreferenceFragmentAutomaticSummary(), SharedPreferences.OnSharedPreferenceChangeListener {
    override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
        val screen = preferenceManager.createPreferenceScreen(preferenceManager.context)!!

        val dropDownPref: DropDownPreference
       
        // --------------------------------------------------------------------------------------------------------------
        // This preference is used to change your application locale on the fly.
        // http://code.intfast.ca/viewtopic.php?t=823
        // --------------------------------------------------------------------------------------------------------------

        dropDownPref = DropDownPreference(context)
        dropDownPref.key = PrefKey.LANG
        dropDownPref.title = getString(R.string.word__language)
        dropDownPref.icon = ContextCompat.getDrawable(context!!, R.drawable.icon_globe)
        dropDownPref.entryValues = Lang.toArray()
        dropDownPref.entries = Lang.toDisplayedValuesArray()
        dropDownPref.setDefaultValue(Lang.ENGLISH)
        screen.addPreference(dropDownPref)

        // ...add other preferences of your app...

        preferenceScreen = screen
    }

    override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
        super.onSharedPreferenceChanged(sharedPreferences, key)
        if (key == PrefKey.LANG) {
            // Change the language of the Settings screen which is currently shown.
            // PrefActivity is inherited from AppCompatActivityWithLangByPref, so activity?.recreate() will fire ???mz
            // AppCompatActivityWithLangByPref.onRestart() which calls LocalHelper.setLocaleByPrefLang():
            activity?.recreate()
        }
    }
}

Obviously, if your app has more settings, add them to onCreatePreferences() as well.

@ When you create Activities, always inherit them from AppCompatActivityWithLangByPref (instead of AppCompatActivity). Now I will use MainActivity as an example - copy-paste and customize:

Code: Select all
import android.os.Bundle
import <YOUR BASE PACKAGE>.R
import <YOUR BASE PACKAGE>.util.AppCompatActivityWithLangByPref

class MainActivity : AppCompatActivityWithLangByPref() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        this.setContentView(R.layout.activity_main)
        setTitle(R.string.<TITLE OF THIS ACTIVITY>)
       
        // ...your stuff...
    }

    override fun onResume() {
        super.onResume()
        // Maybe, user is coming back from Settings screen where he/she changed language -
        // reload the Activity's title in the new language:
        setTitle(R.string.<TITLE OF THIS ACTIVITY>)
    }
}
User avatar
Ursego
Site Admin
 
Posts: 143
Joined: 19 Feb 2013, 20:33



IF you want to ((lose weight) OR (have unbelievable (brain function AND mental clarity))) THEN click:




cron
Traffic Counter

free counters

eXTReMe Tracker