From 8702ac886b317241170ba43e9b123211ec90cf5a Mon Sep 17 00:00:00 2001 From: Alexandre G Pereira Date: Tue, 23 Jan 2024 21:15:15 -0300 Subject: [PATCH] Create app localization core module --- app/build.gradle.kts | 1 + .../hunter/app/HunterApplication.kt | 4 ++- core/localization/build.gradle.kts | 19 ++++++++++++ .../hunter/localization/Localization.kt | 29 +++++++++++++++++ .../localization/di/LocalizationModule.kt | 12 +++++++ .../hunter/state/StateHolder.kt | 4 --- domain/settings/core/build.gradle.kts | 1 + .../domain/settings/GetLanguageUseCase.kt | 9 ++++-- .../domain/settings/IsLanguageSupported.kt | 4 ++- .../domain/settings/SaveLanguageUseCase.kt | 13 ++++++-- .../hunter/domain/settings/di/DomainModule.kt | 4 +-- .../compendium/ui/SpellCompendiumScreen.kt | 2 +- .../state-holder/build.gradle.kts | 1 + .../spell/compendium/SpellCompendiumState.kt | 1 + .../compendium/SpellCompendiumStateHolder.kt | 15 ++++++--- .../compendium/SpellCompendiumStrings.kt | 31 +++++++++++++++++++ .../hunter/spell/compendium/di/Module.kt | 3 +- settings.gradle | 1 + shared/build.gradle.kts | 1 + .../hunter/shared/di/AppModule.kt | 2 ++ 20 files changed, 138 insertions(+), 19 deletions(-) create mode 100644 core/localization/build.gradle.kts create mode 100644 core/localization/src/commonMain/kotlin/br/alexandregpereira/hunter/localization/Localization.kt create mode 100644 core/localization/src/commonMain/kotlin/br/alexandregpereira/hunter/localization/di/LocalizationModule.kt create mode 100644 feature/spell-compendium/state-holder/src/commonMain/kotlin/br/alexandregpereira/hunter/spell/compendium/SpellCompendiumStrings.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 8bd906f3..0fdd0f97 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -103,6 +103,7 @@ java { dependencies { implementation(project(":core:analytics")) implementation(project(":core:event")) + implementation(project(":core:localization")) implementation(project(":domain:app:data")) implementation(project(":domain:app:core")) implementation(project(":feature:folder-detail:event")) diff --git a/app/src/main/kotlin/br/alexandregpereira/hunter/app/HunterApplication.kt b/app/src/main/kotlin/br/alexandregpereira/hunter/app/HunterApplication.kt index a3f10d86..740b354d 100644 --- a/app/src/main/kotlin/br/alexandregpereira/hunter/app/HunterApplication.kt +++ b/app/src/main/kotlin/br/alexandregpereira/hunter/app/HunterApplication.kt @@ -26,6 +26,7 @@ import br.alexandregpereira.hunter.folder.detail.di.folderDetailModule import br.alexandregpereira.hunter.folder.insert.di.folderInsertModule import br.alexandregpereira.hunter.folder.list.di.folderListModule import br.alexandregpereira.hunter.folder.preview.di.folderPreviewModule +import br.alexandregpereira.hunter.localization.di.localizationModule import br.alexandregpereira.hunter.monster.compendium.di.monsterCompendiumModule import br.alexandregpereira.hunter.monster.content.di.monsterContentManagerModule import br.alexandregpereira.hunter.monster.content.preview.di.monsterContentPreviewModule @@ -95,7 +96,8 @@ class HunterApplication : Application() { searchModule, settingsModule, spellDetailModule, - bottomBarEventModule + bottomBarEventModule, + localizationModule, ) } } diff --git a/core/localization/build.gradle.kts b/core/localization/build.gradle.kts new file mode 100644 index 00000000..1968537a --- /dev/null +++ b/core/localization/build.gradle.kts @@ -0,0 +1,19 @@ +plugins { + kotlin("multiplatform") +} + +configureJvmTargets() + +kotlin { + sourceSets { + val commonMain by getting { + dependencies { + implementation(libs.koin.core) + implementation(libs.kotlin.coroutines.core) + } + } + if (isMac()) { + val iosMain by getting + } + } +} diff --git a/core/localization/src/commonMain/kotlin/br/alexandregpereira/hunter/localization/Localization.kt b/core/localization/src/commonMain/kotlin/br/alexandregpereira/hunter/localization/Localization.kt new file mode 100644 index 00000000..130a8433 --- /dev/null +++ b/core/localization/src/commonMain/kotlin/br/alexandregpereira/hunter/localization/Localization.kt @@ -0,0 +1,29 @@ +package br.alexandregpereira.hunter.localization + +interface AppLocalization { + + fun getLanguage(): Language +} + +interface MutableAppLocalization : AppLocalization { + + fun setLanguage(language: String) +} + +internal class AppLocalizationImpl : MutableAppLocalization { + + private var language: Language = Language.ENGLISH + + override fun getLanguage(): Language { + return language + } + + override fun setLanguage(language: String) { + this.language = Language.entries.firstOrNull { it.code == language } ?: Language.ENGLISH + } +} + +enum class Language(val code: String) { + ENGLISH("en-us"), + PORTUGUESE("pt-br") +} diff --git a/core/localization/src/commonMain/kotlin/br/alexandregpereira/hunter/localization/di/LocalizationModule.kt b/core/localization/src/commonMain/kotlin/br/alexandregpereira/hunter/localization/di/LocalizationModule.kt new file mode 100644 index 00000000..14ea8c40 --- /dev/null +++ b/core/localization/src/commonMain/kotlin/br/alexandregpereira/hunter/localization/di/LocalizationModule.kt @@ -0,0 +1,12 @@ +package br.alexandregpereira.hunter.localization.di + +import br.alexandregpereira.hunter.localization.AppLocalization +import br.alexandregpereira.hunter.localization.AppLocalizationImpl +import br.alexandregpereira.hunter.localization.MutableAppLocalization +import org.koin.dsl.module + +val localizationModule = module { + single { AppLocalizationImpl() } + factory { get() } + factory { get() } +} diff --git a/core/state-holder/src/commonMain/kotlin/br/alexandregpereira/hunter/state/StateHolder.kt b/core/state-holder/src/commonMain/kotlin/br/alexandregpereira/hunter/state/StateHolder.kt index 9a1dc148..2aa28d91 100644 --- a/core/state-holder/src/commonMain/kotlin/br/alexandregpereira/hunter/state/StateHolder.kt +++ b/core/state-holder/src/commonMain/kotlin/br/alexandregpereira/hunter/state/StateHolder.kt @@ -91,7 +91,3 @@ private class DefaultStateHolderWithRecovery( } } } - -internal fun StateHolder.setState(block: State.() -> State) { - (this as MutableStateHolder).setState(block) -} diff --git a/domain/settings/core/build.gradle.kts b/domain/settings/core/build.gradle.kts index 1968537a..b4dda9c1 100644 --- a/domain/settings/core/build.gradle.kts +++ b/domain/settings/core/build.gradle.kts @@ -8,6 +8,7 @@ kotlin { sourceSets { val commonMain by getting { dependencies { + implementation(project(":core:localization")) implementation(libs.koin.core) implementation(libs.kotlin.coroutines.core) } diff --git a/domain/settings/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/settings/GetLanguageUseCase.kt b/domain/settings/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/settings/GetLanguageUseCase.kt index 3069a1b4..fb15fb0a 100644 --- a/domain/settings/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/settings/GetLanguageUseCase.kt +++ b/domain/settings/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/settings/GetLanguageUseCase.kt @@ -16,17 +16,22 @@ package br.alexandregpereira.hunter.domain.settings +import br.alexandregpereira.hunter.localization.MutableAppLocalization import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.onEach class GetLanguageUseCase( - private val settingsRepository: SettingsRepository + private val settingsRepository: SettingsRepository, + private val mutableAppLanguage: MutableAppLocalization, ) { operator fun invoke(): Flow { return settingsRepository.getValue( key = SETTING_LANGUAGE_KEY, defaultValue = "en-us" - ) + ).onEach { + mutableAppLanguage.setLanguage(it) + } } companion object { diff --git a/domain/settings/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/settings/IsLanguageSupported.kt b/domain/settings/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/settings/IsLanguageSupported.kt index 0d044287..18191ac0 100644 --- a/domain/settings/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/settings/IsLanguageSupported.kt +++ b/domain/settings/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/settings/IsLanguageSupported.kt @@ -16,9 +16,11 @@ package br.alexandregpereira.hunter.domain.settings +import br.alexandregpereira.hunter.localization.Language + object IsLanguageSupported { - private val supportedLanguage = listOf("en-us", "pt-br") + private val supportedLanguage = Language.entries.map { it.code } operator fun invoke(lang: String): Boolean { return supportedLanguage.contains(lang) diff --git a/domain/settings/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/settings/SaveLanguageUseCase.kt b/domain/settings/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/settings/SaveLanguageUseCase.kt index 20c4b76d..e3c8c32d 100644 --- a/domain/settings/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/settings/SaveLanguageUseCase.kt +++ b/domain/settings/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/settings/SaveLanguageUseCase.kt @@ -17,15 +17,24 @@ package br.alexandregpereira.hunter.domain.settings import br.alexandregpereira.hunter.domain.settings.GetLanguageUseCase.Companion.SETTING_LANGUAGE_KEY +import br.alexandregpereira.hunter.localization.MutableAppLocalization +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf class SaveLanguageUseCase( - private val settingsRepository: SettingsRepository + private val settingsRepository: SettingsRepository, + private val mutableAppLanguage: MutableAppLocalization, ) { + @OptIn(ExperimentalCoroutinesApi::class) operator fun invoke(lang: String): Flow { if (IsLanguageSupported(lang).not()) return flowOf(Unit) - return settingsRepository.saveSettings(mapOf(SETTING_LANGUAGE_KEY to lang)) + return flowOf { + mutableAppLanguage.setLanguage(lang) + }.flatMapLatest { + settingsRepository.saveSettings(mapOf(SETTING_LANGUAGE_KEY to lang)) + } } } diff --git a/domain/settings/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/settings/di/DomainModule.kt b/domain/settings/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/settings/di/DomainModule.kt index fa2d2898..66bb8779 100644 --- a/domain/settings/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/settings/di/DomainModule.kt +++ b/domain/settings/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/settings/di/DomainModule.kt @@ -28,9 +28,9 @@ import org.koin.dsl.module val settingsDomainModule = module { factory { GetAlternativeSourceJsonUrlUseCase(get()) } factory { GetContentVersionUseCase(get()) } - factory { GetLanguageUseCase(get()) } + factory { GetLanguageUseCase(get(), get()) } factory { GetMonsterImageJsonUrlUseCase(get()) } factory { SaveContentVersionUseCase(get()) } - factory { SaveLanguageUseCase(get()) } + factory { SaveLanguageUseCase(get(), get()) } factory { SaveUrlsUseCase(get()) } } diff --git a/feature/spell-compendium/android/src/main/java/br/alexandregpereira/hunter/spell/compendium/ui/SpellCompendiumScreen.kt b/feature/spell-compendium/android/src/main/java/br/alexandregpereira/hunter/spell/compendium/ui/SpellCompendiumScreen.kt index 9a494a47..d0a9311a 100644 --- a/feature/spell-compendium/android/src/main/java/br/alexandregpereira/hunter/spell/compendium/ui/SpellCompendiumScreen.kt +++ b/feature/spell-compendium/android/src/main/java/br/alexandregpereira/hunter/spell/compendium/ui/SpellCompendiumScreen.kt @@ -40,7 +40,7 @@ internal fun SpellCompendiumScreen( ) { AppTextField( text = state.searchText, - label = "Search", + label = state.searchTextLabel, capitalize = false, onValueChange = intent::onSearchTextChange ) diff --git a/feature/spell-compendium/state-holder/build.gradle.kts b/feature/spell-compendium/state-holder/build.gradle.kts index 0435143c..73ad15ff 100644 --- a/feature/spell-compendium/state-holder/build.gradle.kts +++ b/feature/spell-compendium/state-holder/build.gradle.kts @@ -26,6 +26,7 @@ kotlin { dependencies { implementation(project(":core:state-holder")) implementation(project(":core:event")) + implementation(project(":core:localization")) implementation(project(":domain:spell:core")) implementation(project(":feature:spell-compendium:event")) implementation(libs.kotlin.coroutines.core) diff --git a/feature/spell-compendium/state-holder/src/commonMain/kotlin/br/alexandregpereira/hunter/spell/compendium/SpellCompendiumState.kt b/feature/spell-compendium/state-holder/src/commonMain/kotlin/br/alexandregpereira/hunter/spell/compendium/SpellCompendiumState.kt index 4eb8a2e3..8e69c2ec 100644 --- a/feature/spell-compendium/state-holder/src/commonMain/kotlin/br/alexandregpereira/hunter/spell/compendium/SpellCompendiumState.kt +++ b/feature/spell-compendium/state-holder/src/commonMain/kotlin/br/alexandregpereira/hunter/spell/compendium/SpellCompendiumState.kt @@ -4,6 +4,7 @@ data class SpellCompendiumState( val isShowing: Boolean = false, val spellsGroupByLevel: Map> = emptyMap(), val searchText: String = "", + val searchTextLabel: String = "", val initialItemIndex: Int = 0, ) diff --git a/feature/spell-compendium/state-holder/src/commonMain/kotlin/br/alexandregpereira/hunter/spell/compendium/SpellCompendiumStateHolder.kt b/feature/spell-compendium/state-holder/src/commonMain/kotlin/br/alexandregpereira/hunter/spell/compendium/SpellCompendiumStateHolder.kt index 0c27b811..20596d81 100644 --- a/feature/spell-compendium/state-holder/src/commonMain/kotlin/br/alexandregpereira/hunter/spell/compendium/SpellCompendiumStateHolder.kt +++ b/feature/spell-compendium/state-holder/src/commonMain/kotlin/br/alexandregpereira/hunter/spell/compendium/SpellCompendiumStateHolder.kt @@ -3,6 +3,7 @@ package br.alexandregpereira.hunter.spell.compendium import br.alexandregpereira.hunter.domain.spell.model.Spell import br.alexandregpereira.hunter.event.EventDispatcher import br.alexandregpereira.hunter.event.EventListener +import br.alexandregpereira.hunter.localization.AppLocalization import br.alexandregpereira.hunter.spell.compendium.domain.GetSpellsUseCase import br.alexandregpereira.hunter.spell.compendium.event.SpellCompendiumEvent import br.alexandregpereira.hunter.spell.compendium.event.SpellCompendiumResult @@ -24,12 +25,14 @@ class SpellCompendiumStateHolder internal constructor( private val stateHandler: MutableStateHolder, private val eventListener: EventListener, private val resultDispatcher: EventDispatcher, + private val appLocalization: AppLocalization, ) : ScopeManager(), SpellCompendiumIntent, StateHolder by stateHandler { private val searchQuery = MutableStateFlow(state.value.searchText) private val originalSpellsGroupByLevel = mutableMapOf>() + private var strings: SpellCompendiumStrings = getSpellCompendiumStrings(appLocalization.getLanguage()) init { debounceSearch() @@ -43,6 +46,8 @@ class SpellCompendiumStateHolder internal constructor( spellIndex: String? = null, selectedSpellIndexes: List = emptyList(), ) { + strings = getSpellCompendiumStrings(appLocalization.getLanguage()) + stateHandler.setState { copy(searchText = "", searchTextLabel = strings.searchLabel) } getSpellsUseCase() .onEach { spells -> val spellsGroupByLevel = spells.groupByLevel(selectedSpellIndexes) @@ -56,7 +61,7 @@ class SpellCompendiumStateHolder internal constructor( stateHandler.setState { copy( spellsGroupByLevel = spellsGroupByLevel, - initialItemIndex = compendiumIndex?.takeIf { it >= 0 } ?: 0 + initialItemIndex = compendiumIndex?.takeIf { it >= 0 } ?: 0, ) } } @@ -69,9 +74,9 @@ class SpellCompendiumStateHolder internal constructor( .onEach { text -> stateHandler.setState { val spellsGroupByLevel = if (text.isNotBlank()){ - val spellsFiltered = spellsGroupByLevel.values.flatten() + val spellsFiltered = originalSpellsGroupByLevel.values.flatten() .filter { it.name.contains(text, ignoreCase = true) } - mapOf("${spellsFiltered.size} results" to spellsFiltered) + mapOf(strings.searchResults(spellsFiltered.size) to spellsFiltered) } else originalSpellsGroupByLevel copy(spellsGroupByLevel = spellsGroupByLevel) @@ -123,8 +128,8 @@ class SpellCompendiumStateHolder internal constructor( private fun Int.getSpellLevelText(): String { return when (this) { - 0 -> "Cantrip" - else -> "Level $this" + 0 -> strings.cantrips + else -> strings.level(this) } } diff --git a/feature/spell-compendium/state-holder/src/commonMain/kotlin/br/alexandregpereira/hunter/spell/compendium/SpellCompendiumStrings.kt b/feature/spell-compendium/state-holder/src/commonMain/kotlin/br/alexandregpereira/hunter/spell/compendium/SpellCompendiumStrings.kt new file mode 100644 index 00000000..820a4cb2 --- /dev/null +++ b/feature/spell-compendium/state-holder/src/commonMain/kotlin/br/alexandregpereira/hunter/spell/compendium/SpellCompendiumStrings.kt @@ -0,0 +1,31 @@ +package br.alexandregpereira.hunter.spell.compendium + +import br.alexandregpereira.hunter.localization.Language + +internal class SpellCompendiumEnStrings : SpellCompendiumStrings { + override val searchResults: (Int) -> String = { count -> "$count results" } + override val cantrips: String = "Cantrips" + override val level: (Int) -> String = { level -> "Level $level" } + override val searchLabel: String = "Search" +} + +internal class SpellCompendiumPtStrings : SpellCompendiumStrings { + override val searchResults: (Int) -> String = { count -> "$count resultados" } + override val cantrips: String = "Truques" + override val level: (Int) -> String = { level -> "${level}º Círculo" } + override val searchLabel: String = "Buscar" +} + +internal interface SpellCompendiumStrings { + val searchResults: (Int) -> String + val cantrips: String + val level: (Int) -> String + val searchLabel: String +} + +internal fun getSpellCompendiumStrings(lang: Language): SpellCompendiumStrings { + return when (lang) { + Language.ENGLISH -> SpellCompendiumEnStrings() + Language.PORTUGUESE -> SpellCompendiumPtStrings() + } +} diff --git a/feature/spell-compendium/state-holder/src/commonMain/kotlin/br/alexandregpereira/hunter/spell/compendium/di/Module.kt b/feature/spell-compendium/state-holder/src/commonMain/kotlin/br/alexandregpereira/hunter/spell/compendium/di/Module.kt index e073aafa..bb847125 100644 --- a/feature/spell-compendium/state-holder/src/commonMain/kotlin/br/alexandregpereira/hunter/spell/compendium/di/Module.kt +++ b/feature/spell-compendium/state-holder/src/commonMain/kotlin/br/alexandregpereira/hunter/spell/compendium/di/Module.kt @@ -29,7 +29,8 @@ val spellCompendiumStateModule = module { getSpellsUseCase = get(), stateHandler = MutableStateHolder(SpellCompendiumState()), eventListener = get(), - resultDispatcher = get() + resultDispatcher = get(), + appLocalization = get(), ) } } diff --git a/settings.gradle b/settings.gradle index afd1db93..c20c676a 100644 --- a/settings.gradle +++ b/settings.gradle @@ -10,6 +10,7 @@ include ':app' include ':core:analytics' include ':core:event' +include ':core:localization' include ':core:state-holder' include ':core:uuid' diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts index 947506cd..75398ecf 100644 --- a/shared/build.gradle.kts +++ b/shared/build.gradle.kts @@ -25,6 +25,7 @@ kotlin { val commonMain by getting { dependencies { implementation(project(":core:analytics")) + implementation(project(":core:localization")) implementation(project(":domain:app:data")) implementation(project(":domain:app:core")) implementation(project(":domain:sync:core")) diff --git a/shared/src/commonMain/kotlin/br/alexandregpereira/hunter/shared/di/AppModule.kt b/shared/src/commonMain/kotlin/br/alexandregpereira/hunter/shared/di/AppModule.kt index ee28571f..7fe353d2 100644 --- a/shared/src/commonMain/kotlin/br/alexandregpereira/hunter/shared/di/AppModule.kt +++ b/shared/src/commonMain/kotlin/br/alexandregpereira/hunter/shared/di/AppModule.kt @@ -23,6 +23,7 @@ import br.alexandregpereira.hunter.event.folder.insert.emptyFolderInsertEventDis import br.alexandregpereira.hunter.event.monster.lore.detail.emptyMonsterLoreDetailEventDispatcher import br.alexandregpereira.hunter.folder.preview.event.emptyFolderPreviewEventDispatcher import br.alexandregpereira.hunter.folder.preview.event.emptyFolderPreviewResultListener +import br.alexandregpereira.hunter.localization.di.localizationModule import br.alexandregpereira.hunter.monster.compendium.state.di.monsterCompendiumStateModule import br.alexandregpereira.hunter.monster.detail.di.monsterDetailStateModule import br.alexandregpereira.hunter.monster.registration.event.emptyMonsterRegistrationEventDispatcher @@ -38,6 +39,7 @@ fun appModules(): List = domainModules + dataModules + monsterDetailStateModule + syncStateModule + analyticsModule + + localizationModule + module { factory { Dispatchers.Default } factory {