Android N change language programmatically

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.


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.


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
}

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.


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

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