Android N меняет язык программно

Я обнаружил действительно странную ошибку, которая воспроизводится только на устройствах Android N.

В туре моего приложения есть возможность сменить язык. Вот код, который его меняет.

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

Этот код прекрасно работает в активности моего тура (с recreate() звоните), но во всех следующих действиях все строковые ресурсы неверны. Поворот экрана исправляет это. Что я могу сделать с этой проблемой? Должен ли я изменить локаль для Android N по-другому, или это просто системная ошибка?

PS Вот что я нашел. При первом запуске MainActivity (после моего тура) Locale.getDefault() это правильно, но ресурсы не так. Но в других действиях это дает мне неправильную локаль и неправильные ресурсы из этой локали. После поворота экрана (или, возможно, другого изменения конфигурации) Locale.getDefault() верно.

10 ответов

Решение

Хорошо. Наконец мне удалось найти решение.

Сначала вы должны знать, что в 25 API Resources.updateConfiguration(...) устарела. Так что вместо этого вы можете сделать что-то вроде этого:

1) Вам нужно создать свой собственный ContextWrapper, который будет переопределять все параметры конфигурации в baseContext. Например, это мой ContextWrapper, который корректно меняет локаль. Обратите внимание на context.createConfigurationContext(configuration) метод.

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) Вот что вы должны сделать в своей 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);
}

Замечания:

Не забудьте воссоздать свою деятельность, если вы хотите где-то изменить язык в вашем приложении. Вы можете переопределить любую конфигурацию, которую хотите, используя это решение.

Вдохновленный различными кодами (т.е. нашими братьями из Stackru (кричите, ребята)), я создал гораздо более простую версию. ContextWrapper расширение не нужно.

Во-первых, скажем, у вас есть 2 кнопки для 2 языков, EN и KH. В onClick для кнопок сохранить код языка в SharedPreferences, а затем вызвать деятельность recreate() метод.

Пример:

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

Затем создайте статический метод, который возвращает ContextWrapperвозможно в классе Utils (потому что это то, что я сделал, 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);
}

Наконец, загрузите код языка из SharedPreferences во всей деятельности attachBaseContext(Context newBase) метод.

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

БОНУС: Чтобы сохранить пот ладони на клавиатуре, я создал LangSupportBaseActivity класс, который расширяет Activity и использовать последний кусок кода там. А у меня все остальные виды деятельности расширяются LangSupportBaseActivity,

Пример:

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
}

Начиная с Android 7.0+, некоторые части моего приложения больше не меняли свой язык. Даже с новыми методами, предложенными выше. Мне помогло обновление контекста приложения и активности. Вот пример Kotlin переопределений подкласса Activity:

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

Примечание: updateConfiguration устарел, но в любом случае createConfigurationContext для каждого Activity оставил некоторые строки без изменений.

Программно изменить языковой стандарт в приложении для Android довольно сложно. Я потратил много времени, чтобы найти рабочее решение, которое сейчас работает в продакшене.

Вам нужно переопределить контекст в каждом Activity но и в вашем Application class, иначе вы получите смешанные языки в ui.

Итак, вот мое решение, которое работает до API 29:

Подкласс ваш MainApplication класс от:

abstract class LocalApplication : Application() {

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

Также каждый Activity от:

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

добавлять LocaleExt.kt со следующими функциями расширения:

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
    }

Добавить в свой res/values/arrays.xml ваши поддерживаемые языки в массиве:

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

Вот ключевые моменты:

  • Использовать config.setLayoutDirection(toLocale); для изменения направления макета при использовании локалей RTL, таких как арабский, персидский и т. д.
  • "sys" в коде есть значение, которое означает "наследовать язык системы по умолчанию".
  • Здесь "langPref" - ​​это ключ предпочтения, в котором вы указываете текущий язык пользователя.
  • Нет необходимости воссоздавать контекст, если он уже использует необходимый языковой стандарт.
  • Нет необходимости в ContextWraper как написано здесь, просто установите новый контекст, возвращаемый из createConfigurationContext как baseContext
  • Это очень важно! Когда ты звонишьcreateConfigurationContextвы должны передать конфигурацию с нуля и только сLocaleнабор свойств. Для этой конфигурации не должно быть никаких других свойств. Потому что, если мы установим некоторые другие свойства для этой конфигурации (например,ориентацию), мы переопределим это свойство навсегда, и наш контекст больше не изменит это свойство ориентации, даже если мы повернем экран.
  • Недостаточно только recreateактивность, когда пользователь выбирает другой язык, потому что applicationContext останется со старой локалью, и это может привести к неожиданному поведению. Поэтому слушайте изменение предпочтений и вместо этого перезапустите всю задачу приложения:

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

Это мой код, и он работает! Пожалуйста, дайте мне знать, если возникнут проблемы:

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

Приведенные выше ответы поставили меня на правильный путь, но оставили пару вопросов

  1. На Android 7 и 9 я мог бы с радостью перейти на любой язык, кроме приложения по умолчанию. Когда я переключился обратно на язык приложения по умолчанию, он показывал последний выбранный язык - не удивительно, так как это переопределило значение по умолчанию (хотя интересно, что это не проблема для Android 8!).
  2. Для языков RTL он не обновлял макеты до RTL

Чтобы исправить первый элемент, я запустил локаль по умолчанию при запуске приложения.

Примечание. Если для языка по умолчанию задано значение "en", то локали "enGB" или "enUS" должны совпадать с локалью по умолчанию (если вы не предоставите для них отдельную локализацию). Аналогично, в приведенном ниже примере, если язык телефона пользователя arLY (арабская Ливия), тогда defLanguage должен быть "ar", а не "arLY"

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

Чтобы исправить проблемы RTL, я расширил AppCompatActivity согласно комментариям фрагментов в этом ответе.

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

Это работает для меня, я использую 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)
        }

ОБНОВЛЕНИЕ СЕНТЯБРЬ 2020

Для последней версии Androidx Appcombat Stable 1.2.0 удалите все обходные пути для 1.1.0 и добавьте это

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

Добавьте логику своей локали в оболочку функции (вы можете добавить ContextWrapper в вышеупомянутом принятом ответе). Этот класс должен находиться внутри пакета androidx.appcompat.app, потому что единственный существующий конструктор AppCompatDelegate является частным пакетом

private var baseContextWrappingDelegate: AppCompatDelegate? = null

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

изменения конфигурации могут нарушить изменения локали. Чтобы исправить это

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

Вот и все. Вы можете использовать последнюю версию приложения 1.2.0Combat

В моем случае Xamarin.Android:

Создайте контекстную оболочку:

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

и использовать во всех действиях:

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

А работу в андроиде 10,11,12 я ниже не проверял.

ОБНОВЛЕНИЕ НОЯБРЬ 2020

Всем привет, просто хочу поделиться своим опытом. Пару дней назад я начал получать сообщения о проблеме на устройствах Android N, когда язык не меняется в настройках моего приложения. Я много искал и, попробовав несколько изменений в своем коде, я узнал, что в моем коде не было проблем, и проблема была вызвана из-за ограничения androidx. Layout gradle dependency version 2.0.0 и после понижения до 1.1.3, язык проблема решена. Я решил проблему с помощью этой версии библиотеки ConstraintLayout.

implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
Другие вопросы по тегам