diff --git a/android-components/.buildconfig.yml b/android-components/.buildconfig.yml
index 1995fd01067b..d50e357367ca 100644
--- a/android-components/.buildconfig.yml
+++ b/android-components/.buildconfig.yml
@@ -256,6 +256,10 @@ projects:
path: components/support/rustlog
description: 'A bridge allowing log messages from Rust code to be sent to the log system in support-base'
publish: true
+ support-locale:
+ path: components/support/locale
+ description: 'A component to allow apps to change the system defined language by their custom one'
+ publish: true
lib-crash:
path: components/lib/crash
description: 'A generic crash reporter library that can report crashes to multiple services.'
diff --git a/android-components/components/support/base/src/main/res/values/strings.xml b/android-components/components/support/base/src/main/res/values/strings.xml
new file mode 100644
index 000000000000..f542bd4e6856
--- /dev/null
+++ b/android-components/components/support/base/src/main/res/values/strings.xml
@@ -0,0 +1,7 @@
+
+
+
+ mozac_support_base_locale_preference_key_locale
+
diff --git a/android-components/components/support/locale/README.md b/android-components/components/support/locale/README.md
new file mode 100644
index 000000000000..87dfee0eb4fb
--- /dev/null
+++ b/android-components/components/support/locale/README.md
@@ -0,0 +1,19 @@
+# [Android Components](../../../README.md) > Support > Locale
+
+A component to allow apps to change the system defined language by their custom one.
+
+## Usage
+
+### Setting up the dependency
+
+Use Gradle to download the library from [maven.mozilla.org](https://maven.mozilla.org/) ([Setup repository](../../../README.md#maven-repository)):
+
+```Groovy
+implementation "org.mozilla.components:support-locale:{latest-version}"
+```
+
+## License
+
+ This Source Code Form is subject to the terms of the Mozilla Public
+ License, v. 2.0. If a copy of the MPL was not distributed with this
+ file, You can obtain one at http://mozilla.org/MPL/2.0/
diff --git a/android-components/components/support/locale/build.gradle b/android-components/components/support/locale/build.gradle
new file mode 100644
index 000000000000..4ece4c4b10e5
--- /dev/null
+++ b/android-components/components/support/locale/build.gradle
@@ -0,0 +1,42 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+
+android {
+ compileSdkVersion config.compileSdkVersion
+
+ defaultConfig {
+ minSdkVersion config.minSdkVersion
+ targetSdkVersion config.targetSdkVersion
+
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+}
+
+dependencies {
+ implementation Dependencies.kotlin_stdlib
+ implementation Dependencies.androidx_core
+ implementation Dependencies.androidx_core_ktx
+
+ implementation project(':support-base')
+ implementation project(':support-utils')
+
+ testImplementation project(':support-test')
+ testImplementation Dependencies.androidx_test_core
+ testImplementation Dependencies.androidx_test_junit
+ testImplementation Dependencies.testing_robolectric
+ testImplementation Dependencies.testing_mockito
+}
+
+apply from: '../../../publish.gradle'
+ext.configurePublish(config.componentsGroupId, archivesBaseName, project.ext.description)
diff --git a/android-components/components/support/locale/gradle.properties b/android-components/components/support/locale/gradle.properties
new file mode 100644
index 000000000000..ceb2d0421a06
--- /dev/null
+++ b/android-components/components/support/locale/gradle.properties
@@ -0,0 +1,2 @@
+# TODO remove
+android.enableUnitTestBinaryResources=false
diff --git a/android-components/components/support/locale/proguard-rules.pro b/android-components/components/support/locale/proguard-rules.pro
new file mode 100644
index 000000000000..f1b424510da5
--- /dev/null
+++ b/android-components/components/support/locale/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
diff --git a/android-components/components/support/locale/src/main/AndroidManifest.xml b/android-components/components/support/locale/src/main/AndroidManifest.xml
new file mode 100644
index 000000000000..d1e7fe4ada3d
--- /dev/null
+++ b/android-components/components/support/locale/src/main/AndroidManifest.xml
@@ -0,0 +1,5 @@
+
+
diff --git a/android-components/components/support/locale/src/main/java/mozilla/components/support/locale/Extensions.kt b/android-components/components/support/locale/src/main/java/mozilla/components/support/locale/Extensions.kt
new file mode 100644
index 000000000000..e62cc2b187f8
--- /dev/null
+++ b/android-components/components/support/locale/src/main/java/mozilla/components/support/locale/Extensions.kt
@@ -0,0 +1,22 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package mozilla.components.support.locale
+
+import java.util.Locale
+
+fun String.toLocale(): Locale {
+ val index: Int = if (contains('-')) {
+ indexOf('-')
+ } else {
+ indexOf('_')
+ }
+ return if (index != -1) {
+ val langCode = substring(0, index)
+ val countryCode = substring(index + 1)
+ Locale(langCode, countryCode)
+ } else {
+ Locale(this)
+ }
+}
diff --git a/android-components/components/support/locale/src/main/java/mozilla/components/support/locale/LocaleAwareAppCompatActivity.kt b/android-components/components/support/locale/src/main/java/mozilla/components/support/locale/LocaleAwareAppCompatActivity.kt
new file mode 100644
index 000000000000..2c3ef7377051
--- /dev/null
+++ b/android-components/components/support/locale/src/main/java/mozilla/components/support/locale/LocaleAwareAppCompatActivity.kt
@@ -0,0 +1,18 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package mozilla.components.support.locale
+
+import android.content.Context
+import androidx.appcompat.app.AppCompatActivity
+
+/**
+ * Base activity for apps that want to customized the system defined language by their own.
+ */
+open class LocaleAwareAppCompatActivity : AppCompatActivity() {
+ override fun attachBaseContext(base: Context) {
+ val context = LocaleManager.updateResources(base)
+ super.attachBaseContext(context)
+ }
+}
diff --git a/android-components/components/support/locale/src/main/java/mozilla/components/support/locale/LocaleAwareApplication.kt b/android-components/components/support/locale/src/main/java/mozilla/components/support/locale/LocaleAwareApplication.kt
new file mode 100644
index 000000000000..cc565e41cf06
--- /dev/null
+++ b/android-components/components/support/locale/src/main/java/mozilla/components/support/locale/LocaleAwareApplication.kt
@@ -0,0 +1,25 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package mozilla.components.support.locale
+
+import android.app.Application
+import android.content.Context
+import android.content.res.Configuration
+
+/**
+ * Base application for apps that want to customized the system defined language by their own.
+ */
+open class LocaleAwareApplication : Application() {
+
+ override fun attachBaseContext(base: Context) {
+ val context = LocaleManager.updateResources(base)
+ super.attachBaseContext(context)
+ }
+
+ override fun onConfigurationChanged(config: Configuration) {
+ super.onConfigurationChanged(config)
+ LocaleManager.updateResources(this)
+ }
+}
diff --git a/android-components/components/support/locale/src/main/java/mozilla/components/support/locale/LocaleManager.kt b/android-components/components/support/locale/src/main/java/mozilla/components/support/locale/LocaleManager.kt
new file mode 100644
index 000000000000..28965085578e
--- /dev/null
+++ b/android-components/components/support/locale/src/main/java/mozilla/components/support/locale/LocaleManager.kt
@@ -0,0 +1,109 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package mozilla.components.support.locale
+
+import android.content.Context
+import android.content.SharedPreferences
+import android.content.res.Configuration
+import androidx.appcompat.app.AppCompatActivity
+import mozilla.components.support.base.R
+import mozilla.components.support.base.log.logger.Logger
+import java.util.Locale
+
+/**
+ * Helper for apps that want to change locale defined by the system.
+ */
+object LocaleManager {
+ private val logger = Logger("LocaleManager")
+
+ /**
+ * Change the system defined locale to the indicated in the [language] parameter.
+ * This new [language] will be stored and will be the new current locale returned by [getCurrentLocale].
+ *
+ * After calling this function, to visualize the locale changes you have to make sure all your visible activities
+ * get recreated. If your app is using the single activity approach, this will be trivial just call
+ * [AppCompatActivity.recreate]. On the other hand, if you have multiple activity this could be tricky, one
+ * alternative could be restarting your application process see https://github.com/JakeWharton/ProcessPhoenix
+ * @return A new Context object for whose resources are adjusted to match the new [language].
+ */
+ fun setNewLocale(context: Context, language: String): Context {
+ Storage.save(context, language)
+ return updateResources(context)
+ }
+
+ /**
+ * The latest stored locale saved by [setNewLocale].
+ */
+ fun getCurrentLocale(context: Context): Locale? {
+ var currentLocale: Locale? = null
+
+ if (currentLocale == null) {
+ val locale = Storage.getLocale(context)
+ if (locale != null) {
+ currentLocale = locale.toLocale()
+ }
+ }
+ return currentLocale
+ }
+
+ internal fun updateResources(baseContext: Context): Context {
+ val locale = getCurrentLocale(baseContext)
+ return if (locale != null) {
+ updateSystemLocale(locale)
+ updateConfiguration(baseContext, locale)
+ } else {
+ baseContext
+ }
+ }
+
+ private fun updateConfiguration(context: Context, locale: Locale): Context {
+ val configuration = Configuration(context.resources.configuration)
+ configuration.setLocale(locale)
+ configuration.setLayoutDirection(locale)
+ return context.createConfigurationContext(configuration)
+ }
+
+ private fun updateSystemLocale(locale: Locale) {
+ Locale.setDefault(locale)
+ }
+
+ internal fun clear(context: Context) {
+ Storage.clear(context)
+ }
+
+ private object Storage {
+ private const val PREFERENCE_FILE = "mozac_support_base_locale_manager_preference"
+ private var currentLocal: String? = null
+
+ fun getLocale(context: Context): String? {
+ return if (currentLocal == null) {
+ val settings = getSharedPreferences(context)
+ val key = context.getString(R.string.mozac_support_base_locale_preference_key_locale)
+ currentLocal = settings.getString(key, null)
+ currentLocal
+ } else {
+ currentLocal
+ }
+ }
+
+ @Synchronized
+ fun save(context: Context, localeCode: String) {
+ val settings = getSharedPreferences(context)
+ val key = context.getString(R.string.mozac_support_base_locale_preference_key_locale)
+ settings.edit().putString(key, localeCode).apply()
+ currentLocal = localeCode
+ }
+
+ fun clear(context: Context) {
+ val settings = getSharedPreferences(context)
+ settings.edit().clear().apply()
+ currentLocal = null
+ }
+
+ private fun getSharedPreferences(context: Context): SharedPreferences {
+ return context.getSharedPreferences(PREFERENCE_FILE, Context.MODE_PRIVATE)
+ }
+ }
+}
diff --git a/android-components/components/support/locale/src/test/java/mozilla/components/support/locale/LocaleManagerTest.kt b/android-components/components/support/locale/src/test/java/mozilla/components/support/locale/LocaleManagerTest.kt
new file mode 100644
index 000000000000..b5a96803b9a9
--- /dev/null
+++ b/android-components/components/support/locale/src/test/java/mozilla/components/support/locale/LocaleManagerTest.kt
@@ -0,0 +1,51 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package mozilla.components.support.locale
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotEquals
+import org.junit.Assert.assertNull
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.util.Locale
+
+@RunWith(AndroidJUnit4::class)
+class LocaleManagerTest {
+
+ @Before
+ fun setup() {
+ LocaleManager.clear(testContext)
+ Locale.setDefault("en_US".toLocale())
+ }
+
+ @Test
+ fun `changing the language to Spanish must change the system locale to Spanish and change the configurations`() {
+
+ var currentLocale = LocaleManager.getCurrentLocale(testContext)
+
+ assertNull(currentLocale)
+
+ val newContext = LocaleManager.setNewLocale(testContext, "es")
+
+ assertNotEquals(testContext, newContext)
+
+ currentLocale = LocaleManager.getCurrentLocale(testContext)
+
+ assertEquals(currentLocale, "es".toLocale())
+ assertEquals(currentLocale, Locale.getDefault())
+ }
+
+ @Test
+ fun `when calling updateResources with none current language must not change the system locale neither change configurations`() {
+ val previousSystemLocale = Locale.getDefault()
+ val context = LocaleManager.updateResources(testContext)
+
+ assertEquals(testContext, context)
+ assertEquals(previousSystemLocale, Locale.getDefault())
+ }
+}
\ No newline at end of file