3

Regardless the device OS language, My need is to force the app locale to fit my needs which are: translated strings, and layout rtl/ltr to be per app variant.
I would like to know how to correctly do this. as we can see there is a lot of confusion on the web, and no formal answer for doing it.

1 Answer 1

2

The most correct approach to do so is to firstable understand the concept of:

  1. Locale
  2. Configuration
  3. Resources
  4. BaseContext and ContextWrapper

Note that: There are 3 types of [Context.getResources] layers:

  1. Top-level resources (ex: manifest activity name)
  2. Application resources
  3. Activity resources

So, changing the Application layer won't affect the activity layer.

And:
If you wish to support Android 6 you should use .apk file and not .aab (android app bundle). This is because in Android 6 you can only choose one default locale in settings, and then the .aab will download ONLY the required configuration resources:
Let us assume we use strings.xml in English as our default language, and translate it into Hebrew using other strings-iw.xml
If the Android 6 device is configured on English as its primary and only language, .aab will only deliver the regular English strings.xml resource. OR:
If it is possible, you can use only one default strings.xml with the required language.

The solution:

In your BaseActivity and also on your Application class:

abstract class BaseActivity : AppCompatActivity() { override fun attachBaseContext(newBase: Context) { val constrainedBaseCtx = LocaleUtil.constrainConfigurationLocale(newBase) super.attachBaseContext(constrainedBaseCtx) } override fun onConfigurationChanged(newConfig: Configuration) { val constrainedConfiguration = LocaleUtil.constrainConfigurationLocale(newConfig) super.onConfigurationChanged(constrainedConfiguration) } override fun createConfigurationContext(overrideConfiguration: Configuration): Context { val constrainedConf = LocaleUtil.constrainConfigurationLocale(overrideConfiguration) return super.createConfigurationContext(constrainedConf) } } 

Use your Locale Util:

/** * Helps to change locales configuration for [Context] objects. * Remember that there are 3 types of [Context.getResources] layers: * * 1. Top-level resources (ex: manifest activity name) * * 2. Application resources * * 3. Activity resources * * So, changing the Application layer won't affect the activity layer. */ object LocaleUtil { const val DEFAULT_LANGUAGE = "iw" const val DEFAULT_COUNTRY = "il" /** * Constraint This [context] Locale. * @param constrainedCountry - the country to match the activity for * @param constrainedLanguage - the language inside that country to match the activity for * @return new / same instance configured [Context]. (depends on Android OS version) */ fun constrainConfigurationLocale( context: Context, constrainedCountry: String = DEFAULT_COUNTRY, constrainedLanguage: String = DEFAULT_LANGUAGE ) : Context { val newConf = constrainConfigurationLocale( context.resources.configuration, constrainedCountry, constrainedLanguage ) return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) context.createConfigurationContext(newConf) else context } /** * Constraint This [configuration] Locale. * @param constrainedCountry - the country to match the activity for * @param constrainedLanguage - the language inside that country to match the activity for * @return new / same instance of [Configuration]. (depends on Android OS version) */ fun constrainConfigurationLocale( currentConfiguration: Configuration, constrainedCountry: String = DEFAULT_COUNTRY, constrainedLanguage: String = DEFAULT_LANGUAGE ) : Configuration { val configuration = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) Configuration(currentConfiguration) else currentConfiguration val synthesizedLocale = Locale(constrainedLanguage, constrainedCountry) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { val oldLocales = configuration.locales @Suppress("UNNECESSARY_NOT_NULL_ASSERTION") if (!oldLocales.isEmpty) { if (!oldLocales[0].language!!.contentEquals(synthesizedLocale.language)) { var newLocales = arrayOfNulls<Locale>(oldLocales.size() + 1) newLocales[0] = synthesizedLocale // first locale determines layout direction var deductionCount = 0 for (i in 0 until oldLocales.size()) { if (newLocales[0]?.language?.contentEquals(oldLocales[i].language) != true) // add only different locale if not null newLocales[i + 1 - deductionCount] = oldLocales[i] else { val temp = arrayOfNulls<Locale>(newLocales.size - 1) for (j in 0..i) { temp[j] = newLocales[j] } newLocales = temp deductionCount++ } } configuration.locales = LocaleList(*newLocales) } } else { configuration.locales = LocaleList(synthesizedLocale) } } else { @Suppress("DEPRECATION", "UNNECESSARY_NOT_NULL_ASSERTION") if (configuration.locale == null || !synthesizedLocale.language!!.contentEquals(configuration.locale.language!!)) configuration.setLocale(synthesizedLocale) } return configuration } } 
Sign up to request clarification or add additional context in comments.

1 Comment

Sometimes we receive ArrayIndexOutOfBoundsException at temp[j] = newLocales[j] so we replace this reordering logic with code from hidden LocaleList(Locale, LocaleList) constructor

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.