diff --git a/domain/src/main/java/org/oppia/android/domain/locale/AndroidLocaleFactory.kt b/domain/src/main/java/org/oppia/android/domain/locale/AndroidLocaleFactory.kt new file mode 100644 index 00000000000..069e0e8b7ff --- /dev/null +++ b/domain/src/main/java/org/oppia/android/domain/locale/AndroidLocaleFactory.kt @@ -0,0 +1,131 @@ +package org.oppia.android.domain.locale + +import android.os.Build +import java.util.Locale +import javax.inject.Inject +import org.oppia.android.app.model.LanguageSupportDefinition +import org.oppia.android.app.model.LanguageSupportDefinition.LanguageId +import org.oppia.android.app.model.OppiaLocaleContext +import org.oppia.android.app.model.RegionSupportDefinition +import org.oppia.android.util.locale.OppiaLocale + +class AndroidLocaleFactory @Inject constructor( + private val machineLocale: OppiaLocale.MachineLocale +) { + fun createAndroidLocale(localeContext: OppiaLocaleContext): Locale { + val languageId = localeContext.getLanguageId() + val fallbackLanguageId = localeContext.getFallbackLanguageId() + + // Locale is always computed based on the Android resource app string identifier if that's + // defined. If it isn't, the routine falls back to app language & region country codes (which + // also provides interoperability with system-derived contexts). Note that if either identifier + // is missing for the primary language, the fallback is used instead (if available), except that + // IETF BCP 47 tags from the primary language are used before Android resource codes from the + // fallback. Thus, the order of this list is important. Finally, a basic check is done here to + // make sure this version of Android can actually render the target language. + val potentialProfiles = + computePotentialLanguageProfiles(localeContext, languageId) + + computePotentialFallbackLanguageProfiles(localeContext, fallbackLanguageId) + + // Either find the first supported profile or force the locale to use the exact definition + // values, depending on whether to fail over to a forced locale. + val firstSupportedProfile = potentialProfiles.findFirstSupported() + val selectedProfile = firstSupportedProfile + ?: languageId.computeForcedProfile(localeContext.regionDefinition) + return Locale(selectedProfile.languageCode, selectedProfile.regionCode) + } + + private fun computePotentialLanguageProfiles( + localeContext: OppiaLocaleContext, + languageId: LanguageId + ): List = + computeLanguageProfiles(localeContext, localeContext.languageDefinition, languageId) + + private fun computePotentialFallbackLanguageProfiles( + localeContext: OppiaLocaleContext, + fallbackLanguageId: LanguageId + ): List { + return computeLanguageProfiles( + localeContext, localeContext.fallbackLanguageDefinition, fallbackLanguageId + ) + } + + private fun computeLanguageProfiles( + localeContext: OppiaLocaleContext, + definition: LanguageSupportDefinition, + languageId: LanguageId + ): List { + return if (definition.minAndroidSdkVersion <= Build.VERSION.SDK_INT) { + listOf( + languageId.computeLocaleProfileFromAndroidId(), + languageId.computeLocaleProfileFromIetfDefinitions(localeContext.regionDefinition), + languageId.computeLocaleProfileFromMacaronicLanguage() + ) + } else listOf() + } + + private fun LanguageId.computeLocaleProfileFromAndroidId(): AndroidLocaleProfile? { + return if (hasAndroidResourcesLanguageId()) { + androidResourcesLanguageId.run { maybeConstructProfile(languageCode, regionCode) } + } else null + } + + private fun LanguageId.computeLocaleProfileFromIetfDefinitions( + regionDefinition: RegionSupportDefinition + ): AndroidLocaleProfile? { + if (!hasIetfBcp47Id()) return null + if (!regionDefinition.hasRegionId()) return null + return maybeConstructProfile( + ietfBcp47Id.ietfLanguageTag, regionDefinition.regionId.ietfRegionTag + ) + } + + private fun LanguageId.computeLocaleProfileFromMacaronicLanguage(): AndroidLocaleProfile? { + if (!hasMacaronicId()) return null + val (languageCode, regionCode) = macaronicId.combinedLanguageCode.divide("-") ?: return null + return maybeConstructProfile(languageCode, regionCode) + } + + /** + * Returns an [AndroidLocaleProfile] for this [LanguageId] and the specified + * [RegionSupportDefinition] based on the language's & region's IETF BCP 47 codes regardless of + * whether they're defined (i.e. it's fine to default to empty string here since that will + * leverage Android's own root locale behavior). + */ + private fun LanguageId.computeForcedProfile( + regionDefinition: RegionSupportDefinition + ): AndroidLocaleProfile { + return AndroidLocaleProfile( + ietfBcp47Id.ietfLanguageTag, regionDefinition.regionId.ietfRegionTag + ) + } + + private fun maybeConstructProfile( + languageCode: String, regionCode: String + ): AndroidLocaleProfile? { + return if (languageCode.isNotEmpty() && regionCode.isNotEmpty()) { + AndroidLocaleProfile(languageCode, regionCode) + } else null + } + + private fun List.findFirstSupported(): AndroidLocaleProfile? = find { + it?.let { profileToMatch -> + availableLocaleProfiles.any { availableProfile -> + availableProfile.matches(machineLocale, profileToMatch) + } + } ?: false // Ignore null profiles. + } + + private companion object { + private val availableLocaleProfiles by lazy { + Locale.getAvailableLocales().map(AndroidLocaleProfile::createFrom) + } + + private fun String.divide(delimiter: String): Pair? { + val results = split(delimiter) + return if (results.size == 2) { + results[0] to results[1] + } else null + } + } +} diff --git a/domain/src/main/java/org/oppia/android/domain/locale/BUILD.bazel b/domain/src/main/java/org/oppia/android/domain/locale/BUILD.bazel index d51240fad97..fd8f4ca2a64 100644 --- a/domain/src/main/java/org/oppia/android/domain/locale/BUILD.bazel +++ b/domain/src/main/java/org/oppia/android/domain/locale/BUILD.bazel @@ -24,12 +24,34 @@ kt_android_library( ) kt_android_library( - name = "display_locale_impl", + name = "android_locale_factory", + srcs = [ + "AndroidLocaleFactory.kt", + ], + deps = [ + ":android_locale_profile", + ":dagger", + ], +) + +kt_android_library( + name = "android_locale_profile", srcs = [ "AndroidLocaleProfile.kt", + ], + deps = [ + "//utility/src/main/java/org/oppia/android/util/locale:oppia_locale", + ], +) + +kt_android_library( + name = "display_locale_impl", + srcs = [ "DisplayLocaleImpl.kt", ], deps = [ + ":android_locale_factory", + "//third_party:androidx_core_core", "//utility/src/main/java/org/oppia/android/util/data:data_providers", "//utility/src/main/java/org/oppia/android/util/locale:oppia_locale", ], diff --git a/domain/src/main/java/org/oppia/android/domain/locale/DisplayLocaleImpl.kt b/domain/src/main/java/org/oppia/android/domain/locale/DisplayLocaleImpl.kt index 4a68d3c4620..164b2ccdf43 100644 --- a/domain/src/main/java/org/oppia/android/domain/locale/DisplayLocaleImpl.kt +++ b/domain/src/main/java/org/oppia/android/domain/locale/DisplayLocaleImpl.kt @@ -6,6 +6,7 @@ import android.os.Build import android.text.BidiFormatter import androidx.annotation.ArrayRes import androidx.annotation.StringRes +import androidx.core.text.TextUtilsCompat import java.text.DateFormat import java.util.Date import java.util.Locale @@ -19,10 +20,11 @@ import org.oppia.android.util.locale.OppiaLocale // TODO(#3766): Restrict to be 'internal'. class DisplayLocaleImpl( localeContext: OppiaLocaleContext, - private val machineLocale: MachineLocale + private val machineLocale: MachineLocale, + private val androidLocaleFactory: AndroidLocaleFactory ): OppiaLocale.DisplayLocale(localeContext) { // TODO(#3766): Restrict to be 'internal'. - val formattingLocale: Locale by lazy { computeLocale() } + val formattingLocale: Locale by lazy { androidLocaleFactory.createAndroidLocale(localeContext) } private val dateFormat by lazy { DateFormat.getDateInstance(DATE_FORMAT_LENGTH, formattingLocale) } @@ -48,6 +50,10 @@ class DisplayLocaleImpl( override fun computeDateTimeString(timestampMillis: Long): String = dateTimeFormat.format(Date(timestampMillis)) + override fun getLayoutDirection(): Int { + return TextUtilsCompat.getLayoutDirectionFromLocale(formattingLocale) + } + override fun String.formatInLocale(vararg args: Any?): String = format(formattingLocale, *args.map { arg -> if (arg is CharSequence) bidiFormatter.unicodeWrap(arg) else arg @@ -83,110 +89,8 @@ class DisplayLocaleImpl( override fun hashCode(): Int = Objects.hash(localeContext, machineLocale) - private fun computeLocale(): Locale { - // Locale is always computed based on the Android resource app string identifier if that's - // defined. If it isn't, the routine falls back to app language & region country codes (which - // also provides interoperability with system-derived contexts). Note that if either identifier - // is missing for the primary language, the fallback is used instead (if available), except that - // IETF BCP 47 tags from the primary language are used before Android resource codes from the - // fallback. Thus, the order of this list is important. Finally, a basic check is done here to - // make sure this version of Android can actually render the target language. - val potentialProfiles = - computePotentialLanguageProfiles() + computePotentialFallbackLanguageProfiles() - - // Either find the first supported profile or force the locale to use the exact definition - // values. - val selectedProfile = - potentialProfiles.findFirstSupported() - ?: getLanguageId().computeForcedProfile(localeContext.regionDefinition) - - return Locale(selectedProfile.languageCode, selectedProfile.regionCode) - } - - private fun computePotentialLanguageProfiles(): List = - computeLanguageProfiles(localeContext.languageDefinition, getLanguageId()) - - private fun computePotentialFallbackLanguageProfiles(): List = - computeLanguageProfiles(localeContext.fallbackLanguageDefinition, getFallbackLanguageId()) - - private fun computeLanguageProfiles( - definition: LanguageSupportDefinition, - languageId: LanguageId - ): List { - return if (definition.minAndroidSdkVersion <= Build.VERSION.SDK_INT) { - listOf( - languageId.computeLocaleProfileFromAndroidId(), - languageId.computeLocaleProfileFromIetfDefinitions(localeContext.regionDefinition), - languageId.computeLocaleProfileFromMacaronicLanguage() - ) - } else listOf() - } - - private fun LanguageId.computeLocaleProfileFromAndroidId(): AndroidLocaleProfile? { - return if (hasAndroidResourcesLanguageId()) { - androidResourcesLanguageId.run { maybeConstructProfile(languageCode, regionCode) } - } else null - } - - private fun LanguageId.computeLocaleProfileFromIetfDefinitions( - regionDefinition: RegionSupportDefinition - ): AndroidLocaleProfile? { - if (!hasIetfBcp47Id()) return null - if (!regionDefinition.hasRegionId()) return null - return maybeConstructProfile( - ietfBcp47Id.ietfLanguageTag, regionDefinition.regionId.ietfRegionTag - ) - } - - private fun LanguageId.computeLocaleProfileFromMacaronicLanguage(): AndroidLocaleProfile? { - if (!hasMacaronicId()) return null - val (languageCode, regionCode) = macaronicId.combinedLanguageCode.divide("-") ?: return null - return maybeConstructProfile(languageCode, regionCode) - } - - /** - * Returns an [AndroidLocaleProfile] for this [LanguageId] and the specified - * [RegionSupportDefinition] based on the language's & region's IETF BCP 47 codes regardless of - * whether they're defined (i.e. it's fine to default to empty string here since that will - * leverage Android's own root locale behavior). - */ - private fun LanguageId.computeForcedProfile( - regionDefinition: RegionSupportDefinition - ): AndroidLocaleProfile { - return AndroidLocaleProfile( - ietfBcp47Id.ietfLanguageTag, regionDefinition.regionId.ietfRegionTag - ) - } - - private fun maybeConstructProfile( - languageCode: String, regionCode: String - ): AndroidLocaleProfile? { - return if (languageCode.isNotEmpty() && regionCode.isNotEmpty()) { - AndroidLocaleProfile(languageCode, regionCode) - } else null - } - - private fun List.findFirstSupported(): AndroidLocaleProfile? = find { - it?.let { profileToMatch -> - availableLocaleProfiles.any { availableProfile -> - availableProfile.matches(machineLocale, profileToMatch) - } - } ?: false // Ignore null profiles. - } - private companion object { private const val DATE_FORMAT_LENGTH = DateFormat.LONG private const val TIME_FORMAT_LENGTH = DateFormat.SHORT - - private val availableLocaleProfiles by lazy { - Locale.getAvailableLocales().map(AndroidLocaleProfile::createFrom) - } } } - -private fun String.divide(delimiter: String): Pair? { - val results = split(delimiter) - return if (results.size == 2) { - results[0] to results[1] - } else null -} 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 a374500bc56..4962a8320e0 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 @@ -2,34 +2,34 @@ package org.oppia.android.domain.locale import android.content.Context import android.content.res.Configuration -import org.oppia.android.app.model.LanguageSupportDefinition -import org.oppia.android.app.model.OppiaLanguage -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.app.model.SupportedLanguages -import org.oppia.android.app.model.SupportedRegions -import org.oppia.android.util.data.DataProvider import java.util.Locale -import org.oppia.android.domain.oppialogger.OppiaLogger import java.util.concurrent.locks.ReentrantLock import javax.inject.Inject import javax.inject.Singleton import kotlin.concurrent.withLock +import org.oppia.android.app.model.LanguageSupportDefinition +import org.oppia.android.app.model.OppiaLanguage +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.app.model.OppiaLocaleContext.LanguageUsageMode.AUDIO_TRANSLATIONS import org.oppia.android.app.model.OppiaLocaleContext.LanguageUsageMode.CONTENT_STRINGS import org.oppia.android.app.model.OppiaLocaleContext.LanguageUsageMode.UNRECOGNIZED import org.oppia.android.app.model.OppiaLocaleContext.LanguageUsageMode.USAGE_MODE_UNSPECIFIED -import org.oppia.android.util.locale.OppiaLocale.ContentLocale -import org.oppia.android.util.locale.OppiaLocale.DisplayLocale -import org.oppia.android.util.locale.OppiaLocale.MachineLocale +import org.oppia.android.app.model.OppiaRegion +import org.oppia.android.app.model.RegionSupportDefinition +import org.oppia.android.app.model.SupportedLanguages +import org.oppia.android.app.model.SupportedRegions +import org.oppia.android.domain.oppialogger.OppiaLogger import org.oppia.android.util.data.AsyncDataSubscriptionManager import org.oppia.android.util.data.AsyncResult +import org.oppia.android.util.data.DataProvider import org.oppia.android.util.data.DataProviders import org.oppia.android.util.data.DataProviders.Companion.transformAsync import org.oppia.android.util.locale.OppiaLocale +import org.oppia.android.util.locale.OppiaLocale.ContentLocale +import org.oppia.android.util.locale.OppiaLocale.DisplayLocale +import org.oppia.android.util.locale.OppiaLocale.MachineLocale // TODO: document how notifications work (everything is rooted from changing Locale). private const val ANDROID_SYSTEM_LOCALE_DATA_PROVIDER_ID = "android_locale" @@ -45,7 +45,8 @@ class LocaleController @Inject constructor( private val languageConfigRetriever: LanguageConfigRetriever, private val oppiaLogger: OppiaLogger, private val asyncDataSubscriptionManager: AsyncDataSubscriptionManager, - private val machineLocale: MachineLocale + private val machineLocale: MachineLocale, + private val androidLocaleFactory: AndroidLocaleFactory ) { private val definitionsLock = ReentrantLock() private lateinit var supportedLanguages: SupportedLanguages @@ -80,7 +81,7 @@ class LocaleController @Inject constructor( // TODO: this should also work in cases when the process dies. fun reconstituteDisplayLocale(oppiaLocaleContext: OppiaLocaleContext): DisplayLocale { - return DisplayLocaleImpl(oppiaLocaleContext, machineLocale) + return DisplayLocaleImpl(oppiaLocaleContext, machineLocale, androidLocaleFactory) } // TODO: document @@ -137,7 +138,6 @@ class LocaleController @Inject constructor( // consistency by always retrieving the latest state when requested. This does mean locale // changes can be missed if they aren't accompanied by a configuration change or activity // recreation. - // TODO: add regex prohibiting this Locale.setDefault(locale.formattingLocale) notifyPotentialLocaleChange() } ?: error("Invalid display locale type passed in: $displayLocale") @@ -218,7 +218,7 @@ class LocaleController @Inject constructor( } return when (usageMode) { - APP_STRINGS -> DisplayLocaleImpl(localeContext, machineLocale) + APP_STRINGS -> DisplayLocaleImpl(localeContext, machineLocale, androidLocaleFactory) CONTENT_STRINGS, AUDIO_TRANSLATIONS -> ContentLocale(localeContext) USAGE_MODE_UNSPECIFIED, UNRECOGNIZED -> null } 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 3f1eb97a711..49dc2aa93bd 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 = [ + ":oppia_locale_context_extensions", "//model:languages_java_proto_lite", "//third_party:androidx_annotation_annotation", ], @@ -29,6 +30,16 @@ kt_android_library( ], ) +kt_android_library( + name = "oppia_locale_context_extensions", + srcs = [ + "OppiaLocaleContextExtensions.kt", + ], + deps = [ + "//model:languages_java_proto_lite", + ], +) + kt_android_library( name = "prod_module", srcs = [ diff --git a/utility/src/main/java/org/oppia/android/util/locale/MachineLocaleImpl.kt b/utility/src/main/java/org/oppia/android/util/locale/MachineLocaleImpl.kt index 4289c207500..2879a9dad58 100644 --- a/utility/src/main/java/org/oppia/android/util/locale/MachineLocaleImpl.kt +++ b/utility/src/main/java/org/oppia/android/util/locale/MachineLocaleImpl.kt @@ -1,5 +1,6 @@ package org.oppia.android.util.locale +import java.text.DateFormat import java.text.ParseException import java.text.SimpleDateFormat import java.util.Calendar @@ -19,6 +20,9 @@ class MachineLocaleImpl @Inject constructor( private val oppiaClock: OppiaClock ): OppiaLocale.MachineLocale(machineLocaleContext) { private val parsableDateFormat by lazy { SimpleDateFormat("yyyy-MM-dd", machineAndroidLocale) } + private val timeFormat by lazy { + DateFormat.getTimeInstance(DateFormat.SHORT, machineAndroidLocale) + } override fun String.formatForMachines(vararg args: Any?): String = format(machineAndroidLocale, *args) @@ -55,6 +59,8 @@ class MachineLocaleImpl @Inject constructor( return parsedDate?.let { OppiaDateImpl(it, oppiaClock.getCurrentDate()) } } + override fun computeCurrentTimeString(): String = timeFormat.format(Date(oppiaClock.getCurrentTimeMs())) + override fun toString(): String = "MachineLocaleImpl[context=$machineLocaleContext]" override fun equals(other: Any?): Boolean { diff --git a/utility/src/main/java/org/oppia/android/util/locale/OppiaLocale.kt b/utility/src/main/java/org/oppia/android/util/locale/OppiaLocale.kt index 0c6ce3f19b1..09b19e84b78 100644 --- a/utility/src/main/java/org/oppia/android/util/locale/OppiaLocale.kt +++ b/utility/src/main/java/org/oppia/android/util/locale/OppiaLocale.kt @@ -13,28 +13,20 @@ import org.oppia.android.app.model.OppiaLocaleContext.LanguageUsageMode.CONTENT_ import org.oppia.android.app.model.OppiaLocaleContext.LanguageUsageMode.UNRECOGNIZED import org.oppia.android.app.model.OppiaLocaleContext.LanguageUsageMode.USAGE_MODE_UNSPECIFIED import org.oppia.android.app.model.OppiaRegion +import org.oppia.android.domain.locale.getFallbackLanguageId +import org.oppia.android.domain.locale.getLanguageId // TOOD: document that equals, tostring, and hashcode are all properly implemented for subclasses? sealed class OppiaLocale { - protected abstract val localeContext: OppiaLocaleContext + abstract val localeContext: OppiaLocaleContext // TODO: verify exclusivity of regions/languages table in tests. fun getCurrentLanguage(): OppiaLanguage = localeContext.languageDefinition.language - fun getLanguageId(): LanguageId = when (localeContext.usageMode) { - APP_STRINGS -> localeContext.languageDefinition.appStringId - CONTENT_STRINGS -> localeContext.languageDefinition.contentStringId - AUDIO_TRANSLATIONS -> localeContext.languageDefinition.audioTranslationId - USAGE_MODE_UNSPECIFIED, UNRECOGNIZED, null -> LanguageId.getDefaultInstance() - } + fun getLanguageId(): LanguageId = localeContext.getLanguageId() - fun getFallbackLanguageId(): LanguageId = when (localeContext.usageMode) { - APP_STRINGS -> localeContext.fallbackLanguageDefinition.appStringId - CONTENT_STRINGS -> localeContext.fallbackLanguageDefinition.contentStringId - AUDIO_TRANSLATIONS -> localeContext.fallbackLanguageDefinition.audioTranslationId - USAGE_MODE_UNSPECIFIED, UNRECOGNIZED, null -> LanguageId.getDefaultInstance() - } + fun getFallbackLanguageId(): LanguageId = localeContext.getFallbackLanguageId() fun getCurrentRegion(): OppiaRegion = localeContext.regionDefinition.region @@ -61,6 +53,9 @@ sealed class OppiaLocale { // (which isn't tied to the locale). abstract fun parseOppiaDate(dateString: String): OppiaDate? + // TODO: document that this computes time not considering the locale and should only be used for machine cases (like log statements). + abstract fun computeCurrentTimeString(): String + enum class TimeOfDay { MORNING, AFTERNOON, @@ -80,6 +75,8 @@ sealed class OppiaLocale { abstract fun computeDateTimeString(timestampMillis: Long): String + abstract fun getLayoutDirection(): Int + // TODO: mention bidi wrapping (only applied to strings) & machine readable args // TODO: document that receiver is the format (unlike String.format). abstract fun String.formatInLocale(vararg args: Any?): String diff --git a/utility/src/main/java/org/oppia/android/util/locale/OppiaLocaleContextExtensions.kt b/utility/src/main/java/org/oppia/android/util/locale/OppiaLocaleContextExtensions.kt new file mode 100644 index 00000000000..07fa3dc8a40 --- /dev/null +++ b/utility/src/main/java/org/oppia/android/util/locale/OppiaLocaleContextExtensions.kt @@ -0,0 +1,27 @@ +package org.oppia.android.domain.locale + +import org.oppia.android.app.model.LanguageSupportDefinition.LanguageId +import org.oppia.android.app.model.OppiaLocaleContext +import org.oppia.android.app.model.OppiaLocaleContext.LanguageUsageMode.APP_STRINGS +import org.oppia.android.app.model.OppiaLocaleContext.LanguageUsageMode.AUDIO_TRANSLATIONS +import org.oppia.android.app.model.OppiaLocaleContext.LanguageUsageMode.CONTENT_STRINGS +import org.oppia.android.app.model.OppiaLocaleContext.LanguageUsageMode.UNRECOGNIZED +import org.oppia.android.app.model.OppiaLocaleContext.LanguageUsageMode.USAGE_MODE_UNSPECIFIED + +fun OppiaLocaleContext.getLanguageId(): LanguageId { + return when (usageMode) { + APP_STRINGS -> languageDefinition.appStringId + CONTENT_STRINGS -> languageDefinition.contentStringId + AUDIO_TRANSLATIONS -> languageDefinition.audioTranslationId + USAGE_MODE_UNSPECIFIED, UNRECOGNIZED, null -> LanguageId.getDefaultInstance() + } +} + +fun OppiaLocaleContext.getFallbackLanguageId(): LanguageId { + return when (usageMode) { + APP_STRINGS -> fallbackLanguageDefinition.appStringId + CONTENT_STRINGS -> fallbackLanguageDefinition.contentStringId + AUDIO_TRANSLATIONS -> fallbackLanguageDefinition.audioTranslationId + USAGE_MODE_UNSPECIFIED, UNRECOGNIZED, null -> LanguageId.getDefaultInstance() + } +}