Android N change language programmatically
JavaAndroidAndroid 7.0-NougatJava 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:
-
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); }
}
-
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 fromcreateConfigurationContext
as baseContext - This is very important! When you call
createConfigurationContext
you should pass configuration crated from scratch and only withLocale
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
- 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!).
- 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.