diff --git a/app/src/sharedTest/java/org/oppia/android/app/parser/HtmlParserTest.kt b/app/src/sharedTest/java/org/oppia/android/app/parser/HtmlParserTest.kt
index 571ba12ee85..1ac4f3d429b 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/parser/HtmlParserTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/parser/HtmlParserTest.kt
@@ -852,8 +852,10 @@ class HtmlParserTest {
}
}
- private fun createDisplayLocaleImpl(context: OppiaLocaleContext): DisplayLocaleImpl =
- DisplayLocaleImpl(context, machineLocale, androidLocaleFactory, formatterFactory)
+ private fun createDisplayLocaleImpl(context: OppiaLocaleContext): DisplayLocaleImpl {
+ val formattingLocale = androidLocaleFactory.createOneOffAndroidLocale(context)
+ return DisplayLocaleImpl(context, formattingLocale, machineLocale, formatterFactory)
+ }
private fun ActivityScenario.getDimensionPixelSize(
@DimenRes dimenResId: Int
diff --git a/app/src/test/java/org/oppia/android/app/parser/ListItemLeadingMarginSpanTest.kt b/app/src/test/java/org/oppia/android/app/parser/ListItemLeadingMarginSpanTest.kt
index d993ffd3de8..a7904d0d1cf 100644
--- a/app/src/test/java/org/oppia/android/app/parser/ListItemLeadingMarginSpanTest.kt
+++ b/app/src/test/java/org/oppia/android/app/parser/ListItemLeadingMarginSpanTest.kt
@@ -1044,8 +1044,10 @@ class ListItemLeadingMarginSpanTest {
return valueCaptor.value
}
- private fun createDisplayLocaleImpl(context: OppiaLocaleContext): DisplayLocaleImpl =
- DisplayLocaleImpl(context, machineLocale, androidLocaleFactory, formatterFactory)
+ private fun createDisplayLocaleImpl(context: OppiaLocaleContext): DisplayLocaleImpl {
+ val formattingLocale = androidLocaleFactory.createOneOffAndroidLocale(context)
+ return DisplayLocaleImpl(context, formattingLocale, machineLocale, formatterFactory)
+ }
private fun setUpTestApplicationComponent() {
ApplicationProvider.getApplicationContext().inject(this)
diff --git a/domain/src/main/java/org/oppia/android/domain/locale/LocaleController.kt b/domain/src/main/java/org/oppia/android/domain/locale/LocaleController.kt
index 51be2481321..0eeb999c8fe 100644
--- a/domain/src/main/java/org/oppia/android/domain/locale/LocaleController.kt
+++ b/domain/src/main/java/org/oppia/android/domain/locale/LocaleController.kt
@@ -62,7 +62,8 @@ class LocaleController @Inject constructor(
private val asyncDataSubscriptionManager: AsyncDataSubscriptionManager,
private val machineLocale: MachineLocale,
private val androidLocaleFactory: AndroidLocaleFactory,
- private val formatterFactory: OppiaBidiFormatter.Factory
+ private val formatterFactory: OppiaBidiFormatter.Factory,
+ private val androidLocaleProfileFactory: AndroidLocaleProfile.Factory
) {
private val definitionsLock = ReentrantLock()
private lateinit var supportedLanguages: SupportedLanguages
@@ -115,9 +116,8 @@ class LocaleController @Inject constructor(
* for cases in which the user changed their selected language).
*/
fun reconstituteDisplayLocale(oppiaLocaleContext: OppiaLocaleContext): DisplayLocale {
- return DisplayLocaleImpl(
- oppiaLocaleContext, machineLocale, androidLocaleFactory, formatterFactory
- )
+ val formattingLocale = androidLocaleFactory.createOneOffAndroidLocale(oppiaLocaleContext)
+ return DisplayLocaleImpl(oppiaLocaleContext, formattingLocale, machineLocale, formatterFactory)
}
/**
@@ -257,7 +257,7 @@ class LocaleController @Inject constructor(
private fun getSystemLocaleProfile(): DataProvider {
return dataProviders.createInMemoryDataProvider(ANDROID_SYSTEM_LOCALE_DATA_PROVIDER_ID) {
- AndroidLocaleProfile.createFrom(getSystemLocale())
+ androidLocaleProfileFactory.createFrom(getSystemLocale())
}
}
@@ -293,7 +293,7 @@ class LocaleController @Inject constructor(
@Suppress("DEPRECATION") // Old API is needed for SDK versions < N.
private fun getDefaultLocale(configuration: Configuration): Locale = configuration.locale
- private suspend fun computeLocaleResult(
+ private suspend inline fun computeLocaleResult(
language: OppiaLanguage,
systemLocaleProfile: AndroidLocaleProfile,
usageMode: LanguageUsageMode
@@ -302,7 +302,6 @@ class LocaleController @Inject constructor(
// internal weirdness that would lead to a wrong type being produced from the generic helpers.
// This shouldn't actually ever happen in practice, but this code gracefully fails to a null
// (and thus a failure).
- @Suppress("UNCHECKED_CAST") // as? should always be a safe cast, even if unchecked.
val locale = computeLocale(language, systemLocaleProfile, usageMode) as? T
return locale?.let {
AsyncResult.Success(it)
@@ -324,7 +323,13 @@ class LocaleController @Inject constructor(
retrieveLanguageDefinition(languageDefinition.fallbackMacroLanguage)?.let {
fallbackLanguageDefinition = it
}
- regionDefinition = retrieveRegionDefinition(systemLocaleProfile.regionCode)
+ val regionCode = when (systemLocaleProfile) {
+ is AndroidLocaleProfile.LanguageAndRegionProfile -> systemLocaleProfile.regionCode
+ is AndroidLocaleProfile.RegionOnlyProfile -> systemLocaleProfile.regionCode
+ is AndroidLocaleProfile.LanguageAndWildcardRegionProfile,
+ is AndroidLocaleProfile.LanguageOnlyProfile, is AndroidLocaleProfile.RootProfile -> ""
+ }
+ regionDefinition = retrieveRegionDefinition(regionCode)
this.usageMode = usageMode
}.build()
@@ -343,8 +348,10 @@ class LocaleController @Inject constructor(
}
return when (usageMode) {
- APP_STRINGS ->
- DisplayLocaleImpl(localeContext, machineLocale, androidLocaleFactory, formatterFactory)
+ APP_STRINGS -> {
+ val formattingLocale = androidLocaleFactory.createAndroidLocaleAsync(localeContext).await()
+ DisplayLocaleImpl(localeContext, formattingLocale, machineLocale, formatterFactory)
+ }
CONTENT_STRINGS, AUDIO_TRANSLATIONS -> ContentLocaleImpl(localeContext)
USAGE_MODE_UNSPECIFIED, UNRECOGNIZED -> null
}
@@ -413,9 +420,7 @@ class LocaleController @Inject constructor(
return@mapNotNull definition.retrieveAppLanguageProfile()?.let { profile ->
profile to definition
}
- }.find { (profile, _) ->
- localeProfile.matches(machineLocale, profile)
- }?.let { (_, definition) -> definition }
+ }.find { (profile, _) -> localeProfile.matches(profile) }?.let { (_, definition) -> definition }
}
private suspend fun retrieveRegionDefinition(countryCode: String): RegionSupportDefinition {
@@ -431,7 +436,7 @@ class LocaleController @Inject constructor(
} ?: RegionSupportDefinition.newBuilder().apply {
region = OppiaRegion.REGION_UNSPECIFIED
regionId = RegionSupportDefinition.IetfBcp47RegionId.newBuilder().apply {
- ietfRegionTag = countryCode
+ ietfRegionTag = machineLocale.run { countryCode.toMachineUpperCase() }
}.build()
}.build()
}
@@ -455,10 +460,10 @@ class LocaleController @Inject constructor(
private fun LanguageSupportDefinition.retrieveAppLanguageProfile(): AndroidLocaleProfile? {
return when (appStringId.languageTypeCase) {
LanguageSupportDefinition.LanguageId.LanguageTypeCase.IETF_BCP47_ID ->
- AndroidLocaleProfile.createFromIetfDefinitions(appStringId, regionDefinition = null)
+ androidLocaleProfileFactory.createFromIetfDefinitions(appStringId, regionDefinition = null)
LanguageSupportDefinition.LanguageId.LanguageTypeCase.MACARONIC_ID -> {
// Likely won't match against system languages.
- AndroidLocaleProfile.createFromMacaronicLanguage(appStringId)
+ androidLocaleProfileFactory.createFromMacaronicLanguage(appStringId)
}
LanguageSupportDefinition.LanguageId.LanguageTypeCase.LANGUAGETYPE_NOT_SET, null -> null
}
@@ -473,9 +478,12 @@ class LocaleController @Inject constructor(
// must be part of the language definitions. Support for app strings is exposed so that a locale
// can be constructed from it.
appStringId = LanguageSupportDefinition.LanguageId.newBuilder().apply {
- ietfBcp47Id = LanguageSupportDefinition.IetfBcp47LanguageId.newBuilder().apply {
- ietfLanguageTag = systemLocaleProfile.computeIetfLanguageTag()
- }.build()
+ // The root profile is assumed if there is no specific language ID to use.
+ if (systemLocaleProfile !is AndroidLocaleProfile.RootProfile) {
+ ietfBcp47Id = LanguageSupportDefinition.IetfBcp47LanguageId.newBuilder().apply {
+ ietfLanguageTag = systemLocaleProfile.ietfLanguageTag
+ }.build()
+ }
}.build()
}.build()
diff --git a/domain/src/test/java/org/oppia/android/domain/locale/LocaleControllerTest.kt b/domain/src/test/java/org/oppia/android/domain/locale/LocaleControllerTest.kt
index 30467ee6263..9a5eb36e8cc 100644
--- a/domain/src/test/java/org/oppia/android/domain/locale/LocaleControllerTest.kt
+++ b/domain/src/test/java/org/oppia/android/domain/locale/LocaleControllerTest.kt
@@ -114,12 +114,16 @@ class LocaleControllerTest {
}
@Test
- fun testReconstituteDisplayLocale_defaultContext_returnsDisplayLocaleForContext() {
+ fun testReconstituteDisplayLocale_defaultContext_throwsException() {
val context = OppiaLocaleContext.getDefaultInstance()
- val locale = localeController.reconstituteDisplayLocale(context)
+ val exception = assertThrows() {
+ localeController.reconstituteDisplayLocale(context)
+ }
- assertThat(locale.localeContext).isEqualToDefaultInstance()
+ // A default locale context isn't valid by itself (though it can represent the root locale when
+ // at least the app strings context is present & default).
+ assertThat(exception).hasMessageThat().contains("Invalid language case")
}
@Test
@@ -243,6 +247,25 @@ class LocaleControllerTest {
assertThat(context.regionDefinition.regionId.ietfRegionTag).isEqualTo("MC")
}
+ @Test
+ fun testAppStringLocale_rootLocale_defaultLang_returnsRootLocale() {
+ forceDefaultLocale(Locale.ROOT)
+
+ val localeProvider = localeController.retrieveAppStringDisplayLocale(LANGUAGE_UNSPECIFIED)
+
+ // The locale will be forced to the root locale. The root locale also should never provide an
+ // IETF BCP-47 ID.
+ val locale = monitorFactory.waitForNextSuccessfulResult(localeProvider)
+ val context = locale.localeContext
+ val languageDefinition = context.languageDefinition
+ assertThat(languageDefinition.language).isEqualTo(LANGUAGE_UNSPECIFIED)
+ assertThat(languageDefinition.fallbackMacroLanguage).isEqualTo(LANGUAGE_UNSPECIFIED)
+ assertThat(languageDefinition.appStringId.hasIetfBcp47Id()).isFalse()
+ assertThat(context.hasFallbackLanguageDefinition()).isFalse()
+ assertThat(context.regionDefinition.region).isEqualTo(REGION_UNSPECIFIED)
+ assertThat(context.regionDefinition.regionId.ietfRegionTag).isEmpty()
+ }
+
@Test
fun testAppStringLocale_newSystemLocale_doesNotNotifyProvider() {
forceDefaultLocale(Locale.US)
diff --git a/domain/src/test/java/org/oppia/android/domain/translation/TranslationControllerTest.kt b/domain/src/test/java/org/oppia/android/domain/translation/TranslationControllerTest.kt
index 195a53887db..b3b501ff94c 100644
--- a/domain/src/test/java/org/oppia/android/domain/translation/TranslationControllerTest.kt
+++ b/domain/src/test/java/org/oppia/android/domain/translation/TranslationControllerTest.kt
@@ -19,6 +19,7 @@ import org.oppia.android.app.model.AppLanguageSelection.SelectionTypeCase.USE_SY
import org.oppia.android.app.model.AudioTranslationLanguageSelection
import org.oppia.android.app.model.HtmlTranslationList
import org.oppia.android.app.model.LanguageSupportDefinition.LanguageId.LanguageTypeCase.IETF_BCP47_ID
+import org.oppia.android.app.model.LanguageSupportDefinition.LanguageId.LanguageTypeCase.LANGUAGETYPE_NOT_SET
import org.oppia.android.app.model.OppiaLanguage
import org.oppia.android.app.model.OppiaLanguage.ARABIC
import org.oppia.android.app.model.OppiaLanguage.BRAZILIAN_PORTUGUESE
@@ -112,9 +113,7 @@ class TranslationControllerTest {
val appStringId = context.languageDefinition.appStringId
assertThat(context.usageMode).isEqualTo(APP_STRINGS)
assertThat(context.languageDefinition.language).isEqualTo(LANGUAGE_UNSPECIFIED)
- assertThat(appStringId.languageTypeCase).isEqualTo(IETF_BCP47_ID)
- assertThat(appStringId.ietfBcp47Id.ietfLanguageTag).isEmpty()
- assertThat(appStringId.androidResourcesLanguageId.languageCode).isEmpty()
+ assertThat(appStringId.languageTypeCase).isEqualTo(LANGUAGETYPE_NOT_SET)
assertThat(context.regionDefinition.region).isEqualTo(REGION_UNSPECIFIED)
}
@@ -290,12 +289,28 @@ class TranslationControllerTest {
val appStringId = context.languageDefinition.appStringId
assertThat(context.usageMode).isEqualTo(APP_STRINGS)
assertThat(context.languageDefinition.language).isEqualTo(LANGUAGE_UNSPECIFIED)
- assertThat(appStringId.languageTypeCase).isEqualTo(IETF_BCP47_ID)
- assertThat(appStringId.ietfBcp47Id.ietfLanguageTag).isEmpty()
- assertThat(appStringId.androidResourcesLanguageId.languageCode).isEmpty()
+ assertThat(appStringId.languageTypeCase).isEqualTo(LANGUAGETYPE_NOT_SET)
assertThat(context.regionDefinition.region).isEqualTo(REGION_UNSPECIFIED)
}
+ @Test
+ fun testGetAppLanguageLocale_ptBrDefLocale_returnsLocaleWithIetfAndAndroidResourcesLangIds() {
+ forceDefaultLocale(BRAZIL_PORTUGUESE_LOCALE)
+
+ val localeProvider = translationController.getAppLanguageLocale(PROFILE_ID_0)
+
+ val locale = monitorFactory.waitForNextSuccessfulResult(localeProvider)
+ val context = locale.localeContext
+ val appStringId = context.languageDefinition.appStringId
+ assertThat(context.usageMode).isEqualTo(APP_STRINGS)
+ assertThat(context.languageDefinition.language).isEqualTo(BRAZILIAN_PORTUGUESE)
+ assertThat(appStringId.languageTypeCase).isEqualTo(IETF_BCP47_ID)
+ assertThat(appStringId.ietfBcp47Id.ietfLanguageTag).isEqualTo("pt-BR")
+ assertThat(appStringId.androidResourcesLanguageId.languageCode).isEqualTo("pt")
+ assertThat(appStringId.androidResourcesLanguageId.regionCode).isEqualTo("BR")
+ assertThat(context.regionDefinition.region).isEqualTo(BRAZIL)
+ }
+
@Test
fun testGetAppLanguageLocale_updateLanguageToEnglish_returnsEnglishLocale() {
forceDefaultLocale(Locale.ROOT)
@@ -1937,6 +1952,7 @@ class TranslationControllerTest {
private companion object {
private val BRAZIL_ENGLISH_LOCALE = Locale("en", "BR")
+ private val BRAZIL_PORTUGUESE_LOCALE = Locale("pt", "BR")
private val INDIA_HINDI_LOCALE = Locale("hi", "IN")
private val KENYA_KISWAHILI_LOCALE = Locale("sw", "KE")
diff --git a/scripts/assets/file_content_validation_checks.textproto b/scripts/assets/file_content_validation_checks.textproto
index 67116fbd5a2..f830caf8ab2 100644
--- a/scripts/assets/file_content_validation_checks.textproto
+++ b/scripts/assets/file_content_validation_checks.textproto
@@ -598,3 +598,10 @@ file_content_checks {
exempted_file_name: "scripts/src/javatests/org/oppia/android/scripts/regex/RegexPatternValidationCheckTest.kt"
exempted_file_name: "testing/src/test/java/org/oppia/android/testing/espresso/TextInputActionTest.kt"
}
+file_content_checks {
+ file_path_regex: ".+?\\.kt"
+ prohibited_content_regex: "computeIfAbsent\\("
+ failure_message: "computeIfAbsent won't desugar and requires Java 8 support (SDK 24+). Suggest using an atomic Kotlin-specific solution, instead."
+ exempted_file_name: "scripts/src/javatests/org/oppia/android/scripts/regex/RegexPatternValidationCheckTest.kt"
+ exempted_file_name: "utility/src/main/java/org/oppia/android/util/caching/testing/FakeAssetRepository.kt"
+}
diff --git a/scripts/src/javatests/org/oppia/android/scripts/regex/RegexPatternValidationCheckTest.kt b/scripts/src/javatests/org/oppia/android/scripts/regex/RegexPatternValidationCheckTest.kt
index a3eb9c825d9..f2b8b214649 100644
--- a/scripts/src/javatests/org/oppia/android/scripts/regex/RegexPatternValidationCheckTest.kt
+++ b/scripts/src/javatests/org/oppia/android/scripts/regex/RegexPatternValidationCheckTest.kt
@@ -218,6 +218,9 @@ class RegexPatternValidationCheckTest {
"ActivityScenarioRule can result in order dependence when static state leaks across tests" +
" (such as static module variables), and can make staging much more difficult for platform" +
" parameters. Use ActivityScenario directly, instead."
+ private val referenceComputeIfAbsent =
+ "computeIfAbsent won't desugar and requires Java 8 support (SDK 24+). Suggest using an atomic" +
+ " Kotlin-specific solution, instead."
private val wikiReferenceNote =
"Refer to https://github.com/oppia/oppia-android/wiki/Static-Analysis-Checks" +
"#regexpatternvalidation-check for more details on how to fix this."
@@ -2728,6 +2731,28 @@ class RegexPatternValidationCheckTest {
)
}
+ @Test
+ fun testFileContent_includesReferenceToComputeIfAbsent_fileContentIsNotCorrect() {
+ val prohibitedContent =
+ """
+ someMap.computeIfAbsent(key) { createOtherValue() }
+ """.trimIndent()
+ tempFolder.newFolder("testfiles", "app", "src", "main", "java", "org", "oppia", "android")
+ val stringFilePath = "app/src/main/java/org/oppia/android/TestPresenter.kt"
+ tempFolder.newFile("testfiles/$stringFilePath").writeText(prohibitedContent)
+
+ val exception = assertThrows() { runScript() }
+
+ assertThat(exception).hasMessageThat().contains(REGEX_CHECK_FAILED_OUTPUT_INDICATOR)
+ assertThat(outContent.toString().trim())
+ .isEqualTo(
+ """
+ $stringFilePath:1: $referenceComputeIfAbsent
+ $wikiReferenceNote
+ """.trimIndent()
+ )
+ }
+
/** Runs the regex_pattern_validation_check. */
private fun runScript() {
main(File(tempFolder.root, "testfiles").absolutePath)
diff --git a/utility/src/main/java/org/oppia/android/util/data/DataProviders.kt b/utility/src/main/java/org/oppia/android/util/data/DataProviders.kt
index 93049848c3c..4c11876168e 100644
--- a/utility/src/main/java/org/oppia/android/util/data/DataProviders.kt
+++ b/utility/src/main/java/org/oppia/android/util/data/DataProviders.kt
@@ -11,6 +11,7 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.transform
import kotlinx.coroutines.launch
+import org.oppia.android.util.data.DataProviders.Companion.transform
import org.oppia.android.util.logging.ExceptionLogger
import org.oppia.android.util.threading.BackgroundDispatcher
import java.util.concurrent.atomic.AtomicBoolean
@@ -74,7 +75,12 @@ class DataProviders @Inject constructor(
override fun getId(): Any = newId
override suspend fun retrieveData(): AsyncResult {
- return this@transformAsync.retrieveData().transformAsync(function)
+ return try {
+ this@transformAsync.retrieveData().transformAsync(function)
+ } catch (e: Exception) {
+ dataProviders.exceptionLogger.logException(e)
+ AsyncResult.Failure(e)
+ }
}
}
}
diff --git a/utility/src/main/java/org/oppia/android/util/locale/AndroidLocaleFactory.kt b/utility/src/main/java/org/oppia/android/util/locale/AndroidLocaleFactory.kt
index ffe3e186646..e871355442b 100644
--- a/utility/src/main/java/org/oppia/android/util/locale/AndroidLocaleFactory.kt
+++ b/utility/src/main/java/org/oppia/android/util/locale/AndroidLocaleFactory.kt
@@ -1,6 +1,10 @@
package org.oppia.android.util.locale
import android.os.Build
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.async
import org.oppia.android.app.model.LanguageSupportDefinition
import org.oppia.android.app.model.LanguageSupportDefinition.LanguageId
import org.oppia.android.app.model.LanguageSupportDefinition.LanguageId.LanguageTypeCase.IETF_BCP47_ID
@@ -9,8 +13,8 @@ import org.oppia.android.app.model.LanguageSupportDefinition.LanguageId.Language
import org.oppia.android.app.model.OppiaLocaleContext
import org.oppia.android.app.model.OppiaLocaleContext.LanguageUsageMode
import org.oppia.android.app.model.OppiaLocaleContext.LanguageUsageMode.APP_STRINGS
+import org.oppia.android.util.threading.BlockingDispatcher
import java.util.Locale
-import java.util.concurrent.ConcurrentHashMap
import javax.inject.Inject
import javax.inject.Singleton
@@ -20,9 +24,28 @@ import javax.inject.Singleton
*/
@Singleton
class AndroidLocaleFactory @Inject constructor(
- private val profileChooserSelector: ProposalChooser.Selector
+ private val profileChooserSelector: ProposalChooser.Selector,
+ @BlockingDispatcher private val blockingDispatcher: CoroutineDispatcher,
+ private val androidLocaleProfileFactory: AndroidLocaleProfile.Factory
) {
- private val memoizedLocales by lazy { ConcurrentHashMap() }
+ private val memoizedLocales = mutableMapOf()
+
+ /**
+ * Creates and returns a new [Locale] that matches the given [OppiaLocaleContext].
+ *
+ * See [createAndroidLocaleAsync] for specifics. Note that this function, unlike the async
+ * version, does **not** cache or try to load a pre-created [Locale] for the given context.
+ * Creating new [Locale]s can be expensive, so it's always preferred to use
+ * [createAndroidLocaleAsync] except in cases where that isn't an option.
+ */
+ fun createOneOffAndroidLocale(localeContext: OppiaLocaleContext): Locale {
+ val chooser = profileChooserSelector.findBestChooser(localeContext)
+ val primaryLocaleSource =
+ LocaleSource.createFromPrimary(localeContext, androidLocaleProfileFactory)
+ val fallbackLocaleSource =
+ LocaleSource.createFromFallback(localeContext, androidLocaleProfileFactory)
+ return chooser.findBestProposal(primaryLocaleSource, fallbackLocaleSource).computedLocale
+ }
/**
* Creates a new [Locale] that matches the given [OppiaLocaleContext].
@@ -50,18 +73,17 @@ class AndroidLocaleFactory @Inject constructor(
* - For other locale-based operations, the forced [Locale] will behave like the system's
* [Locale.ROOT].
*
+ * Note that the returned [Locale] may be cached within the factory for performance reasons, so
+ * the returned value uses a [Deferred] to ensure that this method can guarantee thread-safe
+ * access.
+ *
* @param localeContext the [OppiaLocaleContext] to use as a basis for finding a similar [Locale]
* @return the best [Locale] to match the provided [localeContext]
*/
- fun createAndroidLocale(localeContext: OppiaLocaleContext): Locale {
- // Note: computeIfAbsent is used here instead of getOrPut to ensure atomicity across multiple
- // threads calling into this create function.
- return memoizedLocales.computeIfAbsent(localeContext) {
- val chooser = profileChooserSelector.findBestChooser(localeContext)
- val primaryLocaleSource = LocaleSource.createFromPrimary(localeContext)
- val fallbackLocaleSource = LocaleSource.createFromFallback(localeContext)
- val proposal = chooser.findBestProposal(primaryLocaleSource, fallbackLocaleSource)
- return@computeIfAbsent proposal.computedLocale
+ fun createAndroidLocaleAsync(localeContext: OppiaLocaleContext): Deferred {
+ // A blocking dispatcher is used to ensure thread safety when updating the locales map.
+ return CoroutineScope(blockingDispatcher).async {
+ memoizedLocales.getOrPut(localeContext) { createOneOffAndroidLocale(localeContext) }
}
}
@@ -76,21 +98,17 @@ class AndroidLocaleFactory @Inject constructor(
/**
* A computed [Locale] that most closely represents the [AndroidLocaleProfile] of this proposal.
*/
- val computedLocale: Locale
- get() = Locale(profile.languageCode, profile.getNonWildcardRegionCode())
+ val computedLocale: Locale by lazy { profile.computeAndroidLocale() }
/**
* Determines whether the [AndroidLocaleProfile] of this proposal is a viable choice for using
* to compute a [Locale] (e.g. via [computedLocale]).
*
- * @param machineLocale the app's [OppiaLocale.MachineLocale]
- * @param systemProfiles [AndroidLocaleProfile]s representing the system's available locales
+ * @param localeProfileRepository the [LocaleProfileRepository]s representing the system's
+ * available locales
* @return whether this proposal has a viable profile for creating a [Locale]
*/
- abstract fun isViable(
- machineLocale: OppiaLocale.MachineLocale,
- systemProfiles: List
- ): Boolean
+ abstract fun isViable(localeProfileRepository: LocaleProfileRepository): Boolean
/**
* A [LocaleProfileProposal] that is only viable if its [profile] is among the available system
@@ -101,10 +119,9 @@ class AndroidLocaleFactory @Inject constructor(
val minAndroidSdkVersion: Int
) : LocaleProfileProposal() {
override fun isViable(
- machineLocale: OppiaLocale.MachineLocale,
- systemProfiles: List
+ localeProfileRepository: LocaleProfileRepository
): Boolean {
- return systemProfiles.any { it.matches(machineLocale, profile) } &&
+ return localeProfileRepository.availableLocaleProfiles.any { it.matches(profile) } &&
minAndroidSdkVersion <= Build.VERSION.SDK_INT
}
}
@@ -119,15 +136,8 @@ class AndroidLocaleFactory @Inject constructor(
override val profile: AndroidLocaleProfile,
val minAndroidSdkVersion: Int
) : LocaleProfileProposal() {
- override fun isViable(
- machineLocale: OppiaLocale.MachineLocale,
- systemProfiles: List
- ): Boolean = minAndroidSdkVersion <= Build.VERSION.SDK_INT
- }
-
- private companion object {
- private fun AndroidLocaleProfile.getNonWildcardRegionCode(): String =
- regionCode.takeIf { it != AndroidLocaleProfile.REGION_WILDCARD } ?: ""
+ override fun isViable(localeProfileRepository: LocaleProfileRepository): Boolean =
+ minAndroidSdkVersion <= Build.VERSION.SDK_INT
}
}
@@ -143,7 +153,8 @@ class AndroidLocaleFactory @Inject constructor(
class LocaleSource private constructor(
private val localeContext: OppiaLocaleContext,
private val definition: LanguageSupportDefinition,
- private val languageId: LanguageId
+ private val languageId: LanguageId,
+ private val androidLocaleProfileFactory: AndroidLocaleProfile.Factory
) {
private val regionDefinition by lazy {
localeContext.regionDefinition.takeIf { localeContext.hasRegionDefinition() }
@@ -156,7 +167,7 @@ class AndroidLocaleFactory @Inject constructor(
*/
fun computeSystemMatchingProposals(): List {
return listOfNotNull(
- computeLocaleProfileFromAndroidId()?.toSystemProposal(),
+ createAndroidResourcesProfile()?.toSystemProposal(),
createIetfProfile()?.toSystemProposal(),
createMacaronicProfile()?.toSystemProposal()
)
@@ -169,7 +180,7 @@ class AndroidLocaleFactory @Inject constructor(
* configured for this source's context.
*/
fun computeForcedAndroidProposal(): LocaleProfileProposal? =
- computeLocaleProfileFromAndroidId()?.toForcedProposal()
+ createAndroidResourcesProfile()?.toForcedProposal()
/**
* Returns a [LocaleProfileProposal] representing a [LocaleProfileProposal.ForcedProposal] that
@@ -177,37 +188,33 @@ class AndroidLocaleFactory @Inject constructor(
*
* Note that the returned proposal will prioritize its Android ID configuration over
* alternatives (such as IETF BCP 47 or a macaronic language configuration).
+ *
+ * @param fallBackToRootProfile whether to return a [AndroidLocaleProfile.RootProfile] for cases
+ * when a valid proposal cannot be determined rather than throwing an exception
*/
- fun computeForcedProposal(): LocaleProfileProposal =
- computeForcedAndroidProposal() ?: languageId.toForcedProposal()
-
- private fun computeLocaleProfileFromAndroidId(): AndroidLocaleProfile? {
- return languageId.androidResourcesLanguageId.takeIf {
- languageId.hasAndroidResourcesLanguageId() && it.languageCode.isNotEmpty()
- }?.let {
- // Empty region codes are allowed for Android resource IDs since they should always be used
- // verbatim to ensure the correct Android resource string can be computed (such as for macro
- // languages).
- AndroidLocaleProfile(
- it.languageCode,
- regionCode = it.regionCode.ifEmpty { AndroidLocaleProfile.REGION_WILDCARD }
- )
- }
- }
+ fun computeForcedProposal(fallBackToRootProfile: Boolean): LocaleProfileProposal =
+ computeForcedAndroidProposal() ?: toForcedProposal(fallBackToRootProfile)
- private fun LanguageId.toForcedProposal(): LocaleProfileProposal {
- return when (languageId.languageTypeCase) {
+ private fun toForcedProposal(fallBackToRootProfile: Boolean): LocaleProfileProposal {
+ return when (val languageTypeCase = languageId.languageTypeCase) {
IETF_BCP47_ID -> createIetfProfile().expectedProfile()
MACARONIC_ID -> createMacaronicProfile().expectedProfile()
- LANGUAGETYPE_NOT_SET, null -> error("Invalid language case: $languageTypeCase.")
+ LANGUAGETYPE_NOT_SET, null -> {
+ if (fallBackToRootProfile) {
+ AndroidLocaleProfile.RootProfile
+ } else error("Invalid language case: $languageTypeCase.")
+ }
}.toForcedProposal()
}
private fun createIetfProfile(): AndroidLocaleProfile? =
- AndroidLocaleProfile.createFromIetfDefinitions(languageId, regionDefinition)
+ androidLocaleProfileFactory.createFromIetfDefinitions(languageId, regionDefinition)
private fun createMacaronicProfile(): AndroidLocaleProfile? =
- AndroidLocaleProfile.createFromMacaronicLanguage(languageId)
+ androidLocaleProfileFactory.createFromMacaronicLanguage(languageId)
+
+ private fun createAndroidResourcesProfile(): AndroidLocaleProfile? =
+ androidLocaleProfileFactory.createFromAndroidResourcesLanguageId(languageId)
private fun AndroidLocaleProfile?.expectedProfile() = this ?: error("Invalid ID: $languageId.")
@@ -222,18 +229,31 @@ class AndroidLocaleFactory @Inject constructor(
* Return a new [LocaleSource] that maps to [localeContext]'s primary language configuration
* (i.e. fallback language details will be ignored).
*/
- fun createFromPrimary(localeContext: OppiaLocaleContext): LocaleSource =
- LocaleSource(localeContext, localeContext.languageDefinition, localeContext.getLanguageId())
+ fun createFromPrimary(
+ localeContext: OppiaLocaleContext,
+ androidLocaleProfileFactory: AndroidLocaleProfile.Factory
+ ): LocaleSource {
+ return LocaleSource(
+ localeContext,
+ localeContext.languageDefinition,
+ localeContext.getLanguageId(),
+ androidLocaleProfileFactory
+ )
+ }
/**
* Return a new [LocaleSource] that maps to [localeContext]'s fallback (secondary) language
* configuration (i.e. primary language details will be ignored).
*/
- fun createFromFallback(localeContext: OppiaLocaleContext): LocaleSource {
+ fun createFromFallback(
+ localeContext: OppiaLocaleContext,
+ androidLocaleProfileFactory: AndroidLocaleProfile.Factory
+ ): LocaleSource {
return LocaleSource(
localeContext,
localeContext.fallbackLanguageDefinition,
- localeContext.getFallbackLanguageId()
+ localeContext.getFallbackLanguageId(),
+ androidLocaleProfileFactory
)
}
}
@@ -288,15 +308,15 @@ class AndroidLocaleFactory @Inject constructor(
* system locales.
*/
class MatchedLocalePreferredChooser @Inject constructor(
- private val machineLocale: OppiaLocale.MachineLocale
+ private val localeProfileRepository: LocaleProfileRepository
) : ProposalChooser {
override fun findBestProposal(
primarySource: LocaleSource,
fallbackSource: LocaleSource
): LocaleProfileProposal {
- return primarySource.computeSystemMatchingProposals().findFirstViable(machineLocale)
- ?: fallbackSource.computeSystemMatchingProposals().findFirstViable(machineLocale)
- ?: primarySource.computeForcedProposal()
+ return primarySource.computeSystemMatchingProposals().findFirstViable(localeProfileRepository)
+ ?: fallbackSource.computeSystemMatchingProposals().findFirstViable(localeProfileRepository)
+ ?: primarySource.computeForcedProposal(fallBackToRootProfile = false)
}
}
@@ -308,31 +328,43 @@ class AndroidLocaleFactory @Inject constructor(
* [Locale]s produced by such profiles in order to correctly produce app UI strings.
*/
class AndroidResourceCompatibilityPreferredChooser @Inject constructor(
- private val machineLocale: OppiaLocale.MachineLocale
+ private val localeProfileRepository: LocaleProfileRepository
) : ProposalChooser {
override fun findBestProposal(
primarySource: LocaleSource,
fallbackSource: LocaleSource
): LocaleProfileProposal {
- return primarySource.computeSystemMatchingProposals().findFirstViable(machineLocale)
- ?: primarySource.computeForcedAndroidProposal()?.takeOnlyIfViable(machineLocale)
- ?: fallbackSource.computeSystemMatchingProposals().findFirstViable(machineLocale)
- ?: fallbackSource.computeForcedAndroidProposal()?.takeOnlyIfViable(machineLocale)
- ?: primarySource.computeForcedProposal()
+ // Note that defaulting to the root locale only makes sense for app strings (since app strings
+ // are picked based on the configured system locale).
+ return primarySource.computeSystemMatchingProposals().findFirstViable(localeProfileRepository)
+ ?: primarySource.computeForcedAndroidProposal()?.takeOnlyIfViable(localeProfileRepository)
+ ?: fallbackSource.computeSystemMatchingProposals().findFirstViable(localeProfileRepository)
+ ?: fallbackSource.computeForcedAndroidProposal()?.takeOnlyIfViable(localeProfileRepository)
+ ?: primarySource.computeForcedProposal(fallBackToRootProfile = true)
}
}
- private companion object {
- private val availableLocaleProfiles by lazy {
- Locale.getAvailableLocales().map(AndroidLocaleProfile::createFrom)
+ /**
+ * An application-injectable repository storing all possible [AndroidLocaleProfile]s available to
+ * use on the local system for the lifetime of the current app instance.
+ */
+ @Singleton
+ class LocaleProfileRepository @Inject constructor(
+ private val androidLocaleProfileFactory: AndroidLocaleProfile.Factory
+ ) {
+ /**
+ * All available [AndroidLocaleProfile]s that represent locales on the current running system.
+ */
+ val availableLocaleProfiles: List by lazy {
+ Locale.getAvailableLocales().map { androidLocaleProfileFactory.createFrom(it) }
}
+ }
- private fun List.findFirstViable(
- machineLocale: OppiaLocale.MachineLocale
- ) = firstOrNull { it.isViable(machineLocale, availableLocaleProfiles) }
+ private companion object {
+ private fun List.findFirstViable(repository: LocaleProfileRepository) =
+ firstOrNull { it.isViable(repository) }
- private fun LocaleProfileProposal.takeOnlyIfViable(
- machineLocale: OppiaLocale.MachineLocale
- ): LocaleProfileProposal? = takeIf { isViable(machineLocale, availableLocaleProfiles) }
+ private fun LocaleProfileProposal.takeOnlyIfViable(repository: LocaleProfileRepository) =
+ takeIf { isViable(repository) }
}
}
diff --git a/utility/src/main/java/org/oppia/android/util/locale/AndroidLocaleProfile.kt b/utility/src/main/java/org/oppia/android/util/locale/AndroidLocaleProfile.kt
index a410c2b06b6..9c96ec0f8fc 100644
--- a/utility/src/main/java/org/oppia/android/util/locale/AndroidLocaleProfile.kt
+++ b/utility/src/main/java/org/oppia/android/util/locale/AndroidLocaleProfile.kt
@@ -3,50 +3,156 @@ package org.oppia.android.util.locale
import org.oppia.android.app.model.LanguageSupportDefinition.LanguageId
import org.oppia.android.app.model.RegionSupportDefinition
import java.util.Locale
+import javax.inject.Inject
/**
* A profile to represent an Android [Locale] object which can be used to easily compare different
* locales (based on the properties the app cares about), or reconstruct a [Locale] object.
*
- * @property languageCode the IETF BCP 47 or ISO 639-2/3 language code
- * @property regionCode the IETF BCP 47 or ISO 3166 alpha-2 region code
+ * Subclasses of this sealed class operate on a language code and/or region code. The language code
+ * is an IETF BCP 47 or ISO 639-2/3 language code, or empty if unknown or not specified. The region
+ * code is an IETF BCP 47 or ISO 3166 alpha-2 region code, or empty if unknown or not specified.
+ *
+ * New instances should be created using [Factory].
*/
-data class AndroidLocaleProfile(val languageCode: String, val regionCode: String) {
+sealed class AndroidLocaleProfile {
+ /**
+ * An IETF BCP 47-esque language tag that represents this locale profile. For profiles that have
+ * valid IETF BCP 47 language & region codes, the returned tag should be a valid IETF BCP 47
+ * language tag.
+ */
+ abstract val ietfLanguageTag: String
+
/** Returns whether this profile matches the specified [otherProfile] for the given locale. */
- fun matches(
- machineLocale: OppiaLocale.MachineLocale,
- otherProfile: AndroidLocaleProfile,
- ): Boolean {
- return machineLocale.run {
- languageCode.equalsIgnoreCase(otherProfile.languageCode)
- } && machineLocale.run {
- val regionsAreEqual = regionCode.equalsIgnoreCase(otherProfile.regionCode)
- val eitherRegionIsWildcard =
- regionCode == REGION_WILDCARD || otherProfile.regionCode == REGION_WILDCARD
- return@run regionsAreEqual || eitherRegionIsWildcard
+ abstract fun matches(otherProfile: AndroidLocaleProfile): Boolean
+
+ /** Returns an Android [Locale] compatible with this profile. */
+ abstract fun computeAndroidLocale(): Locale
+
+ /**
+ * An [AndroidLocaleProfile] that provides both a non-empty language and region code.
+ *
+ * Note that, generally, this should never need to be created directly. Instead, [Factory] should
+ * be used to create new instances of profiles.
+ *
+ * @property languageCode the lowercase two-letter language code in this profile
+ * @property regionCode the lowercase two-letter region code in this profile
+ * @property regionCodeUpperCase the uppercase version of [regionCode]
+ */
+ data class LanguageAndRegionProfile(
+ val languageCode: String,
+ val regionCode: String,
+ private val regionCodeUpperCase: String
+ ) : AndroidLocaleProfile() {
+ // The region code is usually uppercase in IETF BCP-47 tags when extending a language code.
+ override val ietfLanguageTag = "$languageCode-$regionCodeUpperCase"
+
+ override fun matches(otherProfile: AndroidLocaleProfile): Boolean {
+ return when (otherProfile) {
+ is LanguageAndRegionProfile ->
+ languageCode == otherProfile.languageCode && regionCode == otherProfile.regionCode
+ is LanguageAndWildcardRegionProfile -> languageCode == otherProfile.languageCode
+ is LanguageOnlyProfile, is RegionOnlyProfile, is RootProfile -> false
+ }
}
+
+ override fun computeAndroidLocale(): Locale = Locale(languageCode, regionCode)
}
/**
- * Returns an IETF BCP 47-esque language tag that represents this locale profile. For profiles
- * that have valid IETF BCP 47 language & region codes, the returned tag should be a valid IETF
- * BCP 47 language tag.
+ * An [AndroidLocaleProfile] that provides only a non-empty region code.
+ *
+ * Note that, generally, this should never need to be created directly. Instead, [Factory] should
+ * be used to create new instances of profiles.
+ *
+ * @property regionCode the lowercase two-letter region code in this profile
*/
- fun computeIetfLanguageTag(): String {
- return when {
- languageCode.isNotEmpty() && regionCode.isNotEmptyOrWildcard() -> "$languageCode-$regionCode"
- regionCode.isNotEmptyOrWildcard() -> regionCode
- else -> languageCode
+ data class RegionOnlyProfile(val regionCode: String) : AndroidLocaleProfile() {
+ override val ietfLanguageTag = regionCode
+
+ override fun matches(otherProfile: AndroidLocaleProfile): Boolean =
+ otherProfile is RegionOnlyProfile && regionCode == otherProfile.regionCode
+
+ override fun computeAndroidLocale(): Locale = Locale(/* language = */ "", regionCode)
+ }
+
+ /**
+ * An [AndroidLocaleProfile] that provides only a non-empty language code.
+ *
+ * Note that, generally, this should never need to be created directly. Instead, [Factory] should
+ * be used to create new instances of profiles.
+ *
+ * @property languageCode the lowercase two-letter language code in this profile
+ */
+ data class LanguageOnlyProfile(val languageCode: String) : AndroidLocaleProfile() {
+ override val ietfLanguageTag = languageCode
+
+ override fun matches(otherProfile: AndroidLocaleProfile): Boolean {
+ return when (otherProfile) {
+ is LanguageOnlyProfile -> languageCode == otherProfile.languageCode
+ is LanguageAndWildcardRegionProfile -> languageCode == otherProfile.languageCode
+ is LanguageAndRegionProfile, is RegionOnlyProfile, is RootProfile -> false
+ }
+ }
+
+ override fun computeAndroidLocale(): Locale = Locale(languageCode)
+ }
+
+ /**
+ * An [AndroidLocaleProfile] that provides only a non-empty language code, but matches (e.g. via
+ * [matches]) with any profile that has the same language code.
+ *
+ * Note that, generally, this should never need to be created directly. Instead, [Factory] should
+ * be used to create new instances of profiles.
+ *
+ * @property languageCode the lowercase two-letter language code in this profile
+ */
+ data class LanguageAndWildcardRegionProfile(val languageCode: String) : AndroidLocaleProfile() {
+ override val ietfLanguageTag = languageCode
+
+ override fun matches(otherProfile: AndroidLocaleProfile): Boolean {
+ return when (otherProfile) {
+ is LanguageAndRegionProfile -> languageCode == otherProfile.languageCode
+ is LanguageAndWildcardRegionProfile -> languageCode == otherProfile.languageCode
+ is LanguageOnlyProfile -> languageCode == otherProfile.languageCode
+ is RegionOnlyProfile, is RootProfile -> false
+ }
}
+
+ override fun computeAndroidLocale(): Locale = Locale(languageCode)
}
- companion object {
- /** A wildcard that will match against any region when provided. */
- const val REGION_WILDCARD = "*"
+ /**
+ * An [AndroidLocaleProfile] that provides the system's root locale ([Locale.ROOT]).
+ *
+ * Note that, generally, this should never need to be used directly. Instead, [Factory] should be
+ * used to create new instances of profiles.
+ */
+ object RootProfile : AndroidLocaleProfile() {
+ override val ietfLanguageTag = ""
+
+ override fun matches(otherProfile: AndroidLocaleProfile): Boolean = otherProfile is RootProfile
+ override fun computeAndroidLocale(): Locale = Locale.ROOT
+ }
+
+ /** An application-injectable factory for creating new [AndroidLocaleProfile]s. */
+ class Factory @Inject constructor(private val machineLocale: OppiaLocale.MachineLocale) {
/** Returns a new [AndroidLocaleProfile] that represents the specified Android [Locale]. */
- fun createFrom(androidLocale: Locale): AndroidLocaleProfile =
- AndroidLocaleProfile(androidLocale.language, androidLocale.country)
+ fun createFrom(androidLocale: Locale): AndroidLocaleProfile {
+ val languageCode = androidLocale.language
+ val regionCode = androidLocale.country
+ return when {
+ languageCode.isNotEmpty() && regionCode.isNotEmpty() -> {
+ LanguageAndRegionProfile(
+ languageCode.asLowerCase(), regionCode.asLowerCase(), regionCode.asUpperCase()
+ )
+ }
+ regionCode.isNotEmpty() -> RegionOnlyProfile(regionCode.asLowerCase())
+ languageCode.isNotEmpty() -> LanguageOnlyProfile(languageCode.asLowerCase())
+ else -> RootProfile
+ }
+ }
/**
* Returns a new [AndroidLocaleProfile] using the IETF BCP 47 tag in the provided [LanguageId].
@@ -100,17 +206,48 @@ data class AndroidLocaleProfile(val languageCode: String, val regionCode: String
return maybeConstructProfile(languageCode, regionCode)
}
+ /**
+ * Returns a new [AndroidLocaleProfile] using the provided [languageId]'s
+ * [LanguageId.getAndroidResourcesLanguageId] as the basis of the profile, or null if none can
+ * be created.
+ *
+ * This is meant to be used in cases when an [AndroidLocaleProfile] is needed to match a
+ * specific Android-compatible [Locale] (e.g. via [AndroidLocaleProfile.computeAndroidLocale])
+ * that can correctly match to specific Android app strings.
+ */
+ fun createFromAndroidResourcesLanguageId(languageId: LanguageId): AndroidLocaleProfile? {
+ val languageCode = languageId.androidResourcesLanguageId.languageCode
+ val regionCode = languageId.androidResourcesLanguageId.regionCode
+ return when {
+ !languageId.hasAndroidResourcesLanguageId() -> null
+ languageCode.isEmpty() -> null
+ // Empty region codes are allowed for Android resource IDs since they should always be used
+ // verbatim to ensure the correct Android resource string can be computed (such as for macro
+ // languages).
+ regionCode.isEmpty() -> LanguageAndWildcardRegionProfile(languageCode.asLowerCase())
+ else -> {
+ LanguageAndRegionProfile(
+ languageCode.asLowerCase(), regionCode.asLowerCase(), regionCode.asUpperCase()
+ )
+ }
+ }
+ }
+
private fun maybeConstructProfile(
languageCode: String,
regionCode: String,
emptyRegionAsWildcard: Boolean = false
): AndroidLocaleProfile? {
- return if (languageCode.isNotEmpty() && (regionCode.isNotEmpty() || emptyRegionAsWildcard)) {
- val adjustedRegionCode = if (emptyRegionAsWildcard && regionCode.isEmpty()) {
- REGION_WILDCARD
- } else regionCode
- AndroidLocaleProfile(languageCode, adjustedRegionCode)
- } else null
+ return when {
+ languageCode.isEmpty() -> null
+ regionCode.isNotEmpty() -> {
+ LanguageAndRegionProfile(
+ languageCode.asLowerCase(), regionCode.asLowerCase(), regionCode.asUpperCase()
+ )
+ }
+ emptyRegionAsWildcard -> LanguageAndWildcardRegionProfile(languageCode.asLowerCase())
+ else -> null
+ }
}
private fun String.divide(delimiter: String): Pair? {
@@ -120,6 +257,8 @@ data class AndroidLocaleProfile(val languageCode: String, val regionCode: String
} else null
}
- private fun String.isNotEmptyOrWildcard() = isNotEmpty() && this != REGION_WILDCARD
+ private fun String.asLowerCase() = machineLocale.run { toMachineLowerCase() }
+
+ private fun String.asUpperCase() = machineLocale.run { toMachineUpperCase() }
}
}
diff --git a/utility/src/main/java/org/oppia/android/util/locale/BUILD.bazel b/utility/src/main/java/org/oppia/android/util/locale/BUILD.bazel
index dcec5fbc2c9..9635257d80e 100644
--- a/utility/src/main/java/org/oppia/android/util/locale/BUILD.bazel
+++ b/utility/src/main/java/org/oppia/android/util/locale/BUILD.bazel
@@ -12,6 +12,7 @@ kt_android_library(
],
visibility = ["//:oppia_api_visibility"],
deps = [
+ ":dagger",
":oppia_locale",
],
)
@@ -75,6 +76,8 @@ kt_android_library(
deps = [
":android_locale_profile",
":dagger",
+ "//third_party:org_jetbrains_kotlinx_kotlinx-coroutines-core",
+ "//utility/src/main/java/org/oppia/android/util/threading:annotations",
],
)
diff --git a/utility/src/main/java/org/oppia/android/util/locale/DisplayLocaleImpl.kt b/utility/src/main/java/org/oppia/android/util/locale/DisplayLocaleImpl.kt
index 838c8b192f7..f8bff8b2b06 100644
--- a/utility/src/main/java/org/oppia/android/util/locale/DisplayLocaleImpl.kt
+++ b/utility/src/main/java/org/oppia/android/util/locale/DisplayLocaleImpl.kt
@@ -12,17 +12,21 @@ import java.util.Date
import java.util.Locale
import java.util.Objects
-// TODO(#3766): Restrict to be 'internal'.
-/** Implementation of [OppiaLocale.DisplayLocale]. */
+// TODO(#3766): Restrict DisplayLocaleImpl and formattingLocale to be 'internal'.
+/**
+ * Implementation of [OppiaLocale.DisplayLocale].
+ *
+ * @property localeContext the [OppiaLocaleContext] that this locale is representing
+ * @property formattingLocale the [Locale] used for user-facing string formatting
+ * @property machineLocale the application-wide [MachineLocale] used for string formatting
+ * @property formatterFactory the application-wide factory for creating a new [OppiaBidiFormatter]
+ */
class DisplayLocaleImpl(
localeContext: OppiaLocaleContext,
+ val formattingLocale: Locale,
private val machineLocale: MachineLocale,
- private val androidLocaleFactory: AndroidLocaleFactory,
private val formatterFactory: OppiaBidiFormatter.Factory
) : OppiaLocale.DisplayLocale(localeContext) {
- // TODO(#3766): Restrict to be 'internal'.
- /** The [Locale] used for user-facing string formatting in this display locale. */
- val formattingLocale: Locale by lazy { androidLocaleFactory.createAndroidLocale(localeContext) }
private val dateFormat by lazy {
DateFormat.getDateInstance(DATE_FORMAT_LENGTH, formattingLocale)
}
diff --git a/utility/src/test/java/org/oppia/android/util/data/DataProvidersTest.kt b/utility/src/test/java/org/oppia/android/util/data/DataProvidersTest.kt
index d4697e891b7..596b43e2334 100644
--- a/utility/src/test/java/org/oppia/android/util/data/DataProvidersTest.kt
+++ b/utility/src/test/java/org/oppia/android/util/data/DataProvidersTest.kt
@@ -972,6 +972,40 @@ class DataProvidersTest {
}
}
+ @Test
+ fun testTransformAsync_toLiveData_throwsException_deliversFailure() {
+ val baseProvider = createSuccessfulDataProvider(BASE_PROVIDER_ID_0, STR_VALUE_0)
+ val dataProvider = baseProvider.transformAsync(TRANSFORMED_PROVIDER_ID) {
+ throw IllegalStateException("Transform failure")
+ }
+
+ dataProvider.toLiveData().observeForever(mockIntLiveDataObserver)
+ testCoroutineDispatchers.advanceUntilIdle()
+
+ // Note that the exception type here is not chained since the failure occurred in the transform
+ // function.
+ verify(mockIntLiveDataObserver).onChanged(intResultCaptor.capture())
+ assertThat(intResultCaptor.value).isFailureThat().apply {
+ isInstanceOf(IllegalStateException::class.java)
+ hasMessageThat().contains("Transform failure")
+ }
+ }
+
+ @Test
+ fun testTransformAsync_toLiveData_throwsException_deliversFailure_logsException() {
+ val baseProvider = createSuccessfulDataProvider(BASE_PROVIDER_ID_0, STR_VALUE_0)
+ val dataProvider = baseProvider.transformAsync(TRANSFORMED_PROVIDER_ID) {
+ throw IllegalStateException("Transform failure")
+ }
+
+ dataProvider.toLiveData().observeForever(mockIntLiveDataObserver)
+ testCoroutineDispatchers.advanceUntilIdle()
+ val exception = fakeExceptionLogger.getMostRecentException()
+
+ assertThat(exception).isInstanceOf(IllegalStateException::class.java)
+ assertThat(exception).hasMessageThat().contains("Transform failure")
+ }
+
@Test
fun testTransformAsync_toLiveData_basePending_deliversPending() {
val baseProvider = createPendingDataProvider(BASE_PROVIDER_ID_0)
diff --git a/utility/src/test/java/org/oppia/android/util/locale/AndroidLocaleFactoryTest.kt b/utility/src/test/java/org/oppia/android/util/locale/AndroidLocaleFactoryTest.kt
index c009baad4e0..581668c878c 100644
--- a/utility/src/test/java/org/oppia/android/util/locale/AndroidLocaleFactoryTest.kt
+++ b/utility/src/test/java/org/oppia/android/util/locale/AndroidLocaleFactoryTest.kt
@@ -9,6 +9,10 @@ import dagger.BindsInstance
import dagger.Component
import dagger.Module
import dagger.Provides
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.async
+import kotlinx.coroutines.runBlocking
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -22,9 +26,14 @@ import org.oppia.android.app.model.OppiaLocaleContext
import org.oppia.android.app.model.OppiaRegion
import org.oppia.android.app.model.RegionSupportDefinition
import org.oppia.android.testing.assertThrows
+import org.oppia.android.testing.robolectric.RobolectricModule
+import org.oppia.android.testing.threading.TestCoroutineDispatchers
+import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
+import org.oppia.android.util.threading.BackgroundDispatcher
import org.robolectric.annotation.Config
import org.robolectric.annotation.LooperMode
+import java.util.Locale
import javax.inject.Inject
import javax.inject.Singleton
@@ -41,18 +50,63 @@ import javax.inject.Singleton
@LooperMode(LooperMode.Mode.PAUSED)
@Config(manifest = Config.NONE)
class AndroidLocaleFactoryTest {
- @Inject
- lateinit var androidLocaleFactory: AndroidLocaleFactory
+ @Inject lateinit var androidLocaleFactory: AndroidLocaleFactory
+ @field:[Inject BackgroundDispatcher] lateinit var backgroundDispatcher: CoroutineDispatcher
+ @Inject lateinit var testCoroutineDispatchers: TestCoroutineDispatchers
@Before
fun setUp() {
setUpTestApplicationComponent()
}
+ /* Basic tests for one-off Locale creation (latter functions indirectly test in more detail. */
+
+ @Test
+ fun testCreateOneOffAndroidLocale_default_throwsException() {
+ val exception = assertThrows() {
+ androidLocaleFactory.createOneOffAndroidLocale(OppiaLocaleContext.getDefaultInstance())
+ }
+
+ // The operation should fail since there's no language type defined.
+ assertThat(exception).hasMessageThat().contains("Invalid language case")
+ }
+
+ @Test
+ fun testCreateOneOffAndroidLocale_appStrings_defaultLanguage_returnsRootLocale() {
+ val context =
+ createAppStringsContext(
+ language = OppiaLanguage.LANGUAGE_UNSPECIFIED,
+ appStringId = LanguageId.getDefaultInstance(),
+ regionDefinition = RegionSupportDefinition.getDefaultInstance()
+ )
+
+ val locale = androidLocaleFactory.createOneOffAndroidLocale(context)
+
+ assertThat(locale).isEqualTo(Locale.ROOT)
+ }
+
+ @Test
+ fun testCreateOneOffAndroidLocale_appStrings_withAndroidId_compatible_returnsAndroidIdLocale() {
+ val context =
+ createAppStringsContext(
+ language = OppiaLanguage.BRAZILIAN_PORTUGUESE,
+ appStringId = createLanguageId(androidLanguageId = PT_BR_ANDROID_LANGUAGE_ID),
+ regionDefinition = REGION_BRAZIL
+ )
+
+ val locale = androidLocaleFactory.createOneOffAndroidLocale(context)
+
+ // The context should be matched to a valid locale.
+ assertThat(locale.language).isEqualTo("pt")
+ assertThat(locale.country).isEqualTo("BR")
+ }
+
+ /* Begin createAndroidLocaleAsync tests. */
+
@Test
fun testCreateLocale_default_throwsException() {
val exception = assertThrows() {
- androidLocaleFactory.createAndroidLocale(OppiaLocaleContext.getDefaultInstance())
+ androidLocaleFactory.createAndroidLocaleBlocking(OppiaLocaleContext.getDefaultInstance())
}
// The operation should fail since there's no language type defined.
@@ -70,7 +124,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_BRAZIL
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// The context should be matched to a valid locale.
assertThat(locale.language).isEqualTo("pt")
@@ -86,7 +140,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_BRAZIL
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// The context should be matched to a valid locale.
assertThat(locale.language).isEqualTo("pt")
@@ -102,7 +156,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_US
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// The context should be matched to a valid locale. Note that BR is matched since the IETF
// language tag includes the region.
@@ -119,7 +173,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_BRAZIL
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// The context should be matched to a valid locale.
assertThat(locale.language).isEqualTo("pt")
@@ -138,7 +192,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_BRAZIL
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// The Android is preferred when both are present. Note no region is provided since the Android
// language is missing a region definition.
@@ -158,7 +212,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_BRAZIL
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// The Android is preferred when both are present. Note no region is provided since the Android
// language is missing a region definition.
@@ -176,7 +230,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_BRAZIL
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// 'qq' is picked because it's an Android ID for app strings, so it's always taken as a forced
// locale over any fallback options.
@@ -194,7 +248,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_BRAZIL
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// pt-BR should be picked because the primary language doesn't match a real locale, and it's not
// an Android ID that would take precedence.
@@ -212,7 +266,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_BRAZIL
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// pt-BR should be picked because the primary language doesn't match a real locale.
assertThat(locale.language).isEqualTo("pt")
@@ -229,7 +283,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_BRAZIL
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// pt-BR should be picked because the primary language's region doesn't match a real locale.
assertThat(locale.language).isEqualTo("pt")
@@ -246,7 +300,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_ZZ
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// pt-BR should be picked because the supplied region doesn't match a real locale.
assertThat(locale.language).isEqualTo("pt")
@@ -263,7 +317,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_BRAZIL
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// pt-BR should be picked because the primary language doesn't match a real locale.
assertThat(locale.language).isEqualTo("pt")
@@ -280,7 +334,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_BRAZIL
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// pt-BR should be picked because the primary language is invalid.
assertThat(locale.language).isEqualTo("pt")
@@ -298,7 +352,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_BRAZIL
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// pt-BR should be picked because the primary language isn't compatible with the current SDK.
assertThat(locale.language).isEqualTo("pt")
@@ -318,7 +372,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_INDIA
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// 'qq' is picked because it's an Android ID for app strings, so it's always taken as a forced
// locale over any fallback options.
@@ -339,7 +393,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_INDIA
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// pt-BR should be picked because the primary language doesn't match a real locale, and it's not
// an Android ID that would take precedence. Beyond that, the fallback's Android ID should take
@@ -361,7 +415,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_INDIA
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// 'qq' is picked because it's an Android ID for app strings, so it's always taken as a forced
// locale over any fallback options.
@@ -382,7 +436,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_INDIA
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// pt-BR should be picked since Android IDs take precedence among multiple fallback options, and
// none of the primary options are viable.
@@ -400,7 +454,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_INDIA
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// 'qq' is picked over the primary language because it's an Android ID and the primary language
// doesn't match any locales.
@@ -418,7 +472,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_INDIA
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// 'qq' is the exact locale being requested.
assertThat(locale.language).isEqualTo("qq")
@@ -438,7 +492,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_INDIA
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// 'qq' takes precedence over the IETF language since Android IDs are picked first when
// creating a forced locale.
@@ -459,7 +513,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_INDIA
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// 'qq' takes precedence over the macaronic language since Android IDs are picked first when
// creating a forced locale.
@@ -477,7 +531,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_INDIA
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// The IETF language ID is used for the forced locale (note that fallback languages are ignored
// when computing the forced locale).
@@ -495,7 +549,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_INDIA
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// The Hinglish macaronic language ID is used for the forced locale (note that fallback
// languages are ignored when computing the forced locale).
@@ -514,7 +568,7 @@ class AndroidLocaleFactoryTest {
)
val exception = assertThrows() {
- androidLocaleFactory.createAndroidLocale(context)
+ androidLocaleFactory.createAndroidLocaleBlocking(context)
}
assertThat(exception).hasMessageThat().contains("Invalid ID")
@@ -532,7 +586,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_INDIA
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// The 'qq' language should be matched as a forced profile since both language IDs are
// SDK-incompatible (despite the fallback being a matchable language).
@@ -549,7 +603,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_INDIA
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// Simple macro languages may not match any internal locales due to missing regions. They should
// still become a valid locale (due to wildcard matching internally).
@@ -558,7 +612,7 @@ class AndroidLocaleFactoryTest {
}
@Test
- fun testCreateLocale_appStrings_allIncompat_invalidLangType_throwsException() {
+ fun testCreateLocale_appStrings_allIncompat_invalidLangType_returnsRootLocale() {
val context =
createAppStringsContext(
language = OppiaLanguage.ENGLISH,
@@ -566,11 +620,9 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_INDIA
)
- val exception = assertThrows() {
- androidLocaleFactory.createAndroidLocale(context)
- }
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
- assertThat(exception).hasMessageThat().contains("Invalid language case")
+ assertThat(locale).isEqualTo(Locale.ROOT)
}
/* Tests for written content strings. */
@@ -584,7 +636,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_BRAZIL
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// The context should be matched to a valid locale.
assertThat(locale.language).isEqualTo("pt")
@@ -600,7 +652,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_BRAZIL
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// The context should be matched to a valid locale.
assertThat(locale.language).isEqualTo("pt")
@@ -616,7 +668,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_US
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// The context should be matched to a valid locale. Note that BR is matched since the IETF
// language tag includes the region.
@@ -633,7 +685,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_BRAZIL
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// The context should be matched to a valid locale.
assertThat(locale.language).isEqualTo("pt")
@@ -652,7 +704,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_BRAZIL
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// The Android is preferred when both are present. Note no region is provided since the Android
// language is missing a region definition.
@@ -672,7 +724,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_BRAZIL
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// The Android is preferred when both are present. Note no region is provided since the Android
// language is missing a region definition.
@@ -690,7 +742,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_BRAZIL
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// pt-BR should be picked because the primary language doesn't match a real locale.
assertThat(locale.language).isEqualTo("pt")
@@ -707,7 +759,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_BRAZIL
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// pt-BR should be picked because the primary language doesn't match a real locale.
assertThat(locale.language).isEqualTo("pt")
@@ -724,7 +776,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_BRAZIL
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// pt-BR should be picked because the primary language's region doesn't match a real locale.
assertThat(locale.language).isEqualTo("pt")
@@ -741,7 +793,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_ZZ
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// pt-BR should be picked because the supplied region doesn't match a real locale.
assertThat(locale.language).isEqualTo("pt")
@@ -758,7 +810,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_BRAZIL
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// pt-BR should be picked because the primary language doesn't match a real locale.
assertThat(locale.language).isEqualTo("pt")
@@ -775,7 +827,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_BRAZIL
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// pt-BR should be picked because the primary language is invalid.
assertThat(locale.language).isEqualTo("pt")
@@ -793,7 +845,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_BRAZIL
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// pt-BR should be picked because the primary language isn't compatible with the current SDK.
assertThat(locale.language).isEqualTo("pt")
@@ -813,7 +865,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_INDIA
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// pt-BR should be picked since Android IDs take precedence among multiple fallback options.
assertThat(locale.language).isEqualTo("pt")
@@ -833,7 +885,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_INDIA
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// pt-BR should be picked since Android IDs take precedence among multiple fallback options.
assertThat(locale.language).isEqualTo("pt")
@@ -850,7 +902,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_INDIA
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// 'qq' is the exact locale being requested.
assertThat(locale.language).isEqualTo("qq")
@@ -870,7 +922,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_INDIA
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// 'qq' takes precedence over the IETF language since Android IDs are picked first when
// creating a forced locale.
@@ -891,7 +943,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_INDIA
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// 'qq' takes precedence over the macaronic language since Android IDs are picked first when
// creating a forced locale.
@@ -909,7 +961,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_INDIA
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// The IETF language ID is used for the forced locale (note that fallback languages are ignored
// when computing the forced locale).
@@ -927,7 +979,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_INDIA
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// The Hinglish macaronic language ID is used for the forced locale (note that fallback
// languages are ignored when computing the forced locale).
@@ -946,7 +998,7 @@ class AndroidLocaleFactoryTest {
)
val exception = assertThrows() {
- androidLocaleFactory.createAndroidLocale(context)
+ androidLocaleFactory.createAndroidLocaleBlocking(context)
}
assertThat(exception).hasMessageThat().contains("Invalid ID")
@@ -964,7 +1016,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_INDIA
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// The 'qq' language should be matched as a forced profile since both language IDs are
// SDK-incompatible (despite the fallback being a matchable language).
@@ -981,7 +1033,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_INDIA
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// Simple macro languages may not match any internal locales due to missing regions. They should
// still become a valid locale (due to wildcard matching internally).
@@ -999,7 +1051,7 @@ class AndroidLocaleFactoryTest {
)
val exception = assertThrows() {
- androidLocaleFactory.createAndroidLocale(context)
+ androidLocaleFactory.createAndroidLocaleBlocking(context)
}
assertThat(exception).hasMessageThat().contains("Invalid language case")
@@ -1016,7 +1068,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_BRAZIL
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// The context should be matched to a valid locale.
assertThat(locale.language).isEqualTo("pt")
@@ -1032,7 +1084,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_BRAZIL
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// The context should be matched to a valid locale.
assertThat(locale.language).isEqualTo("pt")
@@ -1048,7 +1100,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_US
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// The context should be matched to a valid locale. Note that BR is matched since the IETF
// language tag includes the region.
@@ -1065,7 +1117,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_BRAZIL
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// The context should be matched to a valid locale.
assertThat(locale.language).isEqualTo("pt")
@@ -1084,7 +1136,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_BRAZIL
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// The Android is preferred when both are present. Note no region is provided since the Android
// language is missing a region definition.
@@ -1104,7 +1156,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_BRAZIL
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// The Android is preferred when both are present. Note no region is provided since the Android
// language is missing a region definition.
@@ -1122,7 +1174,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_BRAZIL
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// pt-BR should be picked because the primary language doesn't match a real locale.
assertThat(locale.language).isEqualTo("pt")
@@ -1139,7 +1191,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_BRAZIL
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// pt-BR should be picked because the primary language doesn't match a real locale.
assertThat(locale.language).isEqualTo("pt")
@@ -1156,7 +1208,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_BRAZIL
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// pt-BR should be picked because the primary language's region doesn't match a real locale.
assertThat(locale.language).isEqualTo("pt")
@@ -1173,7 +1225,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_ZZ
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// pt-BR should be picked because the supplied region doesn't match a real locale.
assertThat(locale.language).isEqualTo("pt")
@@ -1190,7 +1242,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_BRAZIL
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// pt-BR should be picked because the primary language doesn't match a real locale.
assertThat(locale.language).isEqualTo("pt")
@@ -1207,7 +1259,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_BRAZIL
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// pt-BR should be picked because the primary language is invalid.
assertThat(locale.language).isEqualTo("pt")
@@ -1225,7 +1277,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_BRAZIL
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// pt-BR should be picked because the primary language isn't compatible with the current SDK.
assertThat(locale.language).isEqualTo("pt")
@@ -1245,7 +1297,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_INDIA
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// pt-BR should be picked since Android IDs take precedence among multiple fallback options.
assertThat(locale.language).isEqualTo("pt")
@@ -1265,7 +1317,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_INDIA
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// pt-BR should be picked since Android IDs take precedence among multiple fallback options.
assertThat(locale.language).isEqualTo("pt")
@@ -1282,7 +1334,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_INDIA
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// 'qq' is the exact locale being requested.
assertThat(locale.language).isEqualTo("qq")
@@ -1302,7 +1354,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_INDIA
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// 'qq' takes precedence over the IETF language since Android IDs are picked first when
// creating a forced locale.
@@ -1323,7 +1375,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_INDIA
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// 'qq' takes precedence over the macaronic language since Android IDs are picked first when
// creating a forced locale.
@@ -1341,7 +1393,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_INDIA
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// The IETF language ID is used for the forced locale (note that fallback languages are ignored
// when computing the forced locale).
@@ -1359,7 +1411,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_INDIA
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// The Hinglish macaronic language ID is used for the forced locale (note that fallback
// languages are ignored when computing the forced locale).
@@ -1378,7 +1430,7 @@ class AndroidLocaleFactoryTest {
)
val exception = assertThrows() {
- androidLocaleFactory.createAndroidLocale(context)
+ androidLocaleFactory.createAndroidLocaleBlocking(context)
}
assertThat(exception).hasMessageThat().contains("Invalid ID")
@@ -1396,7 +1448,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_INDIA
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// The 'qq' language should be matched as a forced profile since both language IDs are
// SDK-incompatible (despite the fallback being a matchable language).
@@ -1413,7 +1465,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_INDIA
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// Simple macro languages may not match any internal locales due to missing regions. They should
// still become a valid locale (due to wildcard matching internally).
@@ -1431,12 +1483,22 @@ class AndroidLocaleFactoryTest {
)
val exception = assertThrows() {
- androidLocaleFactory.createAndroidLocale(context)
+ androidLocaleFactory.createAndroidLocaleBlocking(context)
}
assertThat(exception).hasMessageThat().contains("Invalid language case")
}
+ private fun AndroidLocaleFactory.createAndroidLocaleBlocking(
+ context: OppiaLocaleContext
+ ): Locale {
+ val deferred =
+ CoroutineScope(backgroundDispatcher).async { createAndroidLocaleAsync(context).await() }
+ testCoroutineDispatchers.runCurrent()
+ assertThat(deferred.isCompleted).isTrue()
+ return runBlocking { deferred.await() }
+ }
+
private fun createLanguageId(androidLanguageId: AndroidLanguageId): LanguageId {
return LanguageId.newBuilder().apply {
androidResourcesLanguageId = androidLanguageId
@@ -1602,7 +1664,8 @@ class AndroidLocaleFactoryTest {
@Singleton
@Component(
modules = [
- TestModule::class, LocaleProdModule::class, FakeOppiaClockModule::class
+ TestModule::class, LocaleProdModule::class, FakeOppiaClockModule::class,
+ TestDispatcherModule::class, RobolectricModule::class
]
)
interface TestApplicationComponent {
diff --git a/utility/src/test/java/org/oppia/android/util/locale/AndroidLocaleProfileTest.kt b/utility/src/test/java/org/oppia/android/util/locale/AndroidLocaleProfileTest.kt
index 42384c8531a..d686a5aaea9 100644
--- a/utility/src/test/java/org/oppia/android/util/locale/AndroidLocaleProfileTest.kt
+++ b/utility/src/test/java/org/oppia/android/util/locale/AndroidLocaleProfileTest.kt
@@ -12,6 +12,7 @@ import dagger.Provides
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
+import org.oppia.android.app.model.LanguageSupportDefinition.AndroidLanguageId
import org.oppia.android.app.model.LanguageSupportDefinition.IetfBcp47LanguageId
import org.oppia.android.app.model.LanguageSupportDefinition.LanguageId
import org.oppia.android.app.model.LanguageSupportDefinition.MacaronicLanguageId
@@ -19,6 +20,11 @@ import org.oppia.android.app.model.OppiaRegion
import org.oppia.android.app.model.RegionSupportDefinition
import org.oppia.android.app.model.RegionSupportDefinition.IetfBcp47RegionId
import org.oppia.android.testing.time.FakeOppiaClockModule
+import org.oppia.android.util.locale.AndroidLocaleProfile.LanguageAndRegionProfile
+import org.oppia.android.util.locale.AndroidLocaleProfile.LanguageAndWildcardRegionProfile
+import org.oppia.android.util.locale.AndroidLocaleProfile.LanguageOnlyProfile
+import org.oppia.android.util.locale.AndroidLocaleProfile.RegionOnlyProfile
+import org.oppia.android.util.locale.AndroidLocaleProfile.RootProfile
import org.robolectric.annotation.Config
import org.robolectric.annotation.LooperMode
import java.util.Locale
@@ -32,11 +38,10 @@ import javax.inject.Singleton
@LooperMode(LooperMode.Mode.PAUSED)
@Config(manifest = Config.NONE)
class AndroidLocaleProfileTest {
- @Inject
- lateinit var machineLocale: OppiaLocale.MachineLocale
+ @Inject lateinit var androidLocaleProfileFactory: AndroidLocaleProfile.Factory
- private val portugueseLocale by lazy { Locale("pt") }
private val brazilianPortugueseLocale by lazy { Locale("pt", "BR") }
+ private val kenyaOnlyLocale by lazy { Locale(/* language = */ "", "KE") }
@Before
fun setUp() {
@@ -46,27 +51,38 @@ class AndroidLocaleProfileTest {
/* Tests for createFrom */
@Test
- fun testCreateProfile_fromRootLocale_returnsProfileWithoutLanguageAndRegionCode() {
- val profile = AndroidLocaleProfile.createFrom(Locale.ROOT)
+ fun testCreateProfile_fromRootLocale_returnsRootProfile() {
+ val profile = androidLocaleProfileFactory.createFrom(Locale.ROOT)
- assertThat(profile.languageCode).isEmpty()
- assertThat(profile.regionCode).isEmpty()
+ assertThat(profile).isEqualTo(RootProfile)
}
@Test
- fun testCreateProfile_fromEnglishLocale_returnsProfileWithLanguageAndWithoutRegion() {
- val profile = AndroidLocaleProfile.createFrom(Locale.ENGLISH)
+ fun testCreateProfile_fromEnglishLocale_returnsLanguageOnlyProfile() {
+ val profile = androidLocaleProfileFactory.createFrom(Locale.ENGLISH)
- assertThat(profile.languageCode).isEqualTo("en")
- assertThat(profile.regionCode).isEmpty()
+ val languageOnlyProfile = profile as? LanguageOnlyProfile
+ assertThat(profile).isInstanceOf(LanguageOnlyProfile::class.java)
+ assertThat(languageOnlyProfile?.languageCode).isEqualTo("en")
}
@Test
fun testCreateProfile_fromBrazilianPortuguese_returnsProfileWithLanguageAndRegion() {
- val profile = AndroidLocaleProfile.createFrom(brazilianPortugueseLocale)
+ val profile = androidLocaleProfileFactory.createFrom(brazilianPortugueseLocale)
- assertThat(profile.languageCode).isEqualTo("pt")
- assertThat(profile.regionCode).isEqualTo("BR")
+ val languageAndRegionProfile = profile as? LanguageAndRegionProfile
+ assertThat(profile).isInstanceOf(LanguageAndRegionProfile::class.java)
+ assertThat(languageAndRegionProfile?.languageCode).isEqualTo("pt")
+ assertThat(languageAndRegionProfile?.regionCode).isEqualTo("br")
+ }
+
+ @Test
+ fun testCreateProfile_fromKenyaLocale_returnsRegionOnlyProfile() {
+ val profile = androidLocaleProfileFactory.createFrom(kenyaOnlyLocale)
+
+ val regionOnlyProfile = profile as? RegionOnlyProfile
+ assertThat(profile).isInstanceOf(RegionOnlyProfile::class.java)
+ assertThat(regionOnlyProfile?.regionCode).isEqualTo("ke")
}
/* Tests for createFromIetfDefinitions */
@@ -74,7 +90,7 @@ class AndroidLocaleProfileTest {
@Test
fun testCreateProfileFromIetf_defaultLanguageId_nullRegion_returnsNull() {
val profile =
- AndroidLocaleProfile.createFromIetfDefinitions(
+ androidLocaleProfileFactory.createFromIetfDefinitions(
languageId = LanguageId.getDefaultInstance(), regionDefinition = null
)
@@ -89,7 +105,8 @@ class AndroidLocaleProfileTest {
}.build()
}.build()
- val profile = AndroidLocaleProfile.createFromIetfDefinitions(languageWithoutIetf, REGION_INDIA)
+ val profile =
+ androidLocaleProfileFactory.createFromIetfDefinitions(languageWithoutIetf, REGION_INDIA)
// The language ID needs to have an IETF BCP 47 ID defined.
assertThat(profile).isNull()
@@ -103,7 +120,8 @@ class AndroidLocaleProfileTest {
}.build()
}.build()
- val profile = AndroidLocaleProfile.createFromIetfDefinitions(languageWithIetf, REGION_INDIA)
+ val profile =
+ androidLocaleProfileFactory.createFromIetfDefinitions(languageWithIetf, REGION_INDIA)
// The language ID needs to have an IETF BCP 47 ID defined.
assertThat(profile).isNull()
@@ -117,7 +135,8 @@ class AndroidLocaleProfileTest {
}.build()
}.build()
- val profile = AndroidLocaleProfile.createFromIetfDefinitions(languageWithIetf, REGION_INDIA)
+ val profile =
+ androidLocaleProfileFactory.createFromIetfDefinitions(languageWithIetf, REGION_INDIA)
// The language ID needs to have a well-formed IETF BCP 47 ID defined.
assertThat(profile).isNull()
@@ -131,12 +150,15 @@ class AndroidLocaleProfileTest {
}.build()
}.build()
- val profile = AndroidLocaleProfile.createFromIetfDefinitions(languageWithIetf, REGION_INDIA)
+ val profile =
+ androidLocaleProfileFactory.createFromIetfDefinitions(languageWithIetf, REGION_INDIA)
// The constituent language code should come from the language ID, and the region code from the
// provided region definition.
- assertThat(profile?.languageCode).isEqualTo("pt")
- assertThat(profile?.regionCode).isEqualTo("IN")
+ val languageAndRegionProfile = profile as? LanguageAndRegionProfile
+ assertThat(profile).isInstanceOf(LanguageAndRegionProfile::class.java)
+ assertThat(languageAndRegionProfile?.languageCode).isEqualTo("pt")
+ assertThat(languageAndRegionProfile?.regionCode).isEqualTo("in")
}
@Test
@@ -147,11 +169,14 @@ class AndroidLocaleProfileTest {
}.build()
}.build()
- val profile = AndroidLocaleProfile.createFromIetfDefinitions(languageWithIetf, REGION_INDIA)
+ val profile =
+ androidLocaleProfileFactory.createFromIetfDefinitions(languageWithIetf, REGION_INDIA)
// In this case, the region comes from the IETF language tag since it's included.
- assertThat(profile?.languageCode).isEqualTo("pt")
- assertThat(profile?.regionCode).isEqualTo("BR")
+ val languageAndRegionProfile = profile as? LanguageAndRegionProfile
+ assertThat(profile).isInstanceOf(LanguageAndRegionProfile::class.java)
+ assertThat(languageAndRegionProfile?.languageCode).isEqualTo("pt")
+ assertThat(languageAndRegionProfile?.regionCode).isEqualTo("br")
}
@Test
@@ -163,7 +188,7 @@ class AndroidLocaleProfileTest {
}.build()
val profile =
- AndroidLocaleProfile.createFromIetfDefinitions(
+ androidLocaleProfileFactory.createFromIetfDefinitions(
languageWithIetf, regionDefinition = RegionSupportDefinition.getDefaultInstance()
)
@@ -180,7 +205,7 @@ class AndroidLocaleProfileTest {
}.build()
val profile =
- AndroidLocaleProfile.createFromIetfDefinitions(
+ androidLocaleProfileFactory.createFromIetfDefinitions(
languageWithIetf, regionDefinition = RegionSupportDefinition.getDefaultInstance()
)
@@ -197,11 +222,14 @@ class AndroidLocaleProfileTest {
}.build()
val profile =
- AndroidLocaleProfile.createFromIetfDefinitions(languageWithIetf, regionDefinition = null)
+ androidLocaleProfileFactory.createFromIetfDefinitions(
+ languageWithIetf, regionDefinition = null
+ )
// A null region specifically means to use a wildcard match for regions.
- assertThat(profile?.languageCode).isEqualTo("pt")
- assertThat(profile?.regionCode).isEqualTo(AndroidLocaleProfile.REGION_WILDCARD)
+ val languageAndWildcardRegionProfile = profile as? LanguageAndWildcardRegionProfile
+ assertThat(profile).isInstanceOf(LanguageAndWildcardRegionProfile::class.java)
+ assertThat(languageAndWildcardRegionProfile?.languageCode).isEqualTo("pt")
}
/* Tests for createFromMacaronicLanguage */
@@ -209,7 +237,9 @@ class AndroidLocaleProfileTest {
@Test
fun testCreateProfileFromMacaronic_defaultLanguageId_returnsNull() {
val profile =
- AndroidLocaleProfile.createFromMacaronicLanguage(languageId = LanguageId.getDefaultInstance())
+ androidLocaleProfileFactory.createFromMacaronicLanguage(
+ languageId = LanguageId.getDefaultInstance()
+ )
assertThat(profile).isNull()
}
@@ -222,8 +252,7 @@ class AndroidLocaleProfileTest {
}.build()
}.build()
- val profile =
- AndroidLocaleProfile.createFromMacaronicLanguage(languageWithoutMacaronic)
+ val profile = androidLocaleProfileFactory.createFromMacaronicLanguage(languageWithoutMacaronic)
// The provided language ID must have a macaronic ID defined.
assertThat(profile).isNull()
@@ -237,8 +266,7 @@ class AndroidLocaleProfileTest {
}.build()
}.build()
- val profile =
- AndroidLocaleProfile.createFromMacaronicLanguage(languageWithMacaronic)
+ val profile = androidLocaleProfileFactory.createFromMacaronicLanguage(languageWithMacaronic)
// The provided language ID must have a macaronic ID defined.
assertThat(profile).isNull()
@@ -252,8 +280,7 @@ class AndroidLocaleProfileTest {
}.build()
}.build()
- val profile =
- AndroidLocaleProfile.createFromMacaronicLanguage(languageWithMacaronic)
+ val profile = androidLocaleProfileFactory.createFromMacaronicLanguage(languageWithMacaronic)
// The provided language ID must have a well-formed macaronic ID defined.
assertThat(profile).isNull()
@@ -267,8 +294,7 @@ class AndroidLocaleProfileTest {
}.build()
}.build()
- val profile =
- AndroidLocaleProfile.createFromMacaronicLanguage(languageWithMacaronic)
+ val profile = androidLocaleProfileFactory.createFromMacaronicLanguage(languageWithMacaronic)
// The provided language ID must have a well-formed macaronic ID defined, that is, it must have
// two language parts defined.
@@ -283,8 +309,7 @@ class AndroidLocaleProfileTest {
}.build()
}.build()
- val profile =
- AndroidLocaleProfile.createFromMacaronicLanguage(languageWithMacaronic)
+ val profile = androidLocaleProfileFactory.createFromMacaronicLanguage(languageWithMacaronic)
// The macaronic ID has two parts as expected, but the second language ID must be filled in.
assertThat(profile).isNull()
@@ -298,262 +323,593 @@ class AndroidLocaleProfileTest {
}.build()
}.build()
- val profile =
- AndroidLocaleProfile.createFromMacaronicLanguage(languageWithMacaronic)
+ val profile = androidLocaleProfileFactory.createFromMacaronicLanguage(languageWithMacaronic)
// The macaronic ID was valid. Verify that both language IDs correctly populate the profile.
- assertThat(profile?.languageCode).isEqualTo("hi")
- assertThat(profile?.regionCode).isEqualTo("en")
+ val languageAndRegionProfile = profile as? LanguageAndRegionProfile
+ assertThat(profile).isInstanceOf(LanguageAndRegionProfile::class.java)
+ assertThat(languageAndRegionProfile?.languageCode).isEqualTo("hi")
+ assertThat(languageAndRegionProfile?.regionCode).isEqualTo("en")
+ }
+
+ /* Tests for createFromAndroidResourcesLanguageId(). */
+
+ @Test
+ fun testCreateFromAndroidResourcesLanguageId_defaultLanguageId_returnsNull() {
+ val profile =
+ androidLocaleProfileFactory.createFromAndroidResourcesLanguageId(
+ languageId = LanguageId.getDefaultInstance()
+ )
+
+ assertThat(profile).isNull()
+ }
+
+ @Test
+ fun testCreateFromAndroidResourcesLanguageId_ietfBcp47LanguageId_returnsNull() {
+ val profile =
+ androidLocaleProfileFactory.createFromAndroidResourcesLanguageId(
+ languageId = LanguageId.newBuilder().apply {
+ ietfBcp47Id = IetfBcp47LanguageId.newBuilder().apply {
+ ietfLanguageTag = "pt-BR"
+ }.build()
+ }.build()
+ )
+
+ // This method only creates a profile when provided with a valid Android resources language ID.
+ assertThat(profile).isNull()
+ }
+
+ @Test
+ fun testCreateFromAndroidResourcesLanguageId_macaronicLanguageId_returnsNull() {
+ val profile =
+ androidLocaleProfileFactory.createFromAndroidResourcesLanguageId(
+ languageId = LanguageId.newBuilder().apply {
+ macaronicId = MacaronicLanguageId.newBuilder().apply {
+ combinedLanguageCode = "hi-en"
+ }.build()
+ }.build()
+ )
+
+ // This method only creates a profile when provided with a valid Android resources language ID.
+ assertThat(profile).isNull()
+ }
+
+ @Test
+ fun testCreateFromAndroidResourcesLanguageId_defaultAndroidLanguageId_returnsNull() {
+ val profile =
+ androidLocaleProfileFactory.createFromAndroidResourcesLanguageId(
+ languageId = LanguageId.newBuilder().apply {
+ androidResourcesLanguageId = AndroidLanguageId.getDefaultInstance()
+ }.build()
+ )
+
+ // This method only creates a profile when provided with a valid Android resources language ID.
+ assertThat(profile).isNull()
+ }
+
+ @Test
+ fun testCreateFromAndroidResourcesLanguageId_androidLanguageId_regionOnly_returnsNull() {
+ val profile =
+ androidLocaleProfileFactory.createFromAndroidResourcesLanguageId(
+ languageId = LanguageId.newBuilder().apply {
+ androidResourcesLanguageId = AndroidLanguageId.newBuilder().apply {
+ regionCode = "BR"
+ }.build()
+ }.build()
+ )
+
+ // A valid Android language ID must include at least a language code.
+ assertThat(profile).isNull()
+ }
+
+ @Test
+ fun testCreateFromAndroidResourcesLanguageId_androidLanguageId_langOnly_returnsLangWildcard() {
+ val profile =
+ androidLocaleProfileFactory.createFromAndroidResourcesLanguageId(
+ languageId = LanguageId.newBuilder().apply {
+ androidResourcesLanguageId = AndroidLanguageId.newBuilder().apply {
+ languageCode = "pt"
+ }.build()
+ }.build()
+ )
+
+ // If no region is provided, match against all regions.
+ val languageAndWildcardRegionProfile = profile as? LanguageAndWildcardRegionProfile
+ assertThat(profile).isInstanceOf(LanguageAndWildcardRegionProfile::class.java)
+ assertThat(languageAndWildcardRegionProfile?.languageCode).isEqualTo("pt")
+ }
+
+ @Test
+ fun testCreateFromAndroidResourcesLanguageId_androidLanguageId_returnsLangAndRegionProfile() {
+ val profile =
+ androidLocaleProfileFactory.createFromAndroidResourcesLanguageId(
+ languageId = LanguageId.newBuilder().apply {
+ androidResourcesLanguageId = AndroidLanguageId.newBuilder().apply {
+ languageCode = "pt"
+ regionCode = "BR"
+ }.build()
+ }.build()
+ )
+
+ // Both the language & region codes should be represented in the profile.
+ val languageAndRegionProfile = profile as? LanguageAndRegionProfile
+ assertThat(profile).isInstanceOf(LanguageAndRegionProfile::class.java)
+ assertThat(languageAndRegionProfile?.languageCode).isEqualTo("pt")
+ assertThat(languageAndRegionProfile?.regionCode).isEqualTo("br")
}
/* Tests for matches() */
@Test
- fun testMatchProfile_rootProfile_withItself_match() {
- val profile = AndroidLocaleProfile.createFrom(Locale.ROOT)
+ fun testMatchProfile_rootProfile_andRootProfile_matches() {
+ val profile1 = RootProfile
+ val profile2 = RootProfile
- val matches = profile.matches(machineLocale, profile)
+ val matches = profile1.matches(profile2)
assertThat(matches).isTrue()
}
@Test
- fun testMatchProfile_englishProfile_withItself_match() {
- val profile = AndroidLocaleProfile.createFrom(Locale.ENGLISH)
+ fun testMatchProfile_rootProfile_andPtLanguageOnly_doNotMatch() {
+ val profile1 = RootProfile
+ val profile2 = LanguageOnlyProfile(languageCode = "pt")
- val matches = profile.matches(machineLocale, profile)
+ val matches = profile1.matches(profile2)
- assertThat(matches).isTrue()
+ assertThat(matches).isFalse()
}
@Test
- fun testMatchProfile_brazilianPortuguese_withItself_match() {
- val profile = AndroidLocaleProfile.createFrom(brazilianPortugueseLocale)
+ fun testMatchProfile_rootProfile_andBrRegionOnly_doNotMatch() {
+ val profile1 = RootProfile
+ val profile2 = RegionOnlyProfile(regionCode = "br")
- val matches = profile.matches(machineLocale, profile)
+ val matches = profile1.matches(profile2)
- assertThat(matches).isTrue()
+ assertThat(matches).isFalse()
}
@Test
- fun testMatchProfile_englishProfile_withItselfInDifferentCase_match() {
- val englishProfileLowercase = AndroidLocaleProfile(languageCode = "en", regionCode = "")
- val englishProfileUppercase = AndroidLocaleProfile(languageCode = "EN", regionCode = "")
+ fun testMatchProfile_rootProfile_andPtBrProfile_doNotMatch() {
+ val profile1 = RootProfile
+ val profile2 =
+ LanguageAndRegionProfile(languageCode = "pt", regionCode = "br", regionCodeUpperCase = "BR")
- val matches = englishProfileLowercase.matches(machineLocale, englishProfileUppercase)
+ val matches = profile1.matches(profile2)
- assertThat(matches).isTrue()
+ assertThat(matches).isFalse()
}
@Test
- fun testMatchProfile_englishProfile_withItselfInDifferentCase_reversed_match() {
- val englishProfileLowercase = AndroidLocaleProfile(languageCode = "en", regionCode = "")
- val englishProfileUppercase = AndroidLocaleProfile(languageCode = "EN", regionCode = "")
+ fun testMatchProfile_rootProfile_andPtWildcardProfile_doNotMatch() {
+ val profile1 = RootProfile
+ val profile2 = LanguageAndWildcardRegionProfile(languageCode = "pt")
- val matches = englishProfileUppercase.matches(machineLocale, englishProfileLowercase)
+ val matches = profile1.matches(profile2)
- assertThat(matches).isTrue()
+ assertThat(matches).isFalse()
}
@Test
- fun testMatchProfile_brazilianPortuguese_withItselfInDifferentCase_match() {
- val brazilianPortugueseProfileLowercase =
- AndroidLocaleProfile(languageCode = "pt", regionCode = "br")
- val brazilianPortugueseProfileUppercase =
- AndroidLocaleProfile(languageCode = "PT", regionCode = "BR")
+ fun testMatchProfile_ptLanguageOnly_andRootProfile_doNotMatch() {
+ val profile1 = LanguageOnlyProfile(languageCode = "pt")
+ val profile2 = RootProfile
- val matches =
- brazilianPortugueseProfileLowercase.matches(
- machineLocale, brazilianPortugueseProfileUppercase
- )
+ val matches = profile1.matches(profile2)
+
+ assertThat(matches).isFalse()
+ }
+
+ @Test
+ fun testMatchProfile_ptLanguageOnly_andPtLanguageOnly_matches() {
+ val profile1 = LanguageOnlyProfile(languageCode = "pt")
+ val profile2 = LanguageOnlyProfile(languageCode = "pt")
+
+ val matches = profile1.matches(profile2)
assertThat(matches).isTrue()
}
- fun testMatchProfile_brazilianPortuguese_withItselfInDifferentCase_reversed_match() {
- val brazilianPortugueseProfileLowercase =
- AndroidLocaleProfile(languageCode = "pt", regionCode = "br")
- val brazilianPortugueseProfileUppercase =
- AndroidLocaleProfile(languageCode = "PT", regionCode = "BR")
+ @Test
+ fun testMatchProfile_ptLanguageOnly_andSwLanguageOnly_doNotMatch() {
+ val profile1 = LanguageOnlyProfile(languageCode = "pt")
+ val profile2 = LanguageOnlyProfile(languageCode = "sw")
- val matches =
- brazilianPortugueseProfileUppercase.matches(
- machineLocale, brazilianPortugueseProfileLowercase
- )
+ val matches = profile1.matches(profile2)
+
+ assertThat(matches).isFalse()
+ }
+
+ @Test
+ fun testMatchProfile_ptLanguageOnly_andBrRegionOnly_doNotMatch() {
+ val profile1 = LanguageOnlyProfile(languageCode = "pt")
+ val profile2 = RegionOnlyProfile(regionCode = "br")
+
+ val matches = profile1.matches(profile2)
+
+ assertThat(matches).isFalse()
+ }
+
+ @Test
+ fun testMatchProfile_ptLanguageOnly_andPtBrProfile_doNotMatch() {
+ val profile1 = LanguageOnlyProfile(languageCode = "pt")
+ val profile2 =
+ LanguageAndRegionProfile(languageCode = "pt", regionCode = "br", regionCodeUpperCase = "BR")
+
+ val matches = profile1.matches(profile2)
+
+ assertThat(matches).isFalse()
+ }
+
+ @Test
+ fun testMatchProfile_ptLanguageOnly_andSwWildcardProfile_doNotMatch() {
+ val profile1 = LanguageOnlyProfile(languageCode = "pt")
+ val profile2 = LanguageAndWildcardRegionProfile(languageCode = "sw")
+
+ val matches = profile1.matches(profile2)
+
+ assertThat(matches).isFalse()
+ }
+
+ @Test
+ fun testMatchProfile_brRegionOnly_andRootProfile_doNotMatch() {
+ val profile1 = RegionOnlyProfile(regionCode = "br")
+ val profile2 = RootProfile
+
+ val matches = profile1.matches(profile2)
+
+ assertThat(matches).isFalse()
+ }
+
+ @Test
+ fun testMatchProfile_brRegionOnly_andPtLanguageOnly_doNotMatch() {
+ val profile1 = RegionOnlyProfile(regionCode = "br")
+ val profile2 = LanguageOnlyProfile(languageCode = "pt")
+
+ val matches = profile1.matches(profile2)
+
+ assertThat(matches).isFalse()
+ }
+
+ @Test
+ fun testMatchProfile_brRegionOnly_andBrRegionOnly_matches() {
+ val profile1 = RegionOnlyProfile(regionCode = "br")
+ val profile2 = RegionOnlyProfile(regionCode = "br")
+
+ val matches = profile1.matches(profile2)
assertThat(matches).isTrue()
}
@Test
- fun testMatchProfile_rootProfile_english_doNotMatch() {
- val rootProfile = AndroidLocaleProfile.createFrom(Locale.ROOT)
- val englishProfile = AndroidLocaleProfile.createFrom(Locale.ENGLISH)
+ fun testMatchProfile_brRegionOnly_andKeRegionOnly_doNotMatch() {
+ val profile1 = RegionOnlyProfile(regionCode = "br")
+ val profile2 = RegionOnlyProfile(regionCode = "ke")
- val matches = rootProfile.matches(machineLocale, englishProfile)
+ val matches = profile1.matches(profile2)
assertThat(matches).isFalse()
}
@Test
- fun testMatchProfile_rootProfile_brazilianPortuguese_doNotMatch() {
- val rootProfile = AndroidLocaleProfile.createFrom(Locale.ROOT)
- val brazilianPortugueseProfile = AndroidLocaleProfile.createFrom(brazilianPortugueseLocale)
+ fun testMatchProfile_brRegionOnly_andPtBrProfile_doNotMatch() {
+ val profile1 = RegionOnlyProfile(regionCode = "br")
+ val profile2 =
+ LanguageAndRegionProfile(languageCode = "pt", regionCode = "br", regionCodeUpperCase = "BR")
- val matches = rootProfile.matches(machineLocale, brazilianPortugueseProfile)
+ val matches = profile1.matches(profile2)
assertThat(matches).isFalse()
}
@Test
- fun testMatchProfile_english_brazilianPortuguese_doNotMatch() {
- val englishProfile = AndroidLocaleProfile.createFrom(Locale.ENGLISH)
- val brazilianPortugueseProfile = AndroidLocaleProfile.createFrom(brazilianPortugueseLocale)
+ fun testMatchProfile_brRegionOnly_andPtWildcardProfile_doNotMatch() {
+ val profile1 = RegionOnlyProfile(regionCode = "br")
+ val profile2 = LanguageAndWildcardRegionProfile(languageCode = "pt")
- val matches = englishProfile.matches(machineLocale, brazilianPortugueseProfile)
+ val matches = profile1.matches(profile2)
assertThat(matches).isFalse()
}
@Test
- fun testMatchProfile_rootProfile_englishWithWildcard_doNotMatch() {
- val rootProfile = AndroidLocaleProfile.createFrom(Locale.ROOT)
- val englishWithWildcardProfile = createProfileWithWildcard(languageCode = "en")
+ fun testMatchProfile_ptBrProfile_andRootProfile_doNotMatch() {
+ val profile1 =
+ LanguageAndRegionProfile(languageCode = "pt", regionCode = "br", regionCodeUpperCase = "BR")
+ val profile2 = RootProfile
- val matches = rootProfile.matches(machineLocale, englishWithWildcardProfile)
+ val matches = profile1.matches(profile2)
assertThat(matches).isFalse()
}
@Test
- fun testMatchProfile_rootProfile_rootProfileWithWildcard_match() {
- val rootProfile = AndroidLocaleProfile.createFrom(Locale.ROOT)
- val rootProfileWithWildcardProfile = createProfileWithWildcard(languageCode = "")
+ fun testMatchProfile_ptBrProfile_andPtLanguageOnly_doNotMatch() {
+ val profile1 =
+ LanguageAndRegionProfile(languageCode = "pt", regionCode = "br", regionCodeUpperCase = "BR")
+ val profile2 = LanguageOnlyProfile(languageCode = "pt")
- val matches = rootProfile.matches(machineLocale, rootProfileWithWildcardProfile)
+ val matches = profile1.matches(profile2)
+
+ assertThat(matches).isFalse()
+ }
+
+ @Test
+ fun testMatchProfile_ptBrProfile_andBrRegionOnly_doNotMatch() {
+ val profile1 =
+ LanguageAndRegionProfile(languageCode = "pt", regionCode = "br", regionCodeUpperCase = "BR")
+ val profile2 = RegionOnlyProfile(regionCode = "br")
+
+ val matches = profile1.matches(profile2)
+
+ assertThat(matches).isFalse()
+ }
+
+ @Test
+ fun testMatchProfile_ptBrProfile_andPtBrProfile_matches() {
+ val profile1 =
+ LanguageAndRegionProfile(languageCode = "pt", regionCode = "br", regionCodeUpperCase = "BR")
+ val profile2 =
+ LanguageAndRegionProfile(languageCode = "pt", regionCode = "br", regionCodeUpperCase = "BR")
+
+ val matches = profile1.matches(profile2)
assertThat(matches).isTrue()
}
@Test
- fun testMatchProfile_rootProfileWithWildcard_rootProfile_match() {
- val rootProfile = AndroidLocaleProfile.createFrom(Locale.ROOT)
- val rootProfileWithWildcardProfile = createProfileWithWildcard(languageCode = "")
+ fun testMatchProfile_ptBrProfile_andSwBrProfile_doNotMatch() {
+ val profile1 =
+ LanguageAndRegionProfile(languageCode = "pt", regionCode = "br", regionCodeUpperCase = "BR")
+ val profile2 =
+ LanguageAndRegionProfile(languageCode = "sw", regionCode = "br", regionCodeUpperCase = "BR")
- val matches = rootProfileWithWildcardProfile.matches(machineLocale, rootProfile)
+ val matches = profile1.matches(profile2)
+
+ assertThat(matches).isFalse()
+ }
+
+ @Test
+ fun testMatchProfile_ptBrProfile_andPtKeProfile_doNotMatch() {
+ val profile1 =
+ LanguageAndRegionProfile(languageCode = "pt", regionCode = "br", regionCodeUpperCase = "BR")
+ val profile2 =
+ LanguageAndRegionProfile(languageCode = "pt", regionCode = "ke", regionCodeUpperCase = "KE")
+
+ val matches = profile1.matches(profile2)
+
+ assertThat(matches).isFalse()
+ }
+
+ @Test
+ fun testMatchProfile_ptBrProfile_andSwKeProfile_doNotMatch() {
+ val profile1 =
+ LanguageAndRegionProfile(languageCode = "pt", regionCode = "br", regionCodeUpperCase = "BR")
+ val profile2 =
+ LanguageAndRegionProfile(languageCode = "sw", regionCode = "ke", regionCodeUpperCase = "KE")
+
+ val matches = profile1.matches(profile2)
+
+ assertThat(matches).isFalse()
+ }
+
+ @Test
+ fun testMatchProfile_ptBrProfile_andSwWildcardProfile_doNotMatch() {
+ val profile1 =
+ LanguageAndRegionProfile(languageCode = "pt", regionCode = "br", regionCodeUpperCase = "BR")
+ val profile2 = LanguageAndWildcardRegionProfile(languageCode = "sw")
+
+ val matches = profile1.matches(profile2)
+
+ assertThat(matches).isFalse()
+ }
+
+ @Test
+ fun testMatchProfile_ptWildcardProfile_andRootProfile_doNotMatch() {
+ val profile1 = LanguageAndWildcardRegionProfile(languageCode = "pt")
+ val profile2 = RootProfile
+
+ val matches = profile1.matches(profile2)
+
+ assertThat(matches).isFalse()
+ }
+
+ @Test
+ fun testMatchProfile_ptWildcardProfile_andPtLanguageOnly_matches() {
+ val profile1 = LanguageAndWildcardRegionProfile(languageCode = "pt")
+ val profile2 = LanguageOnlyProfile(languageCode = "pt")
+
+ val matches = profile1.matches(profile2)
assertThat(matches).isTrue()
}
@Test
- fun testMatchProfile_englishProfile_rootProfileWithWildcard_doNotMatch() {
- val englishProfile = AndroidLocaleProfile.createFrom(Locale.ENGLISH)
- val rootProfileWithWildcardProfile = createProfileWithWildcard(languageCode = "")
+ fun testMatchProfile_ptWildcardProfile_andSwLanguageOnly_doNotMatch() {
+ val profile1 = LanguageAndWildcardRegionProfile(languageCode = "pt")
+ val profile2 = LanguageOnlyProfile(languageCode = "sw")
- val matches = englishProfile.matches(machineLocale, rootProfileWithWildcardProfile)
+ val matches = profile1.matches(profile2)
assertThat(matches).isFalse()
}
@Test
- fun testMatchProfile_englishWithWildcard_brazilianPortuguese_doNotMatch() {
- val englishWithWildcardProfile = createProfileWithWildcard(languageCode = "en")
- val brazilianPortugueseProfile = AndroidLocaleProfile.createFrom(brazilianPortugueseLocale)
+ fun testMatchProfile_ptWildcardProfile_andBrRegionOnly_doNotMatch() {
+ val profile1 = LanguageAndWildcardRegionProfile(languageCode = "pt")
+ val profile2 = RegionOnlyProfile(regionCode = "br")
- val matches = englishWithWildcardProfile.matches(machineLocale, brazilianPortugueseProfile)
+ val matches = profile1.matches(profile2)
assertThat(matches).isFalse()
}
@Test
- fun testMatchProfile_brazilianPortuguese_portuguese_doNotMatch() {
- val portugueseProfile = AndroidLocaleProfile.createFrom(portugueseLocale)
- val brazilianPortugueseProfile = AndroidLocaleProfile.createFrom(brazilianPortugueseLocale)
+ fun testMatchProfile_ptWildcardProfile_andPtBrProfile_matches() {
+ val profile1 = LanguageAndWildcardRegionProfile(languageCode = "pt")
+ val profile2 =
+ LanguageAndRegionProfile(languageCode = "pt", regionCode = "br", regionCodeUpperCase = "BR")
+
+ val matches = profile1.matches(profile2)
+
+ assertThat(matches).isTrue()
+ }
+
+ @Test
+ fun testMatchProfile_ptWildcardProfile_andSwBrProfile_doNotMatch() {
+ val profile1 = LanguageAndWildcardRegionProfile(languageCode = "pt")
+ val profile2 =
+ LanguageAndRegionProfile(languageCode = "sw", regionCode = "br", regionCodeUpperCase = "BR")
- val matches = portugueseProfile.matches(machineLocale, brazilianPortugueseProfile)
+ val matches = profile1.matches(profile2)
assertThat(matches).isFalse()
}
@Test
- fun testMatchProfile_brazilianPortuguese_portugueseWithWildcard_match() {
- val portugueseWithWildcardProfile = createProfileWithWildcard(languageCode = "pt")
- val brazilianPortugueseProfile = AndroidLocaleProfile.createFrom(brazilianPortugueseLocale)
+ fun testMatchProfile_ptWildcardProfile_andPtKeProfile_matches() {
+ val profile1 = LanguageAndWildcardRegionProfile(languageCode = "pt")
+ val profile2 =
+ LanguageAndRegionProfile(languageCode = "pt", regionCode = "ke", regionCodeUpperCase = "KE")
- val matches = brazilianPortugueseProfile.matches(machineLocale, portugueseWithWildcardProfile)
+ val matches = profile1.matches(profile2)
assertThat(matches).isTrue()
}
@Test
- fun testMatchProfile_portugueseWithWildcard_brazilianPortuguese_match() {
- val portugueseWithWildcardProfile = createProfileWithWildcard(languageCode = "pt")
- val brazilianPortugueseProfile = AndroidLocaleProfile.createFrom(brazilianPortugueseLocale)
+ fun testMatchProfile_ptWildcardProfile_andSwKeProfile_doNotMatch() {
+ val profile1 = LanguageAndWildcardRegionProfile(languageCode = "pt")
+ val profile2 =
+ LanguageAndRegionProfile(languageCode = "sw", regionCode = "ke", regionCodeUpperCase = "KE")
- val matches = portugueseWithWildcardProfile.matches(machineLocale, brazilianPortugueseProfile)
+ val matches = profile1.matches(profile2)
+
+ assertThat(matches).isFalse()
+ }
+
+ @Test
+ fun testMatchProfile_ptWildcardProfile_andPtWildcardProfile_matches() {
+ val profile1 = LanguageAndWildcardRegionProfile(languageCode = "pt")
+ val profile2 = LanguageAndWildcardRegionProfile(languageCode = "pt")
+
+ val matches = profile1.matches(profile2)
assertThat(matches).isTrue()
}
+ @Test
+ fun testMatchProfile_ptWildcardProfile_andSwWildcardProfile_doNotMatch() {
+ val profile1 = LanguageAndWildcardRegionProfile(languageCode = "pt")
+ val profile2 = LanguageAndWildcardRegionProfile(languageCode = "sw")
+
+ val matches = profile1.matches(profile2)
+
+ assertThat(matches).isFalse()
+ }
+
/* Tests for computeIetfLanguageTag */
@Test
- fun testComputeIetfLanguageTag_noLanguageCode_noRegionCode_returnsEmptyString() {
- val emptyProfile = AndroidLocaleProfile(languageCode = "", regionCode = "")
+ fun testIetfLanguageTag_rootProfile_isEmptyString() {
+ val profile = RootProfile
- val ietfLanguageTag = emptyProfile.computeIetfLanguageTag()
+ val ietfLanguageTag = profile.ietfLanguageTag
assertThat(ietfLanguageTag).isEmpty()
}
@Test
- fun testComputeIetfLanguageTag_languageCode_noRegionCode_returnsLanguageCode() {
- val portugueseProfile = AndroidLocaleProfile(languageCode = "pt", regionCode = "")
+ fun testIetfLanguageTag_languageOnlyProfile_isLanguageCode() {
+ val profile = LanguageOnlyProfile(languageCode = "pt")
- val ietfLanguageTag = portugueseProfile.computeIetfLanguageTag()
+ val ietfLanguageTag = profile.ietfLanguageTag
assertThat(ietfLanguageTag).isEqualTo("pt")
}
@Test
- fun testComputeIetfLanguageTag_languageCode_wildcardRegionCode_returnsLanguageCode() {
- val portugueseAndRegionProfile = createProfileWithWildcard(languageCode = "pt")
+ fun testIetfLanguageTag_regionOnlyProfile_isRegionCode() {
+ val profile = RegionOnlyProfile(regionCode = "br")
- val ietfLanguageTag = portugueseAndRegionProfile.computeIetfLanguageTag()
+ val ietfLanguageTag = profile.ietfLanguageTag
+
+ assertThat(ietfLanguageTag).isEqualTo("br")
+ }
+
+ @Test
+ fun testIetfLanguageTag_languageWithRegionProfile_isIetfBcp47CombinedLanguageTag() {
+ val profile =
+ LanguageAndRegionProfile(languageCode = "pt", regionCode = "br", regionCodeUpperCase = "BR")
+
+ val ietfLanguageTag = profile.ietfLanguageTag
+
+ assertThat(ietfLanguageTag).isEqualTo("pt-BR")
+ }
+
+ @Test
+ fun testIetfLanguageTag_languageWithWildcardProfile_isLanguageCode() {
+ val profile = LanguageAndWildcardRegionProfile(languageCode = "pt")
+
+ val ietfLanguageTag = profile.ietfLanguageTag
// The wildcard shouldn't be part of the IETF BCP 47 tag since that standard doesn't define such
// a concept.
assertThat(ietfLanguageTag).isEqualTo("pt")
}
+ /* Tests for computeAndroidLocale() */
+
@Test
- fun testComputeIetfLanguageTag_noLanguageCode_regionCode_returnsRegionCode() {
- val brazilianProfile = AndroidLocaleProfile(languageCode = "", regionCode = "BR")
+ fun testComputeAndroidLocale_rootProfile_returnsRootLocale() {
+ val profile = RootProfile
- val ietfLanguageTag = brazilianProfile.computeIetfLanguageTag()
+ val locale = profile.computeAndroidLocale()
- assertThat(ietfLanguageTag).isEqualTo("BR")
+ assertThat(locale).isEqualTo(Locale.ROOT)
}
@Test
- fun testComputeIetfLanguageTag_noLanguageCode_wildcardRegionCode_returnsEmptyString() {
- val matchNothingProfile = createProfileWithWildcard(languageCode = "")
+ fun testComputeAndroidLocale_languageOnlyProfile_returnsLocaleWithLanguageAndEmptyCountry() {
+ val profile = LanguageOnlyProfile(languageCode = "pt")
- val ietfLanguageTag = matchNothingProfile.computeIetfLanguageTag()
+ val locale = profile.computeAndroidLocale()
- assertThat(ietfLanguageTag).isEmpty()
+ assertThat(locale.language).ignoringCase().isEqualTo("pt")
+ assertThat(locale.country).isEmpty()
}
@Test
- fun testComputeIetfLanguageTag_languageCode_regionCode_returnsIetfBcp47CombinedLanguageTag() {
- val brazilianPortugueseProfile = AndroidLocaleProfile(languageCode = "pt", regionCode = "BR")
+ fun testComputeAndroidLocale_regionOnlyProfile_returnsLocaleWithCountryAndEmptyLanguage() {
+ val profile = RegionOnlyProfile(regionCode = "br")
- val ietfLanguageTag = brazilianPortugueseProfile.computeIetfLanguageTag()
+ val locale = profile.computeAndroidLocale()
- assertThat(ietfLanguageTag).isEqualTo("pt-BR")
+ assertThat(locale.country).ignoringCase().isEqualTo("br")
+ assertThat(locale.language).isEmpty()
+ }
+
+ @Test
+ fun testComputeAndroidLocale_languageWithWildcardProfile_returnsLocaleWithLangAndEmptyCountry() {
+ val profile = LanguageAndWildcardRegionProfile(languageCode = "pt")
+
+ val locale = profile.computeAndroidLocale()
+
+ assertThat(locale.language).ignoringCase().isEqualTo("pt")
+ assertThat(locale.country).isEmpty()
}
- private fun createProfileWithWildcard(languageCode: String): AndroidLocaleProfile =
- AndroidLocaleProfile(languageCode, regionCode = AndroidLocaleProfile.REGION_WILDCARD)
+ @Test
+ fun testComputeAndroidLocale_languageAndRegionProfile_returnsLocaleWithLanguageAndCountry() {
+ val profile =
+ LanguageAndRegionProfile(languageCode = "pt", regionCode = "br", regionCodeUpperCase = "BR")
+
+ val locale = profile.computeAndroidLocale()
+
+ assertThat(locale.language).ignoringCase().isEqualTo("pt")
+ assertThat(locale.country).ignoringCase().isEqualTo("br")
+ }
private fun setUpTestApplicationComponent() {
DaggerAndroidLocaleProfileTest_TestApplicationComponent.builder()
diff --git a/utility/src/test/java/org/oppia/android/util/locale/BUILD.bazel b/utility/src/test/java/org/oppia/android/util/locale/BUILD.bazel
index 850e0d05370..d1bc33a3988 100644
--- a/utility/src/test/java/org/oppia/android/util/locale/BUILD.bazel
+++ b/utility/src/test/java/org/oppia/android/util/locale/BUILD.bazel
@@ -14,6 +14,8 @@ oppia_android_test(
deps = [
":dagger",
"//testing",
+ "//testing/src/main/java/org/oppia/android/testing/robolectric:test_module",
+ "//testing/src/main/java/org/oppia/android/testing/threading:test_module",
"//testing/src/main/java/org/oppia/android/testing/time:test_module",
"//third_party:androidx_test_ext_junit",
"//third_party:com_google_truth_truth",
@@ -72,6 +74,8 @@ oppia_android_test(
":dagger",
"//model/src/main/proto:languages_java_proto_lite",
"//testing",
+ "//testing/src/main/java/org/oppia/android/testing/robolectric:test_module",
+ "//testing/src/main/java/org/oppia/android/testing/threading:test_module",
"//testing/src/main/java/org/oppia/android/testing/time:test_module",
"//third_party:androidx_test_ext_junit",
"//third_party:com_google_truth_extensions_truth-liteproto-extension",
diff --git a/utility/src/test/java/org/oppia/android/util/locale/DisplayLocaleImplTest.kt b/utility/src/test/java/org/oppia/android/util/locale/DisplayLocaleImplTest.kt
index e300a47d6fa..dfc1ba84b79 100644
--- a/utility/src/test/java/org/oppia/android/util/locale/DisplayLocaleImplTest.kt
+++ b/utility/src/test/java/org/oppia/android/util/locale/DisplayLocaleImplTest.kt
@@ -21,6 +21,8 @@ import org.oppia.android.app.model.OppiaLocaleContext
import org.oppia.android.app.model.OppiaRegion
import org.oppia.android.app.model.RegionSupportDefinition
import org.oppia.android.testing.assertThrows
+import org.oppia.android.testing.robolectric.RobolectricModule
+import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.locale.testing.LocaleTestModule
import org.oppia.android.util.locale.testing.TestOppiaBidiFormatter
@@ -56,13 +58,6 @@ class DisplayLocaleImplTest {
setUpTestApplicationComponent()
}
- @Test
- fun testCreateDisplayLocaleImpl_defaultInstance_hasDefaultInstanceContext() {
- val impl = createDisplayLocaleImpl(OppiaLocaleContext.getDefaultInstance())
-
- assertThat(impl.localeContext).isEqualToDefaultInstance()
- }
-
@Test
fun testCreateDisplayLocaleImpl_forProvidedContext_hasCorrectInstanceContext() {
val impl = createDisplayLocaleImpl(EGYPT_ARABIC_CONTEXT)
@@ -72,7 +67,7 @@ class DisplayLocaleImplTest {
@Test
fun testToString_returnsNonDefaultString() {
- val impl = createDisplayLocaleImpl(OppiaLocaleContext.getDefaultInstance())
+ val impl = createDisplayLocaleImpl(EGYPT_ARABIC_CONTEXT)
val str = impl.toString()
@@ -83,14 +78,14 @@ class DisplayLocaleImplTest {
@Test
fun testEquals_withNullValue_returnsFalse() {
- val impl = createDisplayLocaleImpl(OppiaLocaleContext.getDefaultInstance())
+ val impl = createDisplayLocaleImpl(EGYPT_ARABIC_CONTEXT)
assertThat(impl).isNotEqualTo(null)
}
@Test
fun testEquals_withSameObject_returnsTrue() {
- val impl = createDisplayLocaleImpl(OppiaLocaleContext.getDefaultInstance())
+ val impl = createDisplayLocaleImpl(EGYPT_ARABIC_CONTEXT)
assertThat(impl).isEqualTo(impl)
}
@@ -615,8 +610,10 @@ class DisplayLocaleImplTest {
}
}
- private fun createDisplayLocaleImpl(context: OppiaLocaleContext): DisplayLocaleImpl =
- DisplayLocaleImpl(context, machineLocale, androidLocaleFactory, formatterFactory)
+ private fun createDisplayLocaleImpl(context: OppiaLocaleContext): DisplayLocaleImpl {
+ val formattingLocale = androidLocaleFactory.createOneOffAndroidLocale(context)
+ return DisplayLocaleImpl(context, formattingLocale, machineLocale, formatterFactory)
+ }
private fun setUpTestApplicationComponent() {
DaggerDisplayLocaleImplTest_TestApplicationComponent.builder()
@@ -639,7 +636,8 @@ class DisplayLocaleImplTest {
@Singleton
@Component(
modules = [
- TestModule::class, LocaleTestModule::class, FakeOppiaClockModule::class
+ TestModule::class, LocaleTestModule::class, FakeOppiaClockModule::class,
+ TestDispatcherModule::class, RobolectricModule::class
]
)
interface TestApplicationComponent {
diff --git a/utility/src/test/java/org/oppia/android/util/parser/html/CustomHtmlContentHandlerTest.kt b/utility/src/test/java/org/oppia/android/util/parser/html/CustomHtmlContentHandlerTest.kt
index 5cba77e357d..2042ef72ec8 100644
--- a/utility/src/test/java/org/oppia/android/util/parser/html/CustomHtmlContentHandlerTest.kt
+++ b/utility/src/test/java/org/oppia/android/util/parser/html/CustomHtmlContentHandlerTest.kt
@@ -311,8 +311,10 @@ class CustomHtmlContentHandlerTest {
)
}
- private fun createDisplayLocaleImpl(context: OppiaLocaleContext): DisplayLocaleImpl =
- DisplayLocaleImpl(context, machineLocale, androidLocaleFactory, formatterFactory)
+ private fun createDisplayLocaleImpl(context: OppiaLocaleContext): DisplayLocaleImpl {
+ val formattingLocale = androidLocaleFactory.createOneOffAndroidLocale(context)
+ return DisplayLocaleImpl(context, formattingLocale, machineLocale, formatterFactory)
+ }
private class FakeTagHandler : CustomHtmlContentHandler.CustomTagHandler {
var handleTagCalled = false
diff --git a/utility/src/test/java/org/oppia/android/util/parser/html/LiTagHandlerTest.kt b/utility/src/test/java/org/oppia/android/util/parser/html/LiTagHandlerTest.kt
index 46e4ca83387..1341bc68776 100644
--- a/utility/src/test/java/org/oppia/android/util/parser/html/LiTagHandlerTest.kt
+++ b/utility/src/test/java/org/oppia/android/util/parser/html/LiTagHandlerTest.kt
@@ -137,8 +137,10 @@ class LiTagHandlerTest {
.hasLength(4)
}
- private fun createDisplayLocaleImpl(context: OppiaLocaleContext): DisplayLocaleImpl =
- DisplayLocaleImpl(context, machineLocale, androidLocaleFactory, formatterFactory)
+ private fun createDisplayLocaleImpl(context: OppiaLocaleContext): DisplayLocaleImpl {
+ val formattingLocale = androidLocaleFactory.createOneOffAndroidLocale(context)
+ return DisplayLocaleImpl(context, formattingLocale, machineLocale, formatterFactory)
+ }
private fun Spannable.getSpansFromWholeString(spanClass: KClass): Array =
getSpans(/* start= */ 0, /* end= */ length, spanClass.javaObjectType)