Android N change language programmatically

JavaAndroidAndroid 7.0-Nougat

Java Problem Overview


I found really weird bug that is reproduced only on Android N devices.

In tour of my app there is a possibility to change language. Here is the code that changes it.

 public void update(Locale locale) {

    Locale.setDefault(locale);

    Configuration configuration = res.getConfiguration();

    if (BuildUtils.isAtLeast24Api()) {
        LocaleList localeList = new LocaleList(locale);

        LocaleList.setDefault(localeList);
        configuration.setLocales(localeList);
        configuration.setLocale(locale);

    } else if (BuildUtils.isAtLeast17Api()){
        configuration.setLocale(locale);

    } else {
        configuration.locale = locale;
    }

    res.updateConfiguration(configuration, res.getDisplayMetrics());
}

This code works great in activity of my tour ( with recreate() call) but in all next activities all String resources are wrong. Screen rotation fixes it. What can i do with this problem? Should i change locale for Android N differently or it's just system bug?

P.S. Here's what i found. At first start of MainActivity (which is after my tour) Locale.getDefault() is correct but resources are wrong. But in other activities it gives me wrong Locale and wrong resources from this locale. After rotation screen (or perhaps some other configuration change) Locale.getDefault() is correct.

Java Solutions


Solution 1 - Java

Ok. Finally i managed to find a solution.

First you should know that in 25 API Resources.updateConfiguration(...) is deprecated. So instead you can do something like this:

  1. You need to create your own ContextWrapper that will override all configuration params in baseContext. For example this is mine ContextWrapper that changes Locale correctly. Pay attention on context.createConfigurationContext(configuration) method.

    public class ContextWrapper extends android.content.ContextWrapper {

     public ContextWrapper(Context base) {
         super(base);
     }
    
     public static ContextWrapper wrap(Context context, Locale newLocale) {
         Resources res = context.getResources();
         Configuration configuration = res.getConfiguration();
    
         if (BuildUtils.isAtLeast24Api()) {
             configuration.setLocale(newLocale);
    
             LocaleList localeList = new LocaleList(newLocale);
             LocaleList.setDefault(localeList);
             configuration.setLocales(localeList);
    
             context = context.createConfigurationContext(configuration);
    
         } else if (BuildUtils.isAtLeast17Api()) {
             configuration.setLocale(newLocale);
             context = context.createConfigurationContext(configuration);
    
         } else {
             configuration.locale = newLocale;
             res.updateConfiguration(configuration, res.getDisplayMetrics());
         }
    
         return new ContextWrapper(context);
     }
    

    }

  2. Here's what you should do in your BaseActivity:

    @Override protected void attachBaseContext(Context newBase) {

     Locale newLocale;
     // .. create or get your new Locale object here.
    
     Context context = ContextWrapper.wrap(newBase, newLocale);
     super.attachBaseContext(context);
    

    }

Note:

> Remember to recreate your activity if you want to change Locale in > your App somewhere. You can override any configuration you want using > this solution.

Solution 2 - Java

Inspired by various codes (i.e: our Stackoverflow team (shout out people)), I had produced a much simpler version. The ContextWrapper extension is unnecessary.

First let's say you have 2 buttons for 2 languages, EN and KH. In the onClick for the buttons save the language code into SharedPreferences, then call the activity recreate() method.

Example:

@Override
public void onClick(View v) {
	switch(v.getId()) {
		case R.id.btn_lang_en:
			//save "en" to SharedPref here
			break;
		case R.id.btn_lang_kh:
			//save "kh" to SharedPref here
			break;

		default:
		break;
	}
	getActivity().recreate();
}

Then create a static method that returns ContextWrapper, perhaps in a Utils class (coz that's what I did, lul).

public static ContextWrapper changeLang(Context context, String lang_code){
	Locale sysLocale;

	Resources rs = context.getResources();
	Configuration config = rs.getConfiguration();

	if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
		sysLocale = config.getLocales().get(0);
	} else {
		sysLocale = config.locale;
	}
	if (!lang_code.equals("") && !sysLocale.getLanguage().equals(lang_code)) {
		Locale locale = new Locale(lang_code);
		Locale.setDefault(locale);
		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
			config.setLocale(locale);
		} else {
			config.locale = locale;
		}
		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
			context = context.createConfigurationContext(config);
		} else {
			context.getResources().updateConfiguration(config, context.getResources().getDisplayMetrics());
		}
	}

	return new ContextWrapper(context);
}

Finally, load the language code from SharedPreferences in ALL ACTIVITY'S attachBaseContext(Context newBase) method.

@Override
protected void attachBaseContext(Context newBase) {
	String lang_code = "en"; //load it from SharedPref
	Context context = Utils.changeLang(newBase, lang_code);
	super.attachBaseContext(context);
}

BONUS: To save palm sweat on keyboard, I created a LangSupportBaseActivity class that extends the Activity and use the last chunk of code there. And I have all other activities extends LangSupportBaseActivity.

Example:

public class LangSupportBaseActivity extends Activity{
	...blab blab blab so on and so forth lines of neccessary code

	@Override
	protected void attachBaseContext(Context newBase) {
		String lang_code = "en"; //load it from SharedPref
		Context context = Utils.changeLang(newBase, lang_code);
		super.attachBaseContext(context);
	}
}

public class HomeActivity extends LangSupportBaseActivity{
	...blab blab blab
}

Solution 3 - Java

Since Android 7.0+ some parts of my app didn't change their language anymore. Even with the new methods proposed above. Updating of both application and activity context helped me. Here is a Kotlin example of Activity subclass overrides:

private fun setApplicationLanguage(newLanguage: String) {
    val activityRes = resources
    val activityConf = activityRes.configuration
    val newLocale = Locale(newLanguage)
    activityConf.setLocale(newLocale)
    activityRes.updateConfiguration(activityConf, activityRes.displayMetrics)

    val applicationRes = applicationContext.resources
    val applicationConf = applicationRes.configuration
    applicationConf.setLocale(newLocale)
    applicationRes.updateConfiguration(applicationConf,
            applicationRes.displayMetrics)
}

override fun attachBaseContext(newBase: Context?) {
    super.attachBaseContext(newBase)

    setApplicationLanguage("fa");
}

Note: updateConfiguration is deprecated but anyway, createConfigurationContext for each Activity, left some strings unchanged.

Solution 4 - Java

Changing locale programatically in Android app is quite a pain. I have spent lot of time to find working solution, that currently works in production.

You need to override context in every Activity but also in your Application class, otherwise you will end up with mixed languages in ui.

So here is mine solution which works up to API 29:

Subclass your MainApplication class from:

abstract class LocalApplication : Application() {
    
    override fun attachBaseContext(base: Context) {
        super.attachBaseContext(
            base.toLangIfDiff(
                PreferenceManager
                    .getDefaultSharedPreferences(base)
                    .getString("langPref", "sys")!!
             )
        )
    }
}

Also every Activity from:

abstract class LocalActivity : AppCompatActivity() {

    override fun attachBaseContext(newBase: Context) {
        super.attachBaseContext(            
            PreferenceManager
                .getDefaultSharedPreferences(base)
                    .getString("langPref", "sys")!!
        )
    }

    override fun applyOverrideConfiguration(overrideConfiguration: Configuration) {
        super.applyOverrideConfiguration(baseContext.resources.configuration)
    }
}

Add LocaleExt.kt with next extension functions:

const val SYSTEM_LANG = "sys"
const val ZH_LANG = "zh"
const val SIMPLIFIED_CHINESE_SUFFIX = "rCN"


private fun Context.isAppLangDiff(prefLang: String): Boolean {
    val appConfig: Configuration = this.resources.configuration
    val sysConfig: Configuration = Resources.getSystem().configuration

    val appLang: String = appConfig.localeCompat.language
    val sysLang: String = sysConfig.localeCompat.language

    return if (SYSTEM_LANG == prefLang) {
        appLang != sysLang
    } else {
        appLang != prefLang
                || ZH_LANG == prefLang
    }
}

fun Context.toLangIfDiff(lang: String): Context =
    if (this.isAppLangDiff(lang)) {
        this.toLang(lang)
    } else {
        this
    }

@Suppress("DEPRECATION")
fun Context.toLang(toLang: String): Context {
    val config = Configuration()

    val toLocale = langToLocale(toLang)

    Locale.setDefault(toLocale)
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
        config.setLocale(toLocale)

        val localeList = LocaleList(toLocale)
        LocaleList.setDefault(localeList)
        config.setLocales(localeList)
    } else {
        config.locale = toLocale
    }

    return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
        config.setLayoutDirection(toLocale)
        this.createConfigurationContext(config)
    } else {
        this.resources.updateConfiguration(config, this.resources.displayMetrics)
        this
    }
}

/**
 * @param toLang - two character representation of language, could be "sys" - which represents system's locale
 */
fun langToLocale(toLang: String): Locale =
    when {
        toLang == SYSTEM_LANG ->
            Resources.getSystem().configuration.localeCompat

        toLang.contains(ZH_LANG) -> when {
            toLang.contains(SIMPLIFIED_CHINESE_SUFFIX) ->
                Locale.SIMPLIFIED_CHINESE
            Build.VERSION.SDK_INT >= Build.VERSION_CODES.N ->
                Locale(ZH_LANG, "Hant")
            else ->
                Locale.TRADITIONAL_CHINESE
        }

        else -> Locale(toLang)
    }

@Suppress("DEPRECATION")
private val Configuration.localeCompat: Locale
    get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
        this.locales.get(0)
    } else {
        this.locale
    }

Add to your res/values/arrays.xml your supported languages in array:

<string-array name="lang_values" translatable="false">
    <item>sys</item> <!-- System default -->
    <item>ar</item>
    <item>de</item>
    <item>en</item>
    <item>es</item>
    <item>fa</item>
    ...
    <item>zh</item> <!-- Traditional Chinese -->
    <item>zh-rCN</item> <!-- Simplified Chinese -->
</string-array>

Here is key points:

  • Use config.setLayoutDirection(toLocale); to change layout direction when you use RTL locales like Arabic, Persian, etc.
  • "sys" in the code is a value that means "inherit system default language".
  • Here "langPref" is a key of preference where you put user current language.
  • There is no need to recreate the context if it already uses needed locale.
  • There is no need for ContextWraper as posted here, just set new context returned from createConfigurationContext as baseContext
  • This is very important! When you call createConfigurationContext you should pass configuration crated from scratch and only with Locale property set. There shouldn't be any other property set to this configuration. Because if we set some other properties for this config (orientation for example), we override that property forever, and our context no longer change this orientation property even if we rotate the screen.
  • It is not enough only to recreate activity when user selects a different language, because applicationContext will remain with old locale and it could provide unexpected behaviour. So listen to preference change and restart whole application task instead:

fun Context.recreateTask() {
    this.packageManager
        .getLaunchIntentForPackage(context.packageName)
        ?.let { intent ->
            val restartIntent = Intent.makeRestartActivityTask(intent.component)
            this.startActivity(restartIntent)
            Runtime.getRuntime().exit(0)
         }
}

Solution 5 - Java

The above answers set me on the right track but left a couple of issues

  1. On android 7 and 9 I could happily change to any language other than the app default. When I changed back to the app default language it showed the last language selected - not surprising as this has overridden the default (although interestingly this wasn't an issue on Android 8!).
  2. For RTL languages it didn't update the layouts to RTL

To fix the first item I stored the default locale on app start.

Note If your default language is set to "en" then locales of "enGB" or "enUS" both need to match the default locale (unless you provide seperate localisations for them). Similarly in the example below if the user's phone locale is arEG (Arabic Egypt) then the defLanguage needs to be "ar" not "arEG"

private Locale defLocale = Locale.getDefault();
private Locale locale = Locale.getDefault();
public static myApplication myApp;
public static Resources res;
private static String defLanguage = Locale.getDefault().getLanguage() + Locale.getDefault().getCountry();
private static sLanguage = "en";
private static final Set<String> SUPPORTEDLANGUAGES = new HashSet<>(Arrays.asList(new String[]{"en", "ar", "arEG"})); 

@Override
protected void attachBaseContext(Context base) {
  if (myApp == null) myApp = this;
  if (base == null) super.attachBaseContext(this);
  else super.attachBaseContext(setLocale(base));
}

@Override
public void onCreate() {
  myApp = this;

  if (!SUPPORTEDLANGUAGES.contains(test)) {
    // The default locale (eg enUS) is not in the supported list - lets see if the language is
    if (SUPPORTEDLANGUAGES.contains(defLanguage.substring(0,2))) {
      defLanguage = defLanguage.substring(0,2);
    }
  }
}

private static void setLanguage(String sLang) {
  Configuration baseCfg = myApp.getBaseContext().getResources().getConfiguration();
  if ( sLang.length() > 2 ) {
    String s[] = sLang.split("_");
    myApp.locale = new Locale(s[0],s[1]);
    sLanguage = s[0] + s[1];
  }
  else {
    myApp.locale = new Locale(sLang);
    sLanguage = sLang;
  }
}

public static Context setLocale(Context ctx) {
  Locale.setDefault(myApp.locale);
  Resources tempRes = ctx.getResources();
  Configuration config = tempRes.getConfiguration();

  if (Build.VERSION.SDK_INT >= 24) {
    // If changing to the app default language, set locale to the default locale
    if (sLanguage.equals(myApp.defLanguage)) {
      config.setLocale(myApp.defLocale);
      // restored the default locale as well
      Locale.setDefault(myApp.defLocale);
    }
    else config.setLocale(myApp.locale);

    ctx = ctx.createConfigurationContext(config);

    // update the resources object to point to the current localisation
    res = ctx.getResources();
  } else {
    config.locale = myApp.locale;
    tempRes.updateConfiguration(config, tempRes.getDisplayMetrics());
  }

  return ctx;
}

To fix the RTL issues I extended AppCompatActivity as per Fragments comments in this answer

public class myCompatActivity extends AppCompatActivity {
  @Override
  protected void attachBaseContext(Context base) {
    super.attachBaseContext(myApplication.setLocale(base));
  }

  @Override
  protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    if (Build.VERSION.SDK_INT >= 17) {
      getWindow().getDecorView().setLayoutDirection(myApplication.isRTL() ?
              View.LAYOUT_DIRECTION_RTL : View.LAYOUT_DIRECTION_LTR);
    }
  }
}

Solution 6 - Java

This is my code and it works! Please let me know, if there are problems:

protected void attachBaseContext(Context newBase) {
    String lang = "en"; // your language or load from SharedPref
    Locale locale = new Locale(lang);
    Configuration config = new Configuration(newBase.getResources().getConfiguration());
    Locale.setDefault(locale);
    config.setLocale(locale);
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
        newBase = newBase.createConfigurationContext(config);
    } else {
        newBase.getResources().updateConfiguration(config, newBase.getResources().getDisplayMetrics());
    }
    super.attachBaseContext(newBase);
}

Solution 7 - Java

UPDATE SEP 2020

For Latest Androidx Appcombat Stable version 1.2.0, remove all the work arounds for 1.1.0 and add this

package androidx.appcompat.app

import android.content.Context
import android.content.res.Configuration
import android.os.Bundle
import android.util.AttributeSet
import android.view.MenuInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.view.ActionMode
import androidx.appcompat.widget.Toolbar

class BaseContextWrappingDelegate(private val superDelegate: 
AppCompatDelegate) : AppCompatDelegate() {

override fun getSupportActionBar() = superDelegate.supportActionBar

override fun setSupportActionBar(toolbar: Toolbar?) = superDelegate.setSupportActionBar(toolbar)

override fun getMenuInflater(): MenuInflater? = superDelegate.menuInflater

override fun onCreate(savedInstanceState: Bundle?) {
    superDelegate.onCreate(savedInstanceState)
    removeActivityDelegate(superDelegate)
    addActiveDelegate(this)
}

override fun onPostCreate(savedInstanceState: Bundle?) = superDelegate.onPostCreate(savedInstanceState)

override fun onConfigurationChanged(newConfig: Configuration?) = superDelegate.onConfigurationChanged(newConfig)

override fun onStart() = superDelegate.onStart()

override fun onStop() = superDelegate.onStop()

override fun onPostResume() = superDelegate.onPostResume()

override fun setTheme(themeResId: Int) = superDelegate.setTheme(themeResId)

override fun <T : View?> findViewById(id: Int) = superDelegate.findViewById<T>(id)

override fun setContentView(v: View?) = superDelegate.setContentView(v)

override fun setContentView(resId: Int) = superDelegate.setContentView(resId)

override fun setContentView(v: View?, lp: ViewGroup.LayoutParams?) = superDelegate.setContentView(v, lp)

override fun addContentView(v: View?, lp: ViewGroup.LayoutParams?) = superDelegate.addContentView(v, lp)

override fun attachBaseContext2(context: Context) = wrap(superDelegate.attachBaseContext2(super.attachBaseContext2(context)))

override fun setTitle(title: CharSequence?) = superDelegate.setTitle(title)

override fun invalidateOptionsMenu() = superDelegate.invalidateOptionsMenu()

override fun onDestroy() {
    superDelegate.onDestroy()
    removeActivityDelegate(this)
}

override fun getDrawerToggleDelegate() = superDelegate.drawerToggleDelegate

override fun requestWindowFeature(featureId: Int) = superDelegate.requestWindowFeature(featureId)

override fun hasWindowFeature(featureId: Int) = superDelegate.hasWindowFeature(featureId)

override fun startSupportActionMode(callback: ActionMode.Callback) = superDelegate.startSupportActionMode(callback)

override fun installViewFactory() = superDelegate.installViewFactory()

override fun createView(parent: View?, name: String?, context: Context, attrs: AttributeSet): View? = superDelegate.createView(parent, name, context, attrs)

override fun setHandleNativeActionModesEnabled(enabled: Boolean) {
    superDelegate.isHandleNativeActionModesEnabled = enabled
}

override fun isHandleNativeActionModesEnabled() = superDelegate.isHandleNativeActionModesEnabled

override fun onSaveInstanceState(outState: Bundle?) = superDelegate.onSaveInstanceState(outState)

override fun applyDayNight() = superDelegate.applyDayNight()

override fun setLocalNightMode(mode: Int) {
    superDelegate.localNightMode = mode
}

override fun getLocalNightMode() = superDelegate.localNightMode

private fun wrap(context: Context): Context {
    TODO("your wrapping implementation here")
}
}

Add your locale logics into function wrap(you can add the ContextWrapper in Above accepted Answer). This class must be inside the androidx.appcompat.app package because the only existing AppCompatDelegate constructor is package private

Then inside your base activity class you remove all your 1.1.0 workarounds and simply add this

private var baseContextWrappingDelegate: AppCompatDelegate? = null

override fun getDelegate() = baseContextWrappingDelegate ?: 
BaseContextWrappingDelegate(super.getDelegate()).apply {
baseContextWrappingDelegate = this
}

configuration changes might break locale changes. To fix that

override fun createConfigurationContext(overrideConfiguration: Configuration) 
: Context {
val context = super.createConfigurationContext(overrideConfiguration)
TODO("your wrapping implementation here")
}

That's it. You are good to go with latest 1.2.0 appCombat

Solution 8 - Java

This works for me, I'm using androidx.appcompat:appcompat:1.2.0

 override fun attachBaseContext(newBase: Context?) {
            val sp = PreferenceManager.getDefaultSharedPreferences(newBase)
            val locale = when(sp.getString("app_language", "")) {
                "en" -> { Locale("en") }
                "hu" -> { Locale("hu") }
                else -> {
                    if (Build.VERSION.SDK_INT >= 24) {
                        Resources.getSystem().configuration.locales.get(0);
                    }
                    else {
                        Resources.getSystem().configuration.locale
                    }
                }
            }
            if(newBase != null) {
                Locale.setDefault(locale)
                newBase.resources.configuration.setLocale(locale)
                applyOverrideConfiguration(newBase.resources.configuration)
            }
            super.attachBaseContext(newBase)
        }

Solution 9 - Java

UPDATE NOV 2020

Hi everyone, I just want to share my experience. Couple of days ago, I started to get reports about an issue in Android N devices that language is not changing from the setting of my app. I searched a lot and after trying multiple changes in my code, I came to know that their was no issue in my code and issue was causing due to androidx constraint Layout gradle dependency version 2.0.0 and after downgrading it to 1.1.3, language issue was resolved. I got my issue resolved using this version of ConstraintLayout library.

implementation 'androidx.constraintlayout:constraintlayout:1.1.3'

Solution 10 - Java

In my case, the Xamarin.Android:

Create ContextWrapper:

 public class LanguageContextWrapper : Android.Content.ContextWrapper
 {
   public LanguageContextWrapper(Context @base) : base(@base)
   {
    
   }

   public static ContextWrapper Wrap(Context context, string newLocale)
   {
     Locale.Default = new Locale(newLocale);
     Configuration config = new Configuration();
     config.SetLocale(Locale.Default);
     context = context.CreateConfigurationContext(config);

     return new ContextWrapper(context);
    }
 }

and use in all activity:

protected override void AttachBaseContext(Context newBase)
{                      
    Context context = LanguageContextWrapper.Wrap(newBase, "en"); //need use short name of locale language
    
    base.AttachBaseContext(context);          
}

And work in android 10,11,12 I have not checked below.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionKuvaView Question on Stackoverflow
Solution 1 - JavaKuvaView Answer on Stackoverflow
Solution 2 - JavathyzzView Answer on Stackoverflow
Solution 3 - JavaAli NadalizadehView Answer on Stackoverflow
Solution 4 - JavaOleksandr AlbulView Answer on Stackoverflow
Solution 5 - JavaQuantumTigerView Answer on Stackoverflow
Solution 6 - JavaMrRackoonView Answer on Stackoverflow
Solution 7 - JavaJackson ChengalaiView Answer on Stackoverflow
Solution 8 - JavaBotond SulyokView Answer on Stackoverflow
Solution 9 - JavaUmair MustafaView Answer on Stackoverflow
Solution 10 - JavaMichaƂ SchmudeView Answer on Stackoverflow