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
The most correct approach to do so is to firstable understand the concept of:
Note that: There are 3 types of [Context.getResources] layers:
- Top-level resources (ex: manifest activity name)
- Application resources
- 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 } } 1 Comment
ArrayIndexOutOfBoundsException at temp[j] = newLocales[j] so we replace this reordering logic with code from hidden LocaleList(Locale, LocaleList) constructor