diff --git a/.github/workflows/localization-validation.yml b/.github/workflows/localization-validation.yml
new file mode 100644
index 00000000..42d8d3e2
--- /dev/null
+++ b/.github/workflows/localization-validation.yml
@@ -0,0 +1,146 @@
+name: Localization Validation
+
+on:
+ push:
+ branches:
+ - feature/firebase-remote-localization
+ pull_request:
+ branches:
+ - develop
+ workflow_dispatch:
+
+concurrency:
+ group: localization-validation-${{ github.event_name }}-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ validate:
+ runs-on: ubuntu-latest
+ timeout-minutes: 45
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v5
+
+ - name: Set up JDK 21
+ uses: actions/setup-java@v5
+ with:
+ distribution: temurin
+ java-version: '21'
+
+ - name: Make Gradle wrapper executable
+ run: chmod +x gradlew
+
+ - name: Verify localization architecture
+ shell: bash
+ run: |
+ if grep -R --line-number --include='*.kt' 'Res\.string' app core data domain infrastructure model ui; then
+ echo 'Found remaining Compose Res.string usage.'
+ exit 1
+ fi
+ if grep -R --line-number --include='*.kt' 'org\.jetbrains\.compose\.resources\.stringResource' app core data domain infrastructure model ui; then
+ echo 'Found remaining JetBrains Compose stringResource import.'
+ exit 1
+ fi
+ if grep -R --line-number --include='*.kt' 'com\.google\.firebase' data/localization; then
+ echo 'Localization data code must use Firebase-KMP-Kit.'
+ exit 1
+ fi
+ if ! grep -R --line-number --include='*.kt' 'com\.firebasekit\.remoteconfig' data/localization/src/commonMain; then
+ echo 'Firebase-KMP-Kit Remote Config must be used from commonMain.'
+ exit 1
+ fi
+ if grep -R --line-number --include='*.kt' 'dev\.gitlive\.firebase' data/localization; then
+ echo 'Found obsolete GitLive localization integration.'
+ exit 1
+ fi
+ if [ -d data/firebase ]; then
+ echo 'Obsolete data/firebase module still exists.'
+ exit 1
+ fi
+ if find data/localization/src -type f -name '*.kt' | grep -v '/com/velord/data/localization/' | grep -q .; then
+ echo 'Found Kotlin source in data/localization outside com.velord.data.localization.'
+ find data/localization/src -type f -name '*.kt' | grep -v '/com/velord/data/localization/'
+ exit 1
+ fi
+ if find core/core-resource/src/androidMain/res -path '*/values-*/strings.xml' | grep -q .; then
+ echo 'Translated Android strings.xml files are forbidden; localization.json is canonical.'
+ find core/core-resource/src/androidMain/res -path '*/values-*/strings.xml'
+ exit 1
+ fi
+
+ - name: Validate localization publishing script
+ shell: pwsh
+ run: ./scripts/firebase/publish-localization.ps1 -ValidateOnly
+
+ - name: Verify QA reuses Develop Firebase client
+ shell: bash
+ run: |
+ trap 'rm -f app/android/google-services.json' EXIT
+ cat > app/android/google-services.json <<'JSON'
+ {
+ "project_info": {
+ "project_number": "1",
+ "project_id": "firebase-mapping-test"
+ },
+ "client": [
+ {
+ "client_info": {
+ "mobilesdk_app_id": "1:1:android:develop-test",
+ "android_client_info": {
+ "package_name": "com.velord.composescreenexample.develop"
+ }
+ },
+ "api_key": [
+ {
+ "current_key": "test-api-key"
+ }
+ ]
+ }
+ ]
+ }
+ JSON
+ ./gradlew :app:android:processQaDebugGoogleServices --stacktrace
+ if [ -e app/android/src/qa/google-services.json ]; then
+ echo 'QA must not generate or rewrite a Firebase client configuration.'
+ exit 1
+ fi
+
+ - name: Compile and test localization
+ run: >-
+ ./gradlew
+ :data:localization:desktopTest
+ :infrastructure:di:desktopTest
+ :app:desktop:compileKotlinDesktop
+ :app:android:processDevelopDebugMainManifest
+ :app:android:compileDevelopDebugKotlin
+ -x :app:android:processDevelopDebugGoogleServices
+ --stacktrace
+
+ - name: Compile QA
+ run: >-
+ ./gradlew
+ :app:android:compileQaDebugKotlin
+ -x :app:android:processQaDebugGoogleServices
+ --stacktrace
+
+ - name: Compile Stage
+ run: >-
+ ./gradlew
+ :app:android:compileStageDebugKotlin
+ -x :app:android:processStageDebugGoogleServices
+ --stacktrace
+
+ - name: Compile Production
+ run: >-
+ ./gradlew
+ :app:android:compileProductionDebugKotlin
+ -x :app:android:processProductionDebugGoogleServices
+ --stacktrace
+
+ - name: Run project quality checks
+ run: >-
+ ./gradlew
+ :infrastructure:konsist:test
+ detekt
+ --stacktrace
diff --git a/.gitignore b/.gitignore
index 8dcf1228..c6bc753a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -54,3 +54,7 @@ CLAUDE.local.md
.claude/
.claudeignore
.mcp.json
+
+# Desktop runtime cache
+app/desktop/cache/
+
diff --git a/.idea/gradle.xml b/.idea/gradle.xml
index d9fce01a..a0ddb190 100644
--- a/.idea/gradle.xml
+++ b/.idea/gradle.xml
@@ -34,6 +34,7 @@
+
diff --git a/app/android/build.gradle.kts b/app/android/build.gradle.kts
index f07ad269..ae4fac32 100644
--- a/app/android/build.gradle.kts
+++ b/app/android/build.gradle.kts
@@ -23,6 +23,7 @@ android {
defaultConfig {
applicationId = "com.velord.composescreenexample"
+ manifestPlaceholders["appName"] = "@string/app_name"
targetSdk = libs.versions.targetApi.get().toInt()
@@ -62,8 +63,9 @@ android {
}
create(BuildEnvironment.Qa.value) {
dimension = "environment"
+ manifestPlaceholders["appName"] = "ComposeScreenExample QA"
manifestPlaceholders["enableCrashReporting"] = true
- applicationIdSuffix = ".${BuildEnvironment.Qa.value}"
+ applicationIdSuffix = ".${BuildEnvironment.Develop.value}"
}
create(BuildEnvironment.Stage.value) {
@@ -84,6 +86,7 @@ android {
}
dependencies {
+ // Module Model
implementation(projects.model)
// Module Infrastructure
implementation(projects.infrastructure.util)
@@ -97,6 +100,8 @@ dependencies {
// Module Data
implementation(projects.data.os)
implementation(projects.data.appstate)
+ // Module Domain
+ implementation(projects.domain.usecaseSetting)
// Module UI
implementation(projects.ui.sharedviewmodel)
// Module UI Feature
diff --git a/app/android/src/main/AndroidManifest.xml b/app/android/src/main/AndroidManifest.xml
index cbc6ab69..a8152a9d 100644
--- a/app/android/src/main/AndroidManifest.xml
+++ b/app/android/src/main/AndroidManifest.xml
@@ -29,7 +29,7 @@
diff --git a/app/android/src/main/kotlin/com/velord/composescreenexample/App.kt b/app/android/src/main/kotlin/com/velord/composescreenexample/App.kt
index 17ebe817..3d0f224d 100644
--- a/app/android/src/main/kotlin/com/velord/composescreenexample/App.kt
+++ b/app/android/src/main/kotlin/com/velord/composescreenexample/App.kt
@@ -5,16 +5,20 @@ import android.os.StrictMode
import android.os.StrictMode.ThreadPolicy
import android.os.StrictMode.VmPolicy
import com.velord.data.os.memory.MemoryLeakMonitor
+import com.velord.usecase.setting.InitializeLocalizationUC
+import kotlinx.coroutines.runBlocking
import org.koin.android.ext.android.inject
class App : Application() {
private val memoryLeakMonitor: MemoryLeakMonitor by inject()
+ private val initializeLocalizationUC: InitializeLocalizationUC by inject()
override fun onCreate() {
super.onCreate()
initKoin()
+ initLocalization()
initStrictMode()
initMemoryLeakMonitor()
}
@@ -23,6 +27,12 @@ class App : Application() {
startKoin()
}
+ private fun initLocalization() {
+ runBlocking {
+ initializeLocalizationUC()
+ }
+ }
+
private fun initStrictMode() {
StrictMode.setThreadPolicy(
ThreadPolicy.Builder()
diff --git a/app/desktop/build.gradle.kts b/app/desktop/build.gradle.kts
index b8913fb1..a4ce3150 100644
--- a/app/desktop/build.gradle.kts
+++ b/app/desktop/build.gradle.kts
@@ -30,6 +30,8 @@ kotlin {
implementation(projects.core.coreResource)
// Module Data
implementation(projects.data.appstate)
+ // Module Domain
+ implementation(projects.domain.usecaseSetting)
// Module UI
implementation(projects.ui.sharedviewmodel)
// Module UI Feature
diff --git a/app/desktop/src/desktopMain/kotlin/com/velord/composescreenexample/desktop/Main.kt b/app/desktop/src/desktopMain/kotlin/com/velord/composescreenexample/desktop/Main.kt
index 433f83a8..4aa8dbcb 100644
--- a/app/desktop/src/desktopMain/kotlin/com/velord/composescreenexample/desktop/Main.kt
+++ b/app/desktop/src/desktopMain/kotlin/com/velord/composescreenexample/desktop/Main.kt
@@ -1,18 +1,18 @@
package com.velord.composescreenexample.desktop
import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.window.Window
-import androidx.compose.ui.window.application
-import com.velord.core.ui.compose.component.ToastHost
-import com.velord.core.ui.compose.component.DesktopBackDispatcher
-import com.velord.core.ui.compose.component.LocalDesktopBackDispatcher
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.KeyEventType
import androidx.compose.ui.input.key.key
import androidx.compose.ui.input.key.type
+import androidx.compose.ui.window.Window
+import androidx.compose.ui.window.application
+import com.velord.core.ui.compose.component.DesktopBackDispatcher
+import com.velord.core.ui.compose.component.LocalDesktopBackDispatcher
+import com.velord.core.ui.compose.component.ToastHost
import com.velord.core.ui.theme.AppThemeHost
import com.velord.core.ui.util.ObserveSharedFlow
import com.velord.infrastructure.di.createCommonAppModuleRoster
@@ -21,17 +21,22 @@ import com.velord.model.AppEvent
import com.velord.ui.feature.splash.SplashScreen
import com.velord.ui.feature.splash.SplashVM
import com.velord.ui.sharedviewmodel.MainVM
+import com.velord.usecase.setting.InitializeLocalizationUC
+import kotlinx.coroutines.runBlocking
import org.koin.compose.koinInject
import org.koin.core.context.startKoin
fun main() = application {
- startKoin {
+ val koin = startKoin {
modules(createCommonAppModuleRoster())
+ }.koin
+
+ runBlocking {
+ koin.get()()
}
val splashVM: SplashVM = koinInject()
val mainVM: MainVM = koinInject()
-
val dispatcher = remember { DesktopBackDispatcher() }
Window(
@@ -43,7 +48,7 @@ fun main() = application {
} else {
false
}
- }
+ },
) {
ObserveSharedFlow(mainVM.appEventFlow) {
when (it) {
@@ -60,7 +65,7 @@ fun main() = application {
modifier = Modifier.fillMaxSize(),
content = {
NavigationHost(navigationLib = mainVM.navigationLib)
- }
+ },
)
}
}
diff --git a/build-logic/src/main/kotlin/com/velord/buildlogic/task/GenerateAppStringResourcesTask.kt b/build-logic/src/main/kotlin/com/velord/buildlogic/task/GenerateAppStringResourcesTask.kt
new file mode 100644
index 00000000..0896654f
--- /dev/null
+++ b/build-logic/src/main/kotlin/com/velord/buildlogic/task/GenerateAppStringResourcesTask.kt
@@ -0,0 +1,146 @@
+package com.velord.buildlogic.task
+
+import groovy.json.JsonOutput
+import groovy.json.JsonSlurper
+import org.gradle.api.DefaultTask
+import org.gradle.api.file.DirectoryProperty
+import org.gradle.api.file.RegularFileProperty
+import org.gradle.api.tasks.InputFile
+import org.gradle.api.tasks.OutputDirectory
+import org.gradle.api.tasks.PathSensitive
+import org.gradle.api.tasks.PathSensitivity
+import org.gradle.api.tasks.TaskAction
+
+private const val SUPPORTED_SCHEMA_VERSION = 1
+private val APP_STRING_KEY_REGEX = Regex("[A-Za-z_][A-Za-z0-9_]*")
+private val FORMAT_PLACEHOLDER_REGEX = Regex("""%[0-9]+[$][sd]""")
+
+abstract class GenerateAppStringResourcesTask : DefaultTask() {
+
+ @get:InputFile
+ @get:PathSensitive(PathSensitivity.RELATIVE)
+ abstract val localizationFile: RegularFileProperty
+
+ @get:OutputDirectory
+ abstract val outputDirectory: DirectoryProperty
+
+ @TaskAction
+ fun generate() {
+ val document = JsonSlurper().parse(localizationFile.get().asFile) as Map<*, *>
+ val languageRoster = validate(document)
+ val defaultLanguage = resolveDefaultLanguage(document, languageRoster)
+ val defaultStringMap = languageRoster.getValue(defaultLanguage)
+ val keyRoster = defaultStringMap.keys.sorted()
+ val outputFile = outputDirectory.get().asFile.resolve("com/velord/core/resource/AppString.kt")
+
+ outputFile.parentFile.mkdirs()
+ outputFile.writeText(
+ buildString {
+ appendLine("package com.velord.core.resource")
+ appendLine()
+ appendLine("@JvmInline")
+ appendLine("value class AppStringResource internal constructor(")
+ appendLine(" internal val key: String,")
+ appendLine(")")
+ appendLine()
+ appendLine("object AppString {")
+ keyRoster.forEach { key ->
+ append(" val $key = AppStringResource(")
+ append(key.toKotlinStringLiteral())
+ appendLine(")")
+ }
+ appendLine("}")
+ },
+ )
+ }
+
+ private fun validate(document: Map<*, *>): Map> {
+ val schemaVersion = (document["schemaVersion"] as? Number)?.toInt()
+ require(schemaVersion == SUPPORTED_SCHEMA_VERSION) {
+ "localization.json must use schemaVersion $SUPPORTED_SCHEMA_VERSION"
+ }
+
+ val rawLanguageRoster = document["languageRoster"] as? Map<*, *>
+ ?: error("localization.json must contain languageRoster")
+ require(rawLanguageRoster.isNotEmpty()) {
+ "localization.json must contain at least one language"
+ }
+
+ val languageRoster = rawLanguageRoster.entries.associate { (languageKey, rawStrings) ->
+ val invalidLanguageMessage = "Localization language code must be a string"
+ val language = languageKey as? String ?: error(invalidLanguageMessage)
+ val stringMap = rawStrings.asStringMap("languageRoster.$language")
+
+ language to stringMap
+ }
+ val defaultLanguage = resolveDefaultLanguage(document, languageRoster)
+ val defaultStringMap = languageRoster.getValue(defaultLanguage)
+ require(defaultStringMap.isNotEmpty()) {
+ "localization.json must contain strings"
+ }
+
+ defaultStringMap.keys.forEach { key ->
+ require(APP_STRING_KEY_REGEX.matches(key)) {
+ "Invalid AppString key: $key"
+ }
+ }
+
+ languageRoster.forEach { (language, stringMap) ->
+ require(stringMap.keys == defaultStringMap.keys) {
+ "Localization key mismatch for language: $language"
+ }
+ stringMap.forEach { (key, value) ->
+ require(value.isNotEmpty()) {
+ "Localization value is empty: $language/$key"
+ }
+
+ val defaultValue = defaultStringMap.getValue(key)
+ val defaultPlaceholderRoster = placeholders(defaultValue)
+ val placeholderRoster = placeholders(value)
+ require(placeholderRoster == defaultPlaceholderRoster) {
+ "Localization placeholder mismatch: $language/$key"
+ }
+ }
+ }
+
+ return languageRoster
+ }
+
+ private fun resolveDefaultLanguage(
+ document: Map<*, *>,
+ languageRoster: Map>,
+ ): String {
+ val defaultLanguage = document["defaultLanguage"] as? String
+ ?: error("localization.json must contain defaultLanguage")
+ require(defaultLanguage.isNotBlank()) {
+ "localization.json defaultLanguage must not be blank"
+ }
+ require(defaultLanguage in languageRoster) {
+ "Default localization language '$defaultLanguage' is missing"
+ }
+
+ return defaultLanguage
+ }
+
+ private fun Any?.asStringMap(name: String): Map {
+ val value = this as? Map<*, *> ?: error("localization.json must contain $name")
+ return value.entries.associate { (key, entryValue) ->
+ val invalidKeyMessage = "Localization key in $name must be a string"
+ val invalidValueMessage = "Localization value for $name must be a string"
+ val stringKey = key as? String ?: error(invalidKeyMessage)
+ val stringValue = entryValue as? String ?: error(invalidValueMessage)
+
+ stringKey to stringValue
+ }
+ }
+
+ private fun placeholders(value: String): List = FORMAT_PLACEHOLDER_REGEX
+ .findAll(value)
+ .map { it.value }
+ .sorted()
+ .toList()
+
+ private fun String.toKotlinStringLiteral(): String = JsonOutput
+ .toJson(this)
+ .replace("$", "\\$")
+}
diff --git a/core/core-navigation/build.gradle.kts b/core/core-navigation/build.gradle.kts
index f8444ae9..6b29080f 100644
--- a/core/core-navigation/build.gradle.kts
+++ b/core/core-navigation/build.gradle.kts
@@ -11,6 +11,8 @@ kotlin {
sourceSets {
commonMain.dependencies {
+ // Module Core
+ implementation(projects.core.coreResource)
// Kotlin
implementation(libs.kotlin.coroutine.core)
implementation(libs.kotlin.serialization.json)
diff --git a/core/core-navigation/src/commonMain/kotlin/com/velord/core/navigation/voyager/SharedScreenVoyager.kt b/core/core-navigation/src/commonMain/kotlin/com/velord/core/navigation/voyager/SharedScreenVoyager.kt
index cfbb55f6..67ccd402 100644
--- a/core/core-navigation/src/commonMain/kotlin/com/velord/core/navigation/voyager/SharedScreenVoyager.kt
+++ b/core/core-navigation/src/commonMain/kotlin/com/velord/core/navigation/voyager/SharedScreenVoyager.kt
@@ -2,7 +2,7 @@ package com.velord.core.navigation.voyager
import androidx.compose.ui.Modifier
import cafe.adriel.voyager.core.registry.ScreenProvider
-import org.jetbrains.compose.resources.StringResource
+import com.velord.core.resource.AppStringResource
sealed class SharedScreenVoyager : ScreenProvider {
@@ -13,7 +13,7 @@ sealed class SharedScreenVoyager : ScreenProvider {
}
data class Test(
- val title: StringResource,
+ val title: AppStringResource,
val modifier: Modifier = Modifier,
val onClick: () -> Unit = {},
) : SharedScreenVoyager()
diff --git a/core/core-resource/build.gradle.kts b/core/core-resource/build.gradle.kts
index e3befdc4..410055ac 100644
--- a/core/core-resource/build.gradle.kts
+++ b/core/core-resource/build.gradle.kts
@@ -1,8 +1,9 @@
+import com.velord.buildlogic.task.GenerateAppStringResourcesTask
+
plugins {
alias(libs.plugins.convention.multiplatform.library)
alias(libs.plugins.multiplatform.compose)
alias(libs.plugins.kotlin.compose)
- alias(libs.plugins.kotlin.serialization)
}
compose.resources {
@@ -10,6 +11,19 @@ compose.resources {
packageOfResClass = "com.velord.core.resource"
}
+val bundledLocalizationFile = layout.projectDirectory.file(
+ "src/commonMain/composeResources/files/localization.json",
+)
+val generatedLocalizationDirectory = layout.buildDirectory.dir(
+ "generated/localization/commonMain/kotlin",
+)
+val generateAppStringResources = tasks.register(
+ "generateAppStringResources",
+) {
+ localizationFile.set(bundledLocalizationFile)
+ outputDirectory.set(generatedLocalizationDirectory)
+}
+
kotlin {
android {
namespace = "com.velord.core.resource"
@@ -19,10 +33,15 @@ kotlin {
}
sourceSets {
- commonMain.dependencies {
- // Compose
- api(libs.compose.resources)
- implementation(libs.compose.runtime)
+ commonMain {
+ kotlin.srcDir(generateAppStringResources.flatMap { it.outputDirectory })
+ dependencies {
+ // Module Model
+ api(projects.model)
+ // Compose
+ api(libs.compose.resources)
+ implementation(libs.compose.runtime)
+ }
}
}
}
diff --git a/core/core-resource/src/commonMain/composeResources/files/localization.json b/core/core-resource/src/commonMain/composeResources/files/localization.json
new file mode 100644
index 00000000..9de1b16a
--- /dev/null
+++ b/core/core-resource/src/commonMain/composeResources/files/localization.json
@@ -0,0 +1,194 @@
+{
+ "schemaVersion": 1,
+ "defaultLanguage": "en",
+ "languageRoster": {
+ "en": {
+ "app_name": "ComposeScreenExample",
+ "camera": "Camera",
+ "demo": "Demo",
+ "settings": "Settings",
+ "press_again_to_exit": "Press again to exit",
+ "bottom_navigation_first_back_press": "I am the first at %1$s",
+ "go_to_app_settings": "Go to App Settings",
+ "decline": "Decline",
+ "require_microphone_permission": "Require microphone permission",
+ "require_camera_permission": "Require camera permission",
+ "give_access_to_microphone": "Give access to microphone to proceed",
+ "give_access_to_camera": "Give access to camera to proceed",
+ "can_not_get_permission_for_mic": "Can not get permission for Microphone, seems like you declined it. Click to check again",
+ "can_not_get_permission_for_camera": "Can not get permission for Camera, seems like you declined it. Click to check again",
+ "open_new": "Open new",
+ "open_new_camera": "Open new Camera",
+ "test": "Test",
+ "open_shape_demo": "Open Shape Demo",
+ "open_modifier_demo": "Open Modifier Demo",
+ "open_flow_summator": "Open Flow Summator",
+ "open_morph_demo": "Open Morph Demo",
+ "open_hint_phone_number": "Open Hint Phone Number",
+ "flow_summator": "Flow Summator",
+ "launch": "Launch",
+ "please_enter_the_number": "Please, enter the number",
+ "info_description_flow_summator": "Develop Flow that sums values of other flows.\n\n1. UI - must consist of 3 items. Enter field, ‘launch’ Button and Text field for output. On press ‘launch’ app handle N - digit has been entered in the Enter field. Result of the work should be rendered to the Text field. Every new update should be on the new line.\n\n2. It is necessary to create an array of Flow of Int of N. Each after a delay of (index + 1) * 100, emits the value index + 1. I.e. Flow with index 0 emits value 1 with delay 100, Flow with index 1 emits value 2 with delay 200.\n\n3. The resulting Flow must sum the values of all N Flows. The summing Flow must return a value after updating each of the N Flows.\n\nImportant: The value on the screen must be added every 100ms.\n\nExample:\nN entered: 1\nResult: 1\n\nN entered: 2\nResult: 1\n3\n\nN entered: 3\nResult: 1\n3\n6\n\nN entered: 7\nResult: 1\n3\n6\n10\n15\n21\n28",
+ "abide_to_os_theme": "Abide to Os theme",
+ "os_does_not_support_theme_switching": "Os does not support theme switching on Android 8 and below\nProbably you have 'Invert colors' in Quick Settings represented as Tile",
+ "use_dark_theme": "Use Dark Theme",
+ "use_system_dynamic_theme": "Use System Dynamic Theme",
+ "not_available_on_android_11": "Not available on Android 11 and below",
+ "disable_os_theme_switcher_first": "Disable Os theme switcher first",
+ "error": "Error",
+ "loading": "Loading",
+ "count": "Count: %1$d",
+ "counter_widget": "Counter Widget",
+ "plus_sign": "+",
+ "minus_sign": "-",
+ "refresh": "Refresh Image",
+ "widget_size": "Widget Size:\nW: %1$s x H: %2$s",
+ "image_widget": "Image Widget",
+ "downloaded_from": "Downloaded from:\n%1$s",
+ "rear": "Rear",
+ "front": "Front",
+ "record_audio": "Record audio",
+ "video_quality_sd": "SD",
+ "video_quality_hd": "HD",
+ "video_quality_full_hd": "Full HD",
+ "video_quality_ultra_hd": "Ultra HD",
+ "record_audio_on": "Mic On",
+ "record_audio_off": "Mic Off",
+ "switch_camera_lens": "Switch camera lens",
+ "video_quality": "Video quality",
+ "video_quality_full_hd_short": "FHD",
+ "video_quality_ultra_hd_short": "4K",
+ "start_recording": "Start recording",
+ "stop_recording": "Stop recording",
+ "selected": "Selected",
+ "recording_restarted_after_camera_switch": "Recording restarted after switching camera",
+ "open_video_folder": "Open video folder",
+ "waiting": "Waiting",
+ "invoke_again": "Invoke again",
+ "phone_number_hint_not_available_on_desktop": "Phone number hint is not available on desktop",
+ "all": "All",
+ "favorite": "Favorite",
+ "sort_by": "Sort by",
+ "sort_by_date_ascending": "Sort by date ascending",
+ "sort_by_date_descending": "Sort by date descending",
+ "filter_by": "Filter by",
+ "filter_by_rating": "Filter by rating",
+ "filter_by_vote_count": "Filter by vote count",
+ "open_movie": "Open Movie",
+ "all_movies": "All movies with this filters are loaded",
+ "info_description_movie": "Test Task for android candidates\n\nWrite the application that allows user to review the list of movies and add them to favorites. You should provide your solution as a link to public repository. Please make commits of logical units, so we can evaluate your progress and add comments as you see fit.\n\nFunctional requirements:\n\nThe main screen contains two tabs: All and Favorites.\nTab All should display a list of movies with pagination.\nMovies should be grouped by month (take a look at design example)\nTab Favorites should display a list of bookmarked movies.\nUser can add / remove movie to / from his favorites.\nAll changes should be visible immediately on both tabs.\nBookmarks can be saved localy (without api call).\nUser can refresh movies list using pull-to-refresh.\nUser can see early loaded content without internet connection (only 1st page, without pagination)\n\nMovies list filter:\n\nvote average: 7+\nvote count: 100+\nsorted by primary release date, the newest movies should appear at the top of list\n\nScreen UI should display one of the following states:\n\nLoading\nRefreshing\nLoading More (pagination)\nError\nContent (just showing movies)\n\nOptional Requirements\nImplement sign-in with Google or Facebook\nImplement movie sharing via any existing provider\nTechnical requirements\nKotlin is required\nSqlite / Room is required\nOne of MVVM / MVI / Redux architecture is required;\nClean Architecture is required;\nDependency injection is required\nJetpack Compose is required\nUnit-tests are required;\n\nNotes\n\nYour code should be testable, scalable and flexible\nThe most important parts of logic should be covered with unit tests (you are free to decide what parts)\nYou are free to use any libraries\nYou are free to implement your own design\nMake sure that anyone can build your project.",
+ "range": "Range: %1$s",
+ "info_description_movie_url": "https://github.com/AppSci/android-test-task?tab=readme-ov-file",
+ "open_dialog": "Open Dialog Demo",
+ "shape_demo": "Shape Demo",
+ "modifier_demo": "Modifier Demo",
+ "morph_demo": "Morph Demo",
+ "hint_phone_number": "Hint Phone Number",
+ "movie": "Movie",
+ "dialog_demo": "Dialog Demo",
+ "this_demo_is_deprecated": "This demo is Deprecated on this navigation lib: %1$s",
+ "cancel": "Cancel",
+ "confirm": "Confirm",
+ "ok": "Ok",
+ "show_two_buttons_dialog": "Show two buttons dialog",
+ "show_one_button_dialog": "Show one button dialog",
+ "language": "Language",
+ "language_default": "Default",
+ "language_name": "English"
+ },
+ "es": {
+ "app_name": "ComposeScreenExample",
+ "camera": "Cámara",
+ "demo": "Demo",
+ "settings": "Configuración",
+ "press_again_to_exit": "Pulsa de nuevo para salir",
+ "bottom_navigation_first_back_press": "Soy el primero en %1$s",
+ "go_to_app_settings": "Ir a la configuración de la aplicación",
+ "decline": "Rechazar",
+ "require_microphone_permission": "Se requiere permiso para usar el micrófono",
+ "require_camera_permission": "Se requiere permiso para usar la cámara",
+ "give_access_to_microphone": "Da acceso al micrófono para continuar",
+ "give_access_to_camera": "Da acceso a la cámara para continuar",
+ "can_not_get_permission_for_mic": "No se puede obtener permiso para el micrófono; parece que lo rechazaste. Pulsa para comprobarlo de nuevo",
+ "can_not_get_permission_for_camera": "No se puede obtener permiso para la cámara; parece que lo rechazaste. Pulsa para comprobarlo de nuevo",
+ "open_new": "Abrir nuevo",
+ "open_new_camera": "Abrir nueva cámara",
+ "test": "Prueba",
+ "open_shape_demo": "Abrir demo de formas",
+ "open_modifier_demo": "Abrir demo de modificadores",
+ "open_flow_summator": "Abrir sumador de Flow",
+ "open_morph_demo": "Abrir demo de transformación",
+ "open_hint_phone_number": "Abrir sugerencia de número de teléfono",
+ "flow_summator": "Sumador de Flow",
+ "launch": "Iniciar",
+ "please_enter_the_number": "Introduce el número",
+ "info_description_flow_summator": "Desarrolla un Flow que sume los valores de otros flows.\n\n1. La UI debe constar de 3 elementos: un campo de entrada, un botón «Iniciar» y un campo de texto para la salida. Al pulsar «Iniciar», la aplicación procesa N, el dígito introducido en el campo de entrada. El resultado debe mostrarse en el campo de texto. Cada nueva actualización debe aparecer en una línea nueva.\n\n2. Es necesario crear un array de N Flow. Cada uno, después de un retraso de (index + 1) * 100, emite el valor index + 1. Es decir, el Flow con índice 0 emite el valor 1 con un retraso de 100, y el Flow con índice 1 emite el valor 2 con un retraso de 200.\n\n3. El Flow resultante debe sumar los valores de los N Flows. El Flow de suma debe devolver un valor después de cada actualización de cualquiera de los N Flows.\n\nImportante: el valor en pantalla debe añadirse cada 100 ms.\n\nEjemplo:\nN introducido: 1\nResultado: 1\n\nN introducido: 2\nResultado: 1\n3\n\nN introducido: 3\nResultado: 1\n3\n6\n\nN introducido: 7\nResultado: 1\n3\n6\n10\n15\n21\n28",
+ "abide_to_os_theme": "Seguir el tema del SO",
+ "os_does_not_support_theme_switching": "El SO no admite el cambio de tema en Android 8 y versiones anteriores\nProbablemente tienes «Invertir colores» en Ajustes rápidos representado como un mosaico",
+ "use_dark_theme": "Usar tema oscuro",
+ "use_system_dynamic_theme": "Usar tema dinámico del sistema",
+ "not_available_on_android_11": "No disponible en Android 11 y versiones anteriores",
+ "disable_os_theme_switcher_first": "Desactiva primero el selector de tema del SO",
+ "error": "Error",
+ "loading": "Cargando",
+ "count": "Cantidad: %1$d",
+ "counter_widget": "Widget de contador",
+ "plus_sign": "+",
+ "minus_sign": "-",
+ "refresh": "Actualizar imagen",
+ "widget_size": "Tamaño del widget:\nAn: %1$s x Al: %2$s",
+ "image_widget": "Widget de imagen",
+ "downloaded_from": "Descargado de:\n%1$s",
+ "rear": "Trasera",
+ "front": "Frontal",
+ "record_audio": "Grabar audio",
+ "video_quality_sd": "SD",
+ "video_quality_hd": "HD",
+ "video_quality_full_hd": "Full HD",
+ "video_quality_ultra_hd": "Ultra HD",
+ "record_audio_on": "Micrófono activado",
+ "record_audio_off": "Micrófono desactivado",
+ "switch_camera_lens": "Cambiar lente de la cámara",
+ "video_quality": "Calidad de vídeo",
+ "video_quality_full_hd_short": "FHD",
+ "video_quality_ultra_hd_short": "4K",
+ "start_recording": "Iniciar grabación",
+ "stop_recording": "Detener grabación",
+ "selected": "Seleccionado",
+ "recording_restarted_after_camera_switch": "La grabación se reinició después de cambiar de cámara",
+ "open_video_folder": "Abrir carpeta de vídeos",
+ "waiting": "Esperando",
+ "invoke_again": "Invocar de nuevo",
+ "phone_number_hint_not_available_on_desktop": "La sugerencia de número de teléfono no está disponible en escritorio",
+ "all": "Todos",
+ "favorite": "Favoritos",
+ "sort_by": "Ordenar por",
+ "sort_by_date_ascending": "Ordenar por fecha ascendente",
+ "sort_by_date_descending": "Ordenar por fecha descendente",
+ "filter_by": "Filtrar por",
+ "filter_by_rating": "Filtrar por valoración",
+ "filter_by_vote_count": "Filtrar por número de votos",
+ "open_movie": "Abrir película",
+ "all_movies": "Se han cargado todas las películas con estos filtros",
+ "info_description_movie": "Tarea de prueba para candidatos Android\n\nEscribe una aplicación que permita al usuario revisar una lista de películas y añadirlas a favoritos. Debes proporcionar tu solución mediante un enlace a un repositorio público. Haz commits de unidades lógicas para que podamos evaluar tu progreso y añade comentarios cuando lo consideres oportuno.\n\nRequisitos funcionales:\n\nLa pantalla principal contiene dos pestañas: Todas y Favoritos.\nLa pestaña Todas debe mostrar una lista de películas con paginación.\nLas películas deben agruparse por mes (consulta el ejemplo de diseño).\nLa pestaña Favoritos debe mostrar una lista de películas marcadas.\nEl usuario puede añadir o eliminar películas de sus favoritos.\nTodos los cambios deben ser visibles inmediatamente en ambas pestañas.\nLos marcadores pueden guardarse localmente (sin llamada a la API).\nEl usuario puede actualizar la lista de películas mediante pull-to-refresh.\nEl usuario puede ver contenido cargado previamente sin conexión a Internet (solo la primera página, sin paginación).\n\nFiltro de la lista de películas:\n\nvaloración media: 7+\nnúmero de votos: 100+\nordenado por fecha de estreno principal; las películas más recientes deben aparecer al principio de la lista\n\nLa UI de la pantalla debe mostrar uno de los siguientes estados:\n\nCargando\nActualizando\nCargando más (paginación)\nError\nContenido (solo mostrando películas)\n\nRequisitos opcionales\nImplementar inicio de sesión con Google o Facebook\nImplementar compartir películas mediante cualquier proveedor\nRequisitos técnicos\nKotlin es obligatorio\nSqlite / Room es obligatorio\nSe requiere una arquitectura MVVM / MVI / Redux;\nClean Architecture es obligatoria;\nLa inyección de dependencias es obligatoria\nJetpack Compose es obligatorio\nLas pruebas unitarias son obligatorias;\n\nNotas\n\nEl código debe ser comprobable, escalable y flexible\nLas partes más importantes de la lógica deben estar cubiertas por pruebas unitarias (puedes decidir qué partes)\nPuedes usar cualquier biblioteca\nPuedes implementar tu propio diseño\nAsegúrate de que cualquiera pueda compilar el proyecto.",
+ "range": "Rango: %1$s",
+ "info_description_movie_url": "https://github.com/AppSci/android-test-task?tab=readme-ov-file",
+ "open_dialog": "Abrir demo de diálogo",
+ "shape_demo": "Demo de formas",
+ "modifier_demo": "Demo de modificadores",
+ "morph_demo": "Demo de transformación",
+ "hint_phone_number": "Sugerencia de número de teléfono",
+ "movie": "Película",
+ "dialog_demo": "Demo de diálogo",
+ "this_demo_is_deprecated": "Esta demo está obsoleta en esta biblioteca de navegación: %1$s",
+ "cancel": "Cancelar",
+ "confirm": "Confirmar",
+ "ok": "Aceptar",
+ "show_two_buttons_dialog": "Mostrar diálogo con dos botones",
+ "show_one_button_dialog": "Mostrar diálogo con un botón",
+ "language": "Idioma",
+ "language_default": "Predeterminado",
+ "language_name": "Español"
+ }
+ }
+}
diff --git a/core/core-resource/src/commonMain/composeResources/values/strings.xml b/core/core-resource/src/commonMain/composeResources/values/strings.xml
deleted file mode 100644
index 9ae30f2b..00000000
--- a/core/core-resource/src/commonMain/composeResources/values/strings.xml
+++ /dev/null
@@ -1,152 +0,0 @@
-
- ComposeScreenExample
- Camera
- Demo
- Settings
- Press again to exit
- I am the first at %1$s
- Go to App Settings
- Decline
- Require microphone permission
- Require camera permission
- Give access to microphone to proceed
- Give access to camera to proceed
- Can not get permission for Microphone, seems like you declined it. Click to check again
- Can not get permission for Camera, seems like you declined it. Click to check again
- Open new
- Open new Camera
- Test
- Open Shape Demo
- Open Modifier Demo
- Open Flow Summator
- Open Morph Demo
- Open Hint Phone Number
- Flow Summator
- Launch
- Please, enter the number
- Develop Flow that sums values of other flows.
- \n" "1. UI - must consist of 3 items. Enter field, ‘launch’ Button and Text field for output. On press ‘launch’ app handle N - digit has been entered in the Enter field. Result of the work should be rendered to the Text field. Every new update should be on the new line.
- \n" "2. It is necessary to create an array of Flow of Int of N. Each after a delay of (index + 1) * 100, emits the value index + 1. I.e. Flow with index 0 emits value 1 with delay 100, Flow with index 1 emits value 2 with delay 200.
- \n" "3. The resulting Flow must sum the values of all N Flows. The summing Flow must return a value after updating each of the N Flows.
- \nImportant: The value on the screen must be added every 100ms.
- \n
- \nExample:
- \nN entered: 1
- \nResult: 1
- \n
- \nN entered: 2
- \nResult: 1\n3
- \n
- \nN entered: 3
- \nResult: 1\n3\n6
- \n
- \nN entered: 7
- \nResult: 1\n3\n6\n10\n15\n21\n28
-
- Abide to Os theme
- Os does not support theme switching on Android 8 and below\nProbably you have \'Invert colors\' in Quick Settings represented as Tile
- Use Dark Theme
- Use System Dynamic Theme
- Not available on Android 11 and below
- Disable Os theme switcher first
- Error
- Loading
- Count: %1$d
- Counter Widget
- +
- -
- Refresh Image
- Widget Size:\nW: %1$s x H: %2$s
- Image Widget
- Downloaded from:\n%1$s
- Rear
- Front
- Record audio
- SD
- HD
- Full HD
- Ultra HD
- Mic On
- Mic Off
- Switch camera lens
- Video quality
- FHD
- 4K
- Start recording
- Stop recording
- Selected
- Recording restarted after switching camera
- Open video folder
- Waiting
- Invoke again
- Phone number hint is not available on desktop
- All
- Favorite
- Sort by
- Sort by date ascending
- Sort by date descending
- Filter by
- Filter by rating
- Filter by vote count
- Open Movie
- All movies with this filters are loaded
- Test Task for android candidates\n
-\n
-\nWrite the application that allows user to review the list of movies and add them to favorites. You should provide your solution as a link to public repository. Please make commits of logical units, so we can evaluate your progress and add comments as you see fit.
-\n
-\nFunctional requirements:
-\n\nThe main screen contains two tabs: All and Favorites.
-\nTab All should display a list of movies with pagination.
- \nMovies should be grouped by month (take a look at design example)
-\nTab Favorites should display a list of bookmarked movies.
-\nUser can add / remove movie to / from his favorites.
-\nAll changes should be visible immediately on both tabs.
-\nBookmarks can be saved localy (without api call).
-\nUser can refresh movies list using pull-to-refresh.
-\nUser can see early loaded content without internet connection (only 1st page, without pagination)
-\n\nMovies list filter:
-\n
-\nvote average: 7+
-\nvote count: 100+
-\nsorted by primary release date, the newest movies should appear at the top of list
-\n\nScreen UI should display one of the following states:
-\n
-\nLoading
-\nRefreshing
-\nLoading More (pagination)
-\nError
-\nContent (just showing movies)
-\n\nOptional Requirements
-\nImplement sign-in with Google or Facebook
-\nImplement movie sharing via any existing provider
-\nTechnical requirements
-\nKotlin is required
-\nSqlite / Room is required
-\nOne of MVVM / MVI / Redux architecture is required;
-\nClean Architecture is required;
-\nDependency injection is required
-\nJetpack Compose is required
-\nUnit-tests are required;
-\n\nNotes
-\n\nYour code should be testable, scalable and flexible
-\nThe most important parts of logic should be covered with unit tests (you are free to decide what parts)
-\nYou are free to use any libraries
-\nYou are free to implement your own design
-\nMake sure that anyone can build your project.
-
- Range: %1$s
- https://github.com/AppSci/android-test-task?tab=readme-ov-file
- Open Dialog Demo
- Shape Demo
- Modifier Demo
- Morph Demo
- Hint Phone Number
- Movie
- Dialog Demo
- This demo is Deprecated on this navigation lib: %1$s
- Cancel
- Confirm
- Ok
- Show two buttons dialog
- Show one button dialog
-
diff --git a/core/core-resource/src/commonMain/kotlin/com/velord/core/resource/StringResource.kt b/core/core-resource/src/commonMain/kotlin/com/velord/core/resource/StringResource.kt
new file mode 100644
index 00000000..01304f05
--- /dev/null
+++ b/core/core-resource/src/commonMain/kotlin/com/velord/core/resource/StringResource.kt
@@ -0,0 +1,88 @@
+package com.velord.core.resource
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.staticCompositionLocalOf
+import androidx.compose.ui.platform.LocalInspectionMode
+import com.velord.model.localization.LanguageCode
+import com.velord.model.localization.LocalizationState
+
+private val FORMAT_ARGUMENT_REGEX = Regex("""%([0-9]+)[$]([sd])""")
+
+val LocalLocalizationState = staticCompositionLocalOf { null }
+
+@Composable
+fun stringResource(
+ resource: AppStringResource,
+ vararg formatArgs: Any,
+): String {
+ val localization = localizationForStringResource() ?: error("LocalizationState is not provided")
+
+ return getString(
+ localization = localization,
+ resource = resource,
+ formatArgs = formatArgs,
+ )
+}
+
+@Composable
+fun stringResource(
+ resource: AppStringResource,
+ language: LanguageCode,
+ vararg formatArgs: Any,
+): String {
+ val localization = localizationForStringResource() ?: error("LocalizationState is not provided")
+
+ return getString(
+ localization = localization,
+ language = language,
+ resource = resource,
+ formatArgs = formatArgs,
+ )
+}
+
+fun getString(
+ localization: LocalizationState,
+ resource: AppStringResource,
+ vararg formatArgs: Any,
+): String = getString(
+ localization = localization,
+ language = localization.language,
+ resource = resource,
+ formatArgs = formatArgs,
+)
+
+fun getString(
+ localization: LocalizationState,
+ language: LanguageCode,
+ resource: AppStringResource,
+ vararg formatArgs: Any,
+): String {
+ val languageStringMap = checkNotNull(localization.document.languageRoster[language]) {
+ "Localization language is missing: ${language.value}"
+ }
+ val template = languageStringMap[resource.key]
+ ?: error("Localization resource is missing: ${resource.key}")
+
+ return format(template, formatArgs)
+}
+
+@Composable
+private fun localizationForStringResource(): LocalizationState? {
+ val localization = LocalLocalizationState.current
+ if (localization != null) return localization
+ if (LocalInspectionMode.current) return null
+
+ error("LocalizationState is not provided")
+}
+
+private fun format(
+ template: String,
+ formatArgs: Array,
+): String = FORMAT_ARGUMENT_REGEX.replace(template) { match ->
+ val argumentIndex = match.groupValues[1].toInt() - 1
+ require(argumentIndex in formatArgs.indices) {
+ "Missing format argument ${argumentIndex + 1} for: $template"
+ }
+
+ formatArgs[argumentIndex].toString()
+}
diff --git a/core/core-ui/src/androidMain/kotlin/com/velord/core/ui/dialog/CheckPermission.kt b/core/core-ui/src/androidMain/kotlin/com/velord/core/ui/dialog/CheckPermission.kt
index 0cf81264..fc06b377 100644
--- a/core/core-ui/src/androidMain/kotlin/com/velord/core/ui/dialog/CheckPermission.kt
+++ b/core/core-ui/src/androidMain/kotlin/com/velord/core/ui/dialog/CheckPermission.kt
@@ -6,98 +6,100 @@ import android.content.pm.PackageManager
import androidx.activity.result.ActivityResultLauncher
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
-import com.velord.core.resource.Res
-import com.velord.core.resource.decline
-import com.velord.core.resource.give_access_to_camera
-import com.velord.core.resource.give_access_to_microphone
-import com.velord.core.resource.go_to_app_settings
-import com.velord.core.resource.require_camera_permission
-import com.velord.core.resource.require_microphone_permission
+import com.velord.core.resource.AppString
+import com.velord.core.resource.AppStringResource
+import com.velord.core.resource.getString
import com.velord.infrastructure.util.context.createSettingsIntent
-import kotlinx.coroutines.runBlocking
-import org.jetbrains.compose.resources.getString
+import com.velord.model.localization.LocalizationState
private fun Context.askUserActivatePermissionInSettings(
- title: org.jetbrains.compose.resources.StringResource,
- message: org.jetbrains.compose.resources.StringResource,
- onDecline: () -> Unit
+ localization: LocalizationState,
+ title: AppStringResource,
+ message: AppStringResource,
+ onDecline: () -> Unit,
) {
alertDialog(
- title = runBlocking { getString(title) },
- message = runBlocking { getString(message) },
- positiveText = runBlocking { getString(Res.string.go_to_app_settings) },
- negativeText = runBlocking { getString(Res.string.decline) },
+ title = getString(localization, title),
+ message = getString(localization, message),
+ positiveText = getString(localization, AppString.go_to_app_settings),
+ negativeText = getString(localization, AppString.decline),
positiveCallback = {
startActivity(createSettingsIntent())
},
negativeCallback = onDecline,
- cancelable = false
+ cancelable = false,
)
}
fun Context.showGoToSettingsForMic(
- onDecline: () -> Unit
+ localization: LocalizationState,
+ onDecline: () -> Unit,
) {
askUserActivatePermissionInSettings(
- title = Res.string.require_microphone_permission,
- message = Res.string.give_access_to_microphone,
- onDecline = onDecline
+ localization = localization,
+ title = AppString.require_microphone_permission,
+ message = AppString.give_access_to_microphone,
+ onDecline = onDecline,
)
}
fun Context.showGoToSettingsForCamera(
- onDecline: () -> Unit
+ localization: LocalizationState,
+ onDecline: () -> Unit,
) {
askUserActivatePermissionInSettings(
- title = Res.string.require_camera_permission,
- message = Res.string.give_access_to_camera,
- onDecline = onDecline
+ localization = localization,
+ title = AppString.require_camera_permission,
+ message = AppString.give_access_to_camera,
+ onDecline = onDecline,
)
}
fun Fragment.checkRecordAudioPermission(
+ localization: LocalizationState,
actionLauncher: ActivityResultLauncher,
- onGranted: () -> Unit
+ onGranted: () -> Unit,
) {
val permRecordAudio = Manifest.permission.RECORD_AUDIO
val isGranted = ContextCompat.checkSelfPermission(
requireContext(),
- permRecordAudio
+ permRecordAudio,
) == PackageManager.PERMISSION_GRANTED
when {
isGranted -> onGranted()
shouldShowRequestPermissionRationale(permRecordAudio) ->
- requireContext().showGoToSettingsForMic {}
+ requireContext().showGoToSettingsForMic(localization) {}
else -> actionLauncher.launch(permRecordAudio)
}
}
fun Fragment.checkRecordVideoPermission(
+ localization: LocalizationState,
actionLauncher: ActivityResultLauncher>,
onGranted: () -> Unit,
- onDecline: () -> Unit
+ onDecline: () -> Unit,
) {
val permRecordAudio = Manifest.permission.RECORD_AUDIO
val permCamera = Manifest.permission.CAMERA
val permissionRoster = arrayOf(
permRecordAudio,
- permCamera
+ permCamera,
)
val isGranted = permissionRoster.all { permission ->
ContextCompat.checkSelfPermission(
requireContext(),
- permission
+ permission,
) == PackageManager.PERMISSION_GRANTED
}
when {
isGranted -> onGranted()
shouldShowRequestPermissionRationale(permRecordAudio) ->
- requireContext().showGoToSettingsForMic(onDecline)
+ requireContext().showGoToSettingsForMic(localization, onDecline)
shouldShowRequestPermissionRationale(permCamera) ->
- requireContext().showGoToSettingsForCamera(onDecline)
+ requireContext().showGoToSettingsForCamera(localization, onDecline)
else -> actionLauncher.launch(permissionRoster)
}
}
diff --git a/core/core-ui/src/androidMain/kotlin/com/velord/core/ui/permission/CheckCameraAndAudioRecordPermission.kt b/core/core-ui/src/androidMain/kotlin/com/velord/core/ui/permission/CheckCameraAndAudioRecordPermission.kt
index 1d394577..e0e2de4f 100644
--- a/core/core-ui/src/androidMain/kotlin/com/velord/core/ui/permission/CheckCameraAndAudioRecordPermission.kt
+++ b/core/core-ui/src/androidMain/kotlin/com/velord/core/ui/permission/CheckCameraAndAudioRecordPermission.kt
@@ -15,11 +15,13 @@ import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberMultiplePermissionsState
import com.google.accompanist.permissions.shouldShowRationale
+import com.velord.core.resource.LocalLocalizationState
import com.velord.core.ui.dialog.showGoToSettingsForCamera
import com.velord.core.ui.dialog.showGoToSettingsForMic
import com.velord.core.ui.util.ObserveSharedFlow
import com.velord.infrastructure.util.permission.PermissionGrantState
import com.velord.infrastructure.util.persmission.toPermissionGrantState
+import com.velord.model.localization.LocalizationState
import kotlinx.coroutines.flow.MutableSharedFlow
import com.google.accompanist.permissions.PermissionState as AccompanistPermissionState
@@ -33,6 +35,7 @@ fun CheckCameraAndAudioRecordPermission(
onMicroUpdateState: (PermissionGrantState) -> Unit,
) {
val context = LocalContext.current
+ val localization = LocalLocalizationState.current
// Fix the issue when the user first time asked for permission.
// Can't do anything if user just leaves\close permission dialog infinite times.
val permissionAlreadyRequestedState = rememberSaveable {
@@ -54,16 +57,12 @@ fun CheckCameraAndAudioRecordPermission(
val cameraState = remember {
derivedStateOf {
- permissionsState
- .permissions
- .firstOrNull { it.permission == Manifest.permission.CAMERA }
+ permissionsState.permissions.firstOrNull { it.permission == Manifest.permission.CAMERA }
}
}
val microState = remember {
derivedStateOf {
- permissionsState
- .permissions
- .firstOrNull { it.permission == Manifest.permission.RECORD_AUDIO }
+ permissionsState.permissions.firstOrNull { it.permission == Manifest.permission.RECORD_AUDIO }
}
}
@@ -72,12 +71,13 @@ fun CheckCameraAndAudioRecordPermission(
permissionsState.launchMultiplePermissionRequest()
}
- LaunchedEffect(permissionAlreadyRequestedState.value) {
+ LaunchedEffect(permissionAlreadyRequestedState.value, localization) {
if (permissionAlreadyRequestedState.value.not()) return@LaunchedEffect
+ val currentLocalization = localization ?: return@LaunchedEffect
log.d { "LaunchedEffect permissionAlreadyRequestedState" }
- checkCamera(permissionAlreadyRequestedState, cameraState, context)
- checkAudioRecord(permissionAlreadyRequestedState, microState, context)
+ checkCamera(permissionAlreadyRequestedState, cameraState, context, currentLocalization)
+ checkAudioRecord(permissionAlreadyRequestedState, microState, context, currentLocalization)
}
cameraState.value?.let {
@@ -99,8 +99,10 @@ fun CheckCameraAndAudioRecordPermission(
log.d { "ObserveTrigger != null" }
permissionsState.launchMultiplePermissionRequest()
- checkCamera(permissionAlreadyRequestedState, cameraState, context)
- checkAudioRecord(permissionAlreadyRequestedState, microState, context)
+ localization?.let { currentLocalization ->
+ checkCamera(permissionAlreadyRequestedState, cameraState, context, currentLocalization)
+ checkAudioRecord(permissionAlreadyRequestedState, microState, context, currentLocalization)
+ }
}
}
@@ -108,12 +110,13 @@ fun CheckCameraAndAudioRecordPermission(
private fun checkCamera(
permissionAlreadyRequestedState: State,
cameraState: State,
- context: Context
+ context: Context,
+ localization: LocalizationState,
) {
baseCheck(
permissionAlreadyRequestedState,
cameraState,
- { context.showGoToSettingsForCamera {} },
+ { context.showGoToSettingsForCamera(localization) {} },
"Camera"
)
}
@@ -122,12 +125,13 @@ private fun checkCamera(
private fun checkAudioRecord(
permissionAlreadyRequestedState: State,
microState: State,
- context: Context
+ context: Context,
+ localization: LocalizationState,
) {
baseCheck(
permissionAlreadyRequestedState,
microState,
- { context.showGoToSettingsForMic {} },
+ { context.showGoToSettingsForMic(localization) {} },
"AudioRecord"
)
}
diff --git a/core/core-ui/src/commonMain/kotlin/com/velord/core/ui/compose/preview/PreviewLocalization.kt b/core/core-ui/src/commonMain/kotlin/com/velord/core/ui/compose/preview/PreviewLocalization.kt
new file mode 100644
index 00000000..d864dbb6
--- /dev/null
+++ b/core/core-ui/src/commonMain/kotlin/com/velord/core/ui/compose/preview/PreviewLocalization.kt
@@ -0,0 +1,20 @@
+package com.velord.core.ui.compose.preview
+
+import com.velord.model.localization.LocalizationDocument
+import com.velord.model.localization.LocalizationState
+import com.velord.model.localization.LocalizationStringMap
+import com.velord.model.setting.LanguagePreference
+
+private val previewLanguageRoster = LanguagePreference.entries
+ .filterNot { it.isDefault }
+ .associate { preference -> preference.languageCode to LocalizationStringMap(emptyMap()) }
+private val previewLanguage = previewLanguageRoster.keys.first()
+
+val previewLocalizationState = LocalizationState(
+ document = LocalizationDocument(
+ schemaVersion = 1,
+ defaultLanguage = previewLanguage,
+ languageRoster = previewLanguageRoster,
+ ),
+ language = previewLanguage,
+)
diff --git a/core/core-ui/src/commonMain/kotlin/com/velord/core/ui/theme/AppThemeHost.kt b/core/core-ui/src/commonMain/kotlin/com/velord/core/ui/theme/AppThemeHost.kt
index 7bed8b72..748eae16 100644
--- a/core/core-ui/src/commonMain/kotlin/com/velord/core/ui/theme/AppThemeHost.kt
+++ b/core/core-ui/src/commonMain/kotlin/com/velord/core/ui/theme/AppThemeHost.kt
@@ -5,9 +5,11 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.State
import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.velord.core.resource.LocalLocalizationState
import com.velord.core.ui.compose.theme.MainTheme
import com.velord.core.ui.util.LocalTheme
import com.velord.model.setting.AppThemeConfig
+import com.velord.ui.sharedviewmodel.LanguageVM
import com.velord.ui.sharedviewmodel.ThemeUiState
import com.velord.ui.sharedviewmodel.ThemeVM
import org.koin.compose.viewmodel.koinViewModel
@@ -15,12 +17,20 @@ import org.koin.compose.viewmodel.koinViewModel
@Composable
fun AppThemeHost(
themeVM: ThemeVM = koinViewModel(),
+ languageVM: LanguageVM = koinViewModel(),
content: @Composable () -> Unit,
) {
val themeState: State = themeVM.uiStateFlow.collectAsStateWithLifecycle()
val theme = themeState.value?.appThemeConfig ?: AppThemeConfig.DEFAULT
+ val localizationState = languageVM.uiStateFlow
+ .collectAsStateWithLifecycle()
+ .value
+ .localization
- CompositionLocalProvider(LocalTheme provides theme) {
+ CompositionLocalProvider(
+ LocalTheme provides theme,
+ LocalLocalizationState provides localizationState,
+ ) {
val localThemeConfig = LocalTheme.current
val isDark = if (localThemeConfig.config.abideToOs) {
diff --git a/data/appstate/src/commonMain/kotlin/com/velord/data/appstate/AppStateDataSource.kt b/data/appstate/src/commonMain/kotlin/com/velord/data/appstate/AppStateDataSource.kt
index 49e30698..6cd3387d 100644
--- a/data/appstate/src/commonMain/kotlin/com/velord/data/appstate/AppStateDataSource.kt
+++ b/data/appstate/src/commonMain/kotlin/com/velord/data/appstate/AppStateDataSource.kt
@@ -4,6 +4,7 @@ import com.velord.model.AppEvent
import com.velord.model.camera.CameraSessionWrapper
import com.velord.model.camera.CameraState
import com.velord.model.camera.CameraVideoAsset
+import com.velord.model.localization.LocalizationState
import com.velord.model.movie.Movie
import com.velord.model.movie.MovieSortOption
import com.velord.model.movie.SortType
@@ -21,6 +22,7 @@ interface AppStateDataSource {
val cameraSessionFlow: MutableStateFlow
val cameraStateFlow: MutableStateFlow
val lastCameraVideoAssetFlow: MutableStateFlow
+ val localizationStateFlow: MutableStateFlow
}
@Single(binds = [AppStateDataSource::class])
@@ -42,4 +44,6 @@ class AppStateDataSourceImpl : AppStateDataSource {
override val cameraSessionFlow = MutableStateFlow(null)
override val cameraStateFlow = MutableStateFlow(CameraState.DEFAULT)
override val lastCameraVideoAssetFlow = MutableStateFlow(null)
+
+ override val localizationStateFlow = MutableStateFlow(LocalizationState.DEFAULT)
}
diff --git a/data/datastore/src/commonMain/kotlin/com/velord/data/datastore/DataStoreDataSource.kt b/data/datastore/src/commonMain/kotlin/com/velord/data/datastore/DataStoreDataSource.kt
index 34b131d4..275348e5 100644
--- a/data/datastore/src/commonMain/kotlin/com/velord/data/datastore/DataStoreDataSource.kt
+++ b/data/datastore/src/commonMain/kotlin/com/velord/data/datastore/DataStoreDataSource.kt
@@ -3,6 +3,7 @@ package com.velord.data.datastore
import com.velord.data.datastore.appSetting.AppSettingDataStore
import com.velord.model.movie.MovieFilterOption
import com.velord.model.setting.AppSetting
+import com.velord.model.setting.LanguagePreference
import com.velord.model.setting.ThemeConfig
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
@@ -14,6 +15,7 @@ interface DataStoreDataSource {
suspend fun setThemeConfig(theme: ThemeConfig)
fun getAppSettingFlow(): Flow
suspend fun setMovieFilters(filters: List)
+ suspend fun setLanguagePreference(language: LanguagePreference)
}
@Single(binds = [DataStoreDataSource::class])
@@ -48,4 +50,10 @@ class DataStoreDataSourceImpl(
it.copy(movieFilters = filters)
}
}
+
+ override suspend fun setLanguagePreference(language: LanguagePreference) {
+ appSetting.updateData {
+ it.copy(language = language)
+ }
+ }
}
diff --git a/data/gateway/build.gradle.kts b/data/gateway/build.gradle.kts
index 67c8f2e7..437b9d4a 100644
--- a/data/gateway/build.gradle.kts
+++ b/data/gateway/build.gradle.kts
@@ -12,11 +12,14 @@ kotlin {
commonMain.dependencies {
// Module Model
implementation(projects.model)
+ // Module Core
+ implementation(projects.core.coreResource)
// Module Data
implementation(projects.data.appstate)
implementation(projects.data.backend)
implementation(projects.data.datastore)
implementation(projects.data.db)
+ implementation(projects.data.localization)
implementation(projects.data.os)
// Module Domain
implementation(projects.domain.usecaseMovie)
diff --git a/data/gateway/src/commonMain/kotlin/com/velord/data/gateway/DI.kt b/data/gateway/src/commonMain/kotlin/com/velord/data/gateway/DI.kt
index 1ae49122..767e76a8 100644
--- a/data/gateway/src/commonMain/kotlin/com/velord/data/gateway/DI.kt
+++ b/data/gateway/src/commonMain/kotlin/com/velord/data/gateway/DI.kt
@@ -6,6 +6,7 @@ import com.velord.data.appstate.AppStateModule
import com.velord.data.backend.ktor.BackendModule
import com.velord.data.datastore.DataStoreModule
import com.velord.data.db.DbModule
+import com.velord.data.localization.LocalizationModule
import com.velord.data.os.OsModule
import org.koin.core.annotation.ComponentScan
import org.koin.core.annotation.Module
@@ -16,6 +17,7 @@ import org.koin.core.annotation.Module
DbModule::class,
BackendModule::class,
DataStoreModule::class,
+ LocalizationModule::class,
OsModule::class,
],
)
diff --git a/data/gateway/src/commonMain/kotlin/com/velord/data/gateway/localization/LocalizationGateway.kt b/data/gateway/src/commonMain/kotlin/com/velord/data/gateway/localization/LocalizationGateway.kt
new file mode 100644
index 00000000..17894db0
--- /dev/null
+++ b/data/gateway/src/commonMain/kotlin/com/velord/data/gateway/localization/LocalizationGateway.kt
@@ -0,0 +1,97 @@
+package com.velord.data.gateway.localization
+
+import co.touchlab.kermit.Logger
+import com.velord.data.appstate.AppStateDataSource
+import com.velord.data.gateway.setting.LanguagePreferenceGateway
+import com.velord.data.localization.LocalizationDataSource
+import com.velord.model.localization.LocalizationDocument
+import com.velord.model.localization.LocalizationState
+import com.velord.model.setting.LanguagePreference
+import kotlinx.coroutines.CancellationException
+import kotlinx.coroutines.CoroutineExceptionHandler
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+import org.koin.core.annotation.Single
+
+private val log = Logger.withTag("LocalizationGateway")
+
+@Single
+class LocalizationGateway(
+ private val localizationDataSource: LocalizationDataSource,
+ private val languagePreferenceGateway: LanguagePreferenceGateway,
+ private val appState: AppStateDataSource,
+) {
+ private val errorHandler = CoroutineExceptionHandler { _, throwable ->
+ log.e(throwable) { "Unhandled error in LocalizationGateway coroutine scope" }
+ localizationDataSource.recordException(throwable)
+ }
+ private val scope = CoroutineScope(
+ SupervisorJob() + Dispatchers.Default + errorHandler
+ )
+
+ suspend fun initialize() {
+ if (appState.localizationStateFlow.value != LocalizationState.DEFAULT) return
+
+ // Activate bundled (or previously activated remote) instantly
+ val bundled = localizationDataSource.getBundled()
+ val initial = localizationDataSource.getActivatedRemote(bundled) ?: bundled
+ publishLocalization(initial)
+
+ // Fetch new updates in background
+ scope.launch {
+ fetchAndPublishLocalization(bundled)
+ }
+ }
+
+ fun getStateFlow(): StateFlow = appState.localizationStateFlow
+
+ fun getLanguagePreferenceFlow(): Flow = languagePreferenceGateway.getFlow()
+
+ suspend fun setLanguagePreference(preference: LanguagePreference) {
+ val current = appState.localizationStateFlow.value
+ val deviceLanguage = localizationDataSource.currentLanguageCode()
+ val language = current.document.resolveLanguage(
+ preference = preference,
+ currentDeviceLanguage = deviceLanguage
+ )
+
+ languagePreferenceGateway.save(preference)
+ appState.localizationStateFlow.update {
+ it.copy(language = language)
+ }
+ }
+
+ private suspend fun fetchAndPublishLocalization(bundled: LocalizationDocument) {
+ try {
+ localizationDataSource.fetchAndActivate()
+ val activated = localizationDataSource.getActivatedRemote(bundled)
+ if (activated != null) {
+ publishLocalization(activated)
+ }
+ } catch (error: CancellationException) {
+ throw error
+ } catch (error: Exception) {
+ log.w(error) { "Failed to fetch remote localization from Remote Config" }
+ }
+ }
+
+ private suspend fun publishLocalization(document: LocalizationDocument) {
+ val preference = languagePreferenceGateway.get()
+ val deviceLanguage = localizationDataSource.currentLanguageCode()
+ val language = document.resolveLanguage(
+ preference = preference,
+ currentDeviceLanguage = deviceLanguage
+ )
+
+ val localizationState = LocalizationState(
+ document = document,
+ language = language,
+ )
+ appState.localizationStateFlow.value = localizationState
+ }
+}
diff --git a/data/gateway/src/commonMain/kotlin/com/velord/data/gateway/setting/LanguagePreferenceGateway.kt b/data/gateway/src/commonMain/kotlin/com/velord/data/gateway/setting/LanguagePreferenceGateway.kt
new file mode 100644
index 00000000..8f32fbb5
--- /dev/null
+++ b/data/gateway/src/commonMain/kotlin/com/velord/data/gateway/setting/LanguagePreferenceGateway.kt
@@ -0,0 +1,22 @@
+package com.velord.data.gateway.setting
+
+import com.velord.data.datastore.DataStoreDataSource
+import com.velord.model.setting.LanguagePreference
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.map
+import org.koin.core.annotation.Single
+
+@Single
+class LanguagePreferenceGateway(private val dataStore: DataStoreDataSource) {
+
+ fun getFlow(): Flow = dataStore
+ .getAppSettingFlow()
+ .map { it.language }
+
+ suspend fun get(): LanguagePreference = getFlow().first()
+
+ suspend fun save(language: LanguagePreference) {
+ dataStore.setLanguagePreference(language)
+ }
+}
diff --git a/data/localization/build.gradle.kts b/data/localization/build.gradle.kts
new file mode 100644
index 00000000..ed3ea0a9
--- /dev/null
+++ b/data/localization/build.gradle.kts
@@ -0,0 +1,58 @@
+plugins {
+ alias(libs.plugins.convention.multiplatform.library)
+ alias(libs.plugins.kotlin.serialization)
+ alias(libs.plugins.ksp)
+}
+
+kotlin {
+ android {
+ namespace = "com.velord.data.localization"
+ }
+
+ sourceSets {
+ commonMain.dependencies {
+ // Module Model
+ implementation(projects.model)
+ // Module Core
+ implementation(projects.core.coreResource)
+ // Module Infrastructure
+ implementation(projects.infrastructure.config)
+ // Kotlin
+ implementation(libs.kotlin.coroutine.core)
+ implementation(libs.kotlin.serialization.json)
+ // Koin
+ implementation(libs.koin.core)
+ api(libs.koin.annotation)
+ // Firebase
+ implementation(libs.firebase.kit.remote.config)
+ implementation(libs.firebase.kit.crashlytics)
+ }
+ androidMain.dependencies {
+ // Firebase
+ implementation(project.dependencies.platform(libs.google.firebase.bom))
+ implementation(libs.google.firebase.config)
+ }
+ commonTest.dependencies {
+ // Kotlin
+ implementation(libs.kotlin.test)
+ }
+
+ named("commonMain").configure {
+ kotlin.srcDir("build/generated/ksp/metadata/commonMain/kotlin")
+ }
+ }
+}
+
+dependencies {
+ // KSP
+ add("kspCommonMainMetadata", libs.koin.ksp)
+ add("kspAndroid", libs.koin.ksp)
+ add("kspDesktop", libs.koin.ksp)
+}
+
+tasks.matching {
+ it.name.startsWith("ksp") &&
+ it.name != "kspCommonMainKotlinMetadata"
+}.configureEach {
+ dependsOn("kspCommonMainKotlinMetadata")
+}
diff --git a/data/localization/src/androidMain/kotlin/com/velord/data/localization/LocalizationPlatform.kt b/data/localization/src/androidMain/kotlin/com/velord/data/localization/LocalizationPlatform.kt
new file mode 100644
index 00000000..8252b332
--- /dev/null
+++ b/data/localization/src/androidMain/kotlin/com/velord/data/localization/LocalizationPlatform.kt
@@ -0,0 +1,17 @@
+package com.velord.data.localization
+
+import com.google.firebase.remoteconfig.FirebaseRemoteConfig
+import com.google.firebase.remoteconfig.FirebaseRemoteConfigSettings
+import java.util.Locale
+
+internal actual object LocalizationPlatform {
+
+ actual suspend fun initializeRemoteConfig() {
+ val settings = FirebaseRemoteConfigSettings.Builder()
+ .setMinimumFetchIntervalInSeconds(0) // 0s cache for immediate updates
+ .build()
+ FirebaseRemoteConfig.getInstance().setConfigSettingsAsync(settings)
+ }
+
+ actual fun currentLanguageTag(): String = Locale.getDefault().toLanguageTag()
+}
\ No newline at end of file
diff --git a/data/localization/src/commonMain/kotlin/com/velord/data/localization/DI.kt b/data/localization/src/commonMain/kotlin/com/velord/data/localization/DI.kt
new file mode 100644
index 00000000..c2179ec0
--- /dev/null
+++ b/data/localization/src/commonMain/kotlin/com/velord/data/localization/DI.kt
@@ -0,0 +1,10 @@
+@file:Suppress("MatchingDeclarationName")
+
+package com.velord.data.localization
+
+import org.koin.core.annotation.ComponentScan
+import org.koin.core.annotation.Module
+
+@Module
+@ComponentScan("com.velord.data.localization")
+class LocalizationModule
diff --git a/data/localization/src/commonMain/kotlin/com/velord/data/localization/LocalizationDataSource.kt b/data/localization/src/commonMain/kotlin/com/velord/data/localization/LocalizationDataSource.kt
new file mode 100644
index 00000000..5d4abda0
--- /dev/null
+++ b/data/localization/src/commonMain/kotlin/com/velord/data/localization/LocalizationDataSource.kt
@@ -0,0 +1,163 @@
+package com.velord.data.localization
+
+import com.firebasekit.core.Firebase
+import com.firebasekit.crashlytics.crashlytics
+import com.firebasekit.remoteconfig.remoteConfig
+import com.velord.core.resource.Res
+import com.velord.model.localization.LanguageCode
+import com.velord.model.localization.LocalizationDocument
+import com.velord.model.localization.LocalizationStringMap
+import com.velord.model.setting.LanguagePreference
+import kotlinx.coroutines.CancellationException
+import kotlinx.serialization.json.Json
+import kotlinx.serialization.json.JsonObject
+import kotlinx.serialization.json.jsonObject
+import kotlinx.serialization.json.jsonPrimitive
+import org.jetbrains.compose.resources.ExperimentalResourceApi
+import org.koin.core.annotation.Single
+
+private const val LOCALIZATION_RESOURCE_PATH = "files/localization.json"
+private const val LOCALIZATION_PARAMETER = "localization"
+private const val KEY_SCHEMA_VERSION = "schemaVersion"
+private const val KEY_DEFAULT_LANGUAGE = "defaultLanguage"
+private const val KEY_LANGUAGE_ROSTER = "languageRoster"
+private const val SUPPORTED_SCHEMA_VERSION = 1
+private val PLACEHOLDER_REGEX = Regex("""%([0-9]+)[$]([sd])""")
+
+@Single
+class LocalizationDataSource {
+
+ private val json = Json { ignoreUnknownKeys = false }
+
+ @OptIn(ExperimentalResourceApi::class)
+ suspend fun getBundled(): LocalizationDocument {
+ val jsonString = Res.readBytes(LOCALIZATION_RESOURCE_PATH).decodeToString()
+ return parse(jsonString)
+ }
+
+ suspend fun getActivatedRemote(bundled: LocalizationDocument): LocalizationDocument? = try {
+ val remoteJson = getLocalization()
+ if (remoteJson != null) {
+ parseRemote(remoteJson, bundled)
+ } else {
+ null
+ }
+ } catch (error: CancellationException) {
+ throw error
+ } catch (error: Exception) {
+ recordException(error)
+ null
+ }
+
+ suspend fun getLocalization(): String? {
+ LocalizationPlatform.initializeRemoteConfig()
+ return Firebase.remoteConfig.getString(LOCALIZATION_PARAMETER)?.takeIf(String::isNotBlank)
+ }
+
+ suspend fun fetchAndActivate() {
+ LocalizationPlatform.initializeRemoteConfig()
+ Firebase.remoteConfig.fetchAndActivate()
+ }
+
+ fun recordException(error: Throwable) {
+ Firebase.crashlytics.recordException(error)
+ }
+
+ fun currentLanguageCode(): LanguageCode =
+ LanguageCode(LocalizationPlatform.currentLanguageTag())
+
+ fun parse(value: String): LocalizationDocument {
+ val root = json.parseToJsonElement(value).jsonObject
+ val schemaVersion = root.getValue(KEY_SCHEMA_VERSION).jsonPrimitive.content.toInt()
+ val defaultLanguage = LanguageCode(root.getValue(KEY_DEFAULT_LANGUAGE).jsonPrimitive.content)
+ val rawLanguageRoster = root.getValue(KEY_LANGUAGE_ROSTER).jsonObject
+
+ val languageRoster = rawLanguageRoster.entries.associate { (language, rawStrings) ->
+ val stringMap = rawStrings.jsonObject.toLocalizationStringMap(language)
+ LanguageCode(language) to stringMap
+ }
+
+ val document = LocalizationDocument(
+ schemaVersion = schemaVersion,
+ defaultLanguage = defaultLanguage,
+ languageRoster = languageRoster,
+ )
+ validate(document)
+ return document
+ }
+
+ fun parseRemote(value: String, bundled: LocalizationDocument): LocalizationDocument {
+ val remote = parse(value)
+ val bundledDefaultStrings = bundled.languageRoster.getValue(bundled.defaultLanguage)
+ val bundledKeyRoster = bundledDefaultStrings.keys
+
+ require(remote.schemaVersion == bundled.schemaVersion) {
+ "Remote localization schema does not match bundled localization"
+ }
+ require(remote.defaultLanguage == bundled.defaultLanguage) {
+ "Remote localization default language does not match bundled localization"
+ }
+ require(remote.languageRoster.keys == bundled.languageRoster.keys) {
+ "Remote localization language roster does not match bundled localization"
+ }
+ remote.languageRoster.forEach { (language, stringMap) ->
+ val missingKeys = bundledKeyRoster - stringMap.keys
+ val extraKeys = stringMap.keys - bundledKeyRoster
+ require(stringMap.keys == bundledKeyRoster) {
+ "Remote key mismatch for '${language.value}'. Missing: $missingKeys, Extra: $extraKeys"
+ }
+ }
+ return remote
+ }
+
+ private fun validate(document: LocalizationDocument) {
+ require(document.schemaVersion == SUPPORTED_SCHEMA_VERSION) {
+ "Unsupported localization schema: ${document.schemaVersion}"
+ }
+ require(document.languageRoster.isNotEmpty()) {
+ "Localization must contain at least one language"
+ }
+ val supportedLanguageRoster = LanguagePreference.entries
+ .filterNot { it.isDefault }
+ .map { it.languageCode }
+ .toSet()
+ require(document.languageRoster.keys == supportedLanguageRoster) {
+ "Localization language roster does not match supported languages"
+ }
+ val defaultStrings = requireNotNull(document.languageRoster[document.defaultLanguage]) {
+ "Localization must contain default language: ${document.defaultLanguage.value}"
+ }
+ require(defaultStrings.value.isNotEmpty()) {
+ "Localization must contain strings"
+ }
+ document.languageRoster.forEach { (language, stringMap) ->
+ require(stringMap.keys == defaultStrings.keys) {
+ "Localization key mismatch for language: ${language.value}"
+ }
+ stringMap.value.forEach { (key, value) ->
+ require(value.isNotEmpty()) { "Localization value is empty: ${language.value}/$key" }
+ val defaultValue = defaultStrings.value.getValue(key)
+ val defaultPlaceholderRoster = placeholders(defaultValue)
+ val placeholderRoster = placeholders(value)
+ require(placeholderRoster == defaultPlaceholderRoster) {
+ "Localization placeholder mismatch: ${language.value}/$key"
+ }
+ }
+ }
+ }
+
+ private fun JsonObject.toLocalizationStringMap(language: String): LocalizationStringMap {
+ val stringMap = mapValues { (key, value) ->
+ val primitive = value.jsonPrimitive
+ require(primitive.isString) { "Localization value must be a string: $language/$key" }
+ primitive.content
+ }
+ return LocalizationStringMap(stringMap)
+ }
+
+ private fun placeholders(value: String): List = PLACEHOLDER_REGEX
+ .findAll(value)
+ .map { it.value }
+ .sorted()
+ .toList()
+}
diff --git a/data/localization/src/commonMain/kotlin/com/velord/data/localization/LocalizationPlatform.kt b/data/localization/src/commonMain/kotlin/com/velord/data/localization/LocalizationPlatform.kt
new file mode 100644
index 00000000..0d0ad587
--- /dev/null
+++ b/data/localization/src/commonMain/kotlin/com/velord/data/localization/LocalizationPlatform.kt
@@ -0,0 +1,6 @@
+package com.velord.data.localization
+
+internal expect object LocalizationPlatform {
+ suspend fun initializeRemoteConfig()
+ fun currentLanguageTag(): String
+}
diff --git a/data/localization/src/commonTest/kotlin/com/velord/data/localization/LocalizationDataSourceTest.kt b/data/localization/src/commonTest/kotlin/com/velord/data/localization/LocalizationDataSourceTest.kt
new file mode 100644
index 00000000..6a27f1f9
--- /dev/null
+++ b/data/localization/src/commonTest/kotlin/com/velord/data/localization/LocalizationDataSourceTest.kt
@@ -0,0 +1,143 @@
+package com.velord.data.localization
+
+import com.velord.model.localization.LanguageCode
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFails
+
+class LocalizationDataSourceTest {
+
+ private val dataSource = LocalizationDataSource()
+
+ @Test
+ fun `parses declared default language`() {
+ val document = dataSource.parse(localizationJson())
+
+ assertEquals(LanguageCode("en"), document.defaultLanguage)
+ assertEquals(setOf(LanguageCode("en"), LanguageCode("es")), document.languageRoster.keys)
+ }
+
+ @Test
+ fun `unsupported bundled language fails parsing`() {
+ val localization = localizationJson(
+ extraLanguageRoster = """
+ ,"fr": {
+ "language_name": "Français",
+ "settings": "Paramètres",
+ "count": "Nombre : %1${'$'}d"
+ }
+ """.trimIndent(),
+ )
+
+ assertFails {
+ dataSource.parse(localization)
+ }
+ }
+
+ @Test
+ fun `default language must be declared`() {
+ assertFails {
+ dataSource.parse(localizationJson(defaultLanguage = "de"))
+ }
+ }
+
+ @Test
+ fun `all languages must have the same keys`() {
+ val invalid = localizationJson().replace("\"settings\": \"Configuración\",", "")
+
+ assertFails {
+ dataSource.parse(invalid)
+ }
+ }
+
+ @Test
+ fun `placeholder types must match default language`() {
+ val invalid = localizationJson().replace("Cantidad: %1\$d", "Cantidad: %1\$s")
+
+ assertFails {
+ dataSource.parse(invalid)
+ }
+ }
+
+ @Test
+ fun `remote cannot add an unsupported language`() {
+ val bundled = dataSource.parse(localizationJson())
+ val remote = localizationJson(
+ extraLanguageRoster = """
+ ,"fr": {
+ "language_name": "Français",
+ "settings": "Paramètres",
+ "count": "Nombre : %1${'$'}d"
+ }
+ """.trimIndent(),
+ )
+
+ assertFails {
+ dataSource.parseRemote(remote, bundled)
+ }
+ }
+
+ @Test
+ fun `remote cannot remove a bundled language`() {
+ val bundled = dataSource.parse(localizationJson())
+ val remote = """
+ {
+ "schemaVersion": 1,
+ "defaultLanguage": "en",
+ "languageRoster": {
+ "en": {
+ "language_name": "English",
+ "settings": "Remote settings",
+ "count": "Count: %1${'$'}d"
+ }
+ }
+ }
+ """.trimIndent()
+
+ assertFails {
+ dataSource.parseRemote(remote, bundled)
+ }
+ }
+
+ @Test
+ fun `remote cannot change default language`() {
+ val bundled = dataSource.parse(localizationJson())
+ val remote = localizationJson(defaultLanguage = "es")
+
+ assertFails {
+ dataSource.parseRemote(remote, bundled)
+ }
+ }
+
+ @Test
+ fun `malformed remote localization fails parsing`() {
+ val bundled = dataSource.parse(localizationJson())
+
+ assertFails {
+ dataSource.parseRemote("{", bundled)
+ }
+ }
+
+ private fun localizationJson(
+ defaultLanguage: String = "en",
+ extraLanguageRoster: String = "",
+ ): String = """
+ {
+ "schemaVersion": 1,
+ "defaultLanguage": "$defaultLanguage",
+ "languageRoster": {
+ "en": {
+ "language_name": "English",
+ "settings": "Settings",
+ "count": "Count: %1${'$'}d"
+ },
+ "es": {
+ "language_name": "Español",
+ "settings": "Configuración",
+ "count": "Cantidad: %1${'$'}d"
+ }
+ $extraLanguageRoster
+ }
+ }
+ """.trimIndent()
+}
diff --git a/data/localization/src/desktopMain/kotlin/com/velord/data/localization/LocalizationPlatform.kt b/data/localization/src/desktopMain/kotlin/com/velord/data/localization/LocalizationPlatform.kt
new file mode 100644
index 00000000..bba9bfb8
--- /dev/null
+++ b/data/localization/src/desktopMain/kotlin/com/velord/data/localization/LocalizationPlatform.kt
@@ -0,0 +1,41 @@
+package com.velord.data.localization
+
+import com.firebasekit.core.Firebase
+import com.firebasekit.core.initialize
+import com.velord.infrastructure.config.GeneratedBuildConfigResolver
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
+import java.util.Locale
+
+internal actual object LocalizationPlatform {
+
+ private val initializationMutex = Mutex()
+
+ @Volatile
+ private var isRemoteConfigInitialized = false
+
+ actual suspend fun initializeRemoteConfig() {
+ if (isRemoteConfigInitialized) return
+
+ initializationMutex.withLock {
+ if (isRemoteConfigInitialized) return@withLock
+
+ val config = GeneratedBuildConfigResolver()
+ val apiKey = config.getFirebaseApiKey().takeIf { it.isNotBlank() }
+ ?: error("Desktop Firebase API key is missing. Set firebase.apiKey in local.properties.")
+ val projectId = config.getFirebaseProjectId().takeIf { it.isNotBlank() }
+ ?: error("Desktop Firebase project ID is missing. Set firebase.projectId in local.properties.")
+ val appId = config.getFirebaseAppId().takeIf { it.isNotBlank() }
+ ?: error("Desktop Firebase app ID is missing. Set firebase.appId in local.properties.")
+
+ Firebase.initialize(
+ apiKey = apiKey,
+ projectId = projectId,
+ appId = appId,
+ )
+ isRemoteConfigInitialized = true
+ }
+ }
+
+ actual fun currentLanguageTag(): String = Locale.getDefault().toLanguageTag()
+}
diff --git a/domain/usecase-setting/src/commonMain/kotlin/com/velord/usecase/setting/GetLanguagePreferenceUC.kt b/domain/usecase-setting/src/commonMain/kotlin/com/velord/usecase/setting/GetLanguagePreferenceUC.kt
new file mode 100644
index 00000000..81ca15df
--- /dev/null
+++ b/domain/usecase-setting/src/commonMain/kotlin/com/velord/usecase/setting/GetLanguagePreferenceUC.kt
@@ -0,0 +1,6 @@
+package com.velord.usecase.setting
+
+import com.velord.model.setting.LanguagePreference
+import kotlinx.coroutines.flow.Flow
+
+fun interface GetLanguagePreferenceUC : () -> Flow
diff --git a/domain/usecase-setting/src/commonMain/kotlin/com/velord/usecase/setting/GetLocalizationStateUC.kt b/domain/usecase-setting/src/commonMain/kotlin/com/velord/usecase/setting/GetLocalizationStateUC.kt
new file mode 100644
index 00000000..6bd27656
--- /dev/null
+++ b/domain/usecase-setting/src/commonMain/kotlin/com/velord/usecase/setting/GetLocalizationStateUC.kt
@@ -0,0 +1,6 @@
+package com.velord.usecase.setting
+
+import com.velord.model.localization.LocalizationState
+import kotlinx.coroutines.flow.StateFlow
+
+fun interface GetLocalizationStateUC : () -> StateFlow
diff --git a/domain/usecase-setting/src/commonMain/kotlin/com/velord/usecase/setting/InitializeLocalizationUC.kt b/domain/usecase-setting/src/commonMain/kotlin/com/velord/usecase/setting/InitializeLocalizationUC.kt
new file mode 100644
index 00000000..32a22158
--- /dev/null
+++ b/domain/usecase-setting/src/commonMain/kotlin/com/velord/usecase/setting/InitializeLocalizationUC.kt
@@ -0,0 +1,3 @@
+package com.velord.usecase.setting
+
+fun interface InitializeLocalizationUC : suspend () -> Unit
diff --git a/domain/usecase-setting/src/commonMain/kotlin/com/velord/usecase/setting/SetLanguagePreferenceUC.kt b/domain/usecase-setting/src/commonMain/kotlin/com/velord/usecase/setting/SetLanguagePreferenceUC.kt
new file mode 100644
index 00000000..23767791
--- /dev/null
+++ b/domain/usecase-setting/src/commonMain/kotlin/com/velord/usecase/setting/SetLanguagePreferenceUC.kt
@@ -0,0 +1,5 @@
+package com.velord.usecase.setting
+
+import com.velord.model.setting.LanguagePreference
+
+fun interface SetLanguagePreferenceUC : suspend (LanguagePreference) -> Unit
diff --git a/gradle.properties b/gradle.properties
index 3521c241..9046642a 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -21,6 +21,8 @@ kotlin.code.style=official
# resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true
+# Firebase-KMP-Kit 0.4.0 publishes Android modules with a shared namespace.
+android.uniquePackageNames=false
# Improve build speed
org.gradle.unsafe.configuration-cache=true
# Use this flag carefully, in case some of the plugins are not fully compatible.
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 063b6bf5..3ad8ec79 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -18,6 +18,7 @@ koinKsp = "2.3.2-Beta1"
# Google Firebase
googleFirebaseBom = "34.16.0"
googleFirebaseCrashlyticPlugin = "3.0.7"
+firebaseKit = "0.4.0"
# Google Other
googleGuava = "33.6.0-android"
googleKsp = "2.3.4"
@@ -621,6 +622,9 @@ google-firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref
google-firebase-messaging = { module = "com.google.firebase:firebase-messaging" }
google-firebase-crashlytic = { module = "com.google.firebase:firebase-crashlytics" }
google-firebase-analytic = { module = "com.google.firebase:firebase-analytics" }
+google-firebase-config = { module = "com.google.firebase:firebase-config" }
+firebase-kit-remote-config = { module = "io.github.razotron.firebase-kit:remote-config", version.ref = "firebaseKit" }
+firebase-kit-crashlytics = { module = "io.github.razotron.firebase-kit:crashlytics", version.ref = "firebaseKit" }
## Google Other
google-guava = { module = "com.google.guava:guava", version.ref = "googleGuava" }
google-modernstorage = { module = "com.google.modernstorage:modernstorage-photopicker", version.ref = "googleModernStorage" }
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index a351597e..73053b72 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,7 @@
+#Thu Aug 20 13:18:14 AST 2026
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.1-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
diff --git a/infrastructure/config/build.gradle.kts b/infrastructure/config/build.gradle.kts
index f9f4f126..9cbf8be8 100644
--- a/infrastructure/config/build.gradle.kts
+++ b/infrastructure/config/build.gradle.kts
@@ -2,6 +2,7 @@ import com.codingfeline.buildkonfig.compiler.FieldSpec.Type.BOOLEAN
import com.codingfeline.buildkonfig.compiler.FieldSpec.Type.STRING
import com.velord.buildlogic.model.BuildEnvironment
import com.velord.buildlogic.model.BuildType
+import java.util.Properties
private val baseUrl = "https://google.com"
private val buildConfigPackage = "com.velord.infrastructure.config"
@@ -9,6 +10,17 @@ private val buildConfigObject = "ProjectBuildConfig"
private val debugNavigationLib = "Nav3"
private val releaseNavigationLib = "Nav3"
+val localProperties = Properties().apply {
+ val localPropertiesFile = rootProject.file("local.properties")
+ if (localPropertiesFile.exists()) {
+ localPropertiesFile.inputStream().use { load(it) }
+ }
+}
+
+val firebaseApiKey: String = localProperties.getProperty("firebase.apiKey").orEmpty()
+val firebaseProjectId: String = localProperties.getProperty("firebase.projectId").orEmpty()
+val firebaseAppId: String = localProperties.getProperty("firebase.appId").orEmpty()
+
plugins {
alias(libs.plugins.convention.build.config)
}
@@ -27,6 +39,9 @@ buildkonfig {
buildConfigField(BOOLEAN, "isLoggingEnabled", "true")
buildConfigField(STRING, "navigationLib", debugNavigationLib)
buildConfigField(STRING, "baseUrl", baseUrl)
+ buildConfigField(STRING, "firebaseApiKey", firebaseApiKey)
+ buildConfigField(STRING, "firebaseProjectId", firebaseProjectId)
+ buildConfigField(STRING, "firebaseAppId", firebaseAppId)
}
BuildEnvironment.entries.forEach { environment ->
@@ -35,6 +50,9 @@ buildkonfig {
buildConfigField(BOOLEAN, "isLoggingEnabled", "true")
buildConfigField(STRING, "navigationLib", debugNavigationLib)
buildConfigField(STRING, "baseUrl", baseUrl)
+ buildConfigField(STRING, "firebaseApiKey", firebaseApiKey)
+ buildConfigField(STRING, "firebaseProjectId", firebaseProjectId)
+ buildConfigField(STRING, "firebaseAppId", firebaseAppId)
}
}
@@ -44,6 +62,9 @@ buildkonfig {
buildConfigField(BOOLEAN, "isLoggingEnabled", "false")
buildConfigField(STRING, "navigationLib", releaseNavigationLib)
buildConfigField(STRING, "baseUrl", baseUrl)
+ buildConfigField(STRING, "firebaseApiKey", firebaseApiKey)
+ buildConfigField(STRING, "firebaseProjectId", firebaseProjectId)
+ buildConfigField(STRING, "firebaseAppId", firebaseAppId)
}
}
}
diff --git a/infrastructure/config/src/commonMain/kotlin/com/velord/infrastructure/config/BuildConfigResolver.kt b/infrastructure/config/src/commonMain/kotlin/com/velord/infrastructure/config/BuildConfigResolver.kt
index 80f71183..aa3d3481 100644
--- a/infrastructure/config/src/commonMain/kotlin/com/velord/infrastructure/config/BuildConfigResolver.kt
+++ b/infrastructure/config/src/commonMain/kotlin/com/velord/infrastructure/config/BuildConfigResolver.kt
@@ -4,4 +4,8 @@ interface BuildConfigResolver {
fun getNavigationLib(): NavigationLib
fun getBaseUrl(): String
fun isLoggingEnabled(): Boolean
+ fun getFirebaseApiKey(): String
+ fun getFirebaseProjectId(): String
+ fun getFirebaseAppId(): String
}
+
diff --git a/infrastructure/config/src/commonMain/kotlin/com/velord/infrastructure/config/GeneratedBuildConfigResolver.kt b/infrastructure/config/src/commonMain/kotlin/com/velord/infrastructure/config/GeneratedBuildConfigResolver.kt
index 67910b5b..dc117020 100644
--- a/infrastructure/config/src/commonMain/kotlin/com/velord/infrastructure/config/GeneratedBuildConfigResolver.kt
+++ b/infrastructure/config/src/commonMain/kotlin/com/velord/infrastructure/config/GeneratedBuildConfigResolver.kt
@@ -10,4 +10,11 @@ class GeneratedBuildConfigResolver : BuildConfigResolver {
override fun getBaseUrl(): String = ProjectBuildConfig.baseUrl
override fun isLoggingEnabled(): Boolean = ProjectBuildConfig.isLoggingEnabled
+
+ override fun getFirebaseApiKey(): String = ProjectBuildConfig.firebaseApiKey
+
+ override fun getFirebaseProjectId(): String = ProjectBuildConfig.firebaseProjectId
+
+ override fun getFirebaseAppId(): String = ProjectBuildConfig.firebaseAppId
}
+
diff --git a/infrastructure/di/build.gradle.kts b/infrastructure/di/build.gradle.kts
index 0fa62809..7c826d33 100644
--- a/infrastructure/di/build.gradle.kts
+++ b/infrastructure/di/build.gradle.kts
@@ -24,6 +24,7 @@ kotlin {
implementation(projects.data.appstate)
implementation(projects.data.gateway)
implementation(projects.data.db)
+ implementation(projects.data.localization)
implementation(projects.data.os)
// Module UI
implementation(projects.ui.sharedviewmodel)
diff --git a/infrastructure/di/src/commonMain/kotlin/com/velord/infrastructure/di/UseCaseModule.kt b/infrastructure/di/src/commonMain/kotlin/com/velord/infrastructure/di/UseCaseModule.kt
index a4627f2d..1b66d76d 100644
--- a/infrastructure/di/src/commonMain/kotlin/com/velord/infrastructure/di/UseCaseModule.kt
+++ b/infrastructure/di/src/commonMain/kotlin/com/velord/infrastructure/di/UseCaseModule.kt
@@ -5,6 +5,7 @@ import com.velord.data.gateway.camera.CameraSessionGateway
import com.velord.data.gateway.camera.CameraStateGateway
import com.velord.data.gateway.event.AppEventGateway
import com.velord.data.gateway.file.FileGateway
+import com.velord.data.gateway.localization.LocalizationGateway
import com.velord.data.gateway.movie.MovieByGateway
import com.velord.data.gateway.movie.MovieFavoriteGateway
import com.velord.data.gateway.movie.MovieFilterGateway
@@ -38,7 +39,11 @@ import com.velord.usecase.movie.SetMovieFilterOptionUC
import com.velord.usecase.movie.SetMovieSortOptionUC
import com.velord.usecase.movie.ShareMovieUC
import com.velord.usecase.movie.UpdateMovieLikeUC
+import com.velord.usecase.setting.GetLanguagePreferenceUC
+import com.velord.usecase.setting.GetLocalizationStateUC
import com.velord.usecase.setting.GetThemeConfigUC
+import com.velord.usecase.setting.InitializeLocalizationUC
+import com.velord.usecase.setting.SetLanguagePreferenceUC
import com.velord.usecase.setting.SwitchAbideToOsThemeConfigUC
import com.velord.usecase.setting.SwitchDarkThemeConfigUC
import com.velord.usecase.setting.SwitchDynamicColorThemeConfigUC
@@ -69,6 +74,18 @@ val useCaseModule = module {
single {
SwitchDynamicColorThemeConfigUC(get()::switchDynamicColor)
}
+ single {
+ InitializeLocalizationUC(get()::initialize)
+ }
+ single {
+ GetLocalizationStateUC(get()::getStateFlow)
+ }
+ single {
+ GetLanguagePreferenceUC(get()::getLanguagePreferenceFlow)
+ }
+ single {
+ SetLanguagePreferenceUC(get()::setLanguagePreference)
+ }
single {
GetAllMovieUC(get()::getBySortAndFilter)
}
diff --git a/infrastructure/di/src/commonMain/kotlin/com/velord/infrastructure/di/ViewModelModule.kt b/infrastructure/di/src/commonMain/kotlin/com/velord/infrastructure/di/ViewModelModule.kt
index 73bb75b9..b1caab36 100644
--- a/infrastructure/di/src/commonMain/kotlin/com/velord/infrastructure/di/ViewModelModule.kt
+++ b/infrastructure/di/src/commonMain/kotlin/com/velord/infrastructure/di/ViewModelModule.kt
@@ -9,6 +9,7 @@ import com.velord.ui.feature.movie.viewModel.AllMovieVM
import com.velord.ui.feature.movie.viewModel.FavoriteMovieVM
import com.velord.ui.feature.movie.viewModel.MovieVM
import com.velord.ui.feature.splash.SplashVM
+import com.velord.ui.sharedviewmodel.LanguageVM
import com.velord.ui.sharedviewmodel.MainVM
import com.velord.ui.sharedviewmodel.ThemeVM
import org.koin.core.module.dsl.viewModel
@@ -17,6 +18,7 @@ import org.koin.dsl.module
val viewModelModule = module {
// VieModel that can not be \or have not to be instantiated here has their own module DI
viewModel { ThemeVM(get(), get(), get(), get()) }
+ viewModel { LanguageVM(get(), get()) }
viewModel { SplashVM() }
viewModel { DemoVM(get(), get()) }
viewModel { FlowSummatorVM() }
diff --git a/infrastructure/navigation/src/androidMain/kotlin/com/velord/infrastructure/navigation/compose/destinations/graph/BottomNavigationGraphDestinations.kt b/infrastructure/navigation/src/androidMain/kotlin/com/velord/infrastructure/navigation/compose/destinations/graph/BottomNavigationGraphDestinations.kt
index 24bef58a..2484e13b 100644
--- a/infrastructure/navigation/src/androidMain/kotlin/com/velord/infrastructure/navigation/compose/destinations/graph/BottomNavigationGraphDestinations.kt
+++ b/infrastructure/navigation/src/androidMain/kotlin/com/velord/infrastructure/navigation/compose/destinations/graph/BottomNavigationGraphDestinations.kt
@@ -9,24 +9,28 @@ import com.velord.infrastructure.navigation.compose.destinations.transition.PopF
import com.velord.ui.feature.bottomnavigation.viewmodel.BottomNavigationUiAction
import com.velord.ui.feature.bottomnavigation.viewmodel.BottomNavigationVM
import com.velord.ui.feature.setting.SettingScreen
+import com.velord.ui.sharedviewmodel.LanguageVM
import com.velord.ui.sharedviewmodel.ThemeVM
import org.koin.compose.viewmodel.koinViewModel
private const val BOTTOM_NAVIGATION_GRAPH = "bottom_navigation_graph"
+
@NavHostGraph(
defaultTransitions = PopFadeTransition::class,
route = BOTTOM_NAVIGATION_GRAPH,
- visibility = CodeGenVisibility.INTERNAL
+ visibility = CodeGenVisibility.INTERNAL,
)
annotation class BottomNavigationGraph
@Destination
@Composable
internal fun BottomNavigationSettingsDestination() {
- val viewModel = koinViewModel()
+ val themeViewModel = koinViewModel()
+ val languageViewModel = koinViewModel()
val bottomNavVM = koinViewModel()
SettingScreen(
- viewModel = viewModel,
+ themeViewModel = themeViewModel,
+ languageViewModel = languageViewModel,
onGraphCompleted = {
bottomNavVM.onAction(BottomNavigationUiAction.GraphCompletedHandling)
},
@@ -39,9 +43,11 @@ internal fun BottomNavigationSettingsDestination() {
@Destination
@Composable
internal fun MainSettingsDestination(navigator: SupremeNavigatorDestinations) {
- val viewModel = koinViewModel()
+ val themeViewModel = koinViewModel()
+ val languageViewModel = koinViewModel()
SettingScreen(
- viewModel = viewModel,
+ themeViewModel = themeViewModel,
+ languageViewModel = languageViewModel,
onGraphCompleted = {},
onBackClick = navigator::goBack,
)
diff --git a/infrastructure/navigation/src/androidMain/kotlin/com/velord/infrastructure/navigation/compose/vanilla/graph/BottomNavigationGraphVanilla.kt b/infrastructure/navigation/src/androidMain/kotlin/com/velord/infrastructure/navigation/compose/vanilla/graph/BottomNavigationGraphVanilla.kt
index 825deb82..4c47dd2b 100644
--- a/infrastructure/navigation/src/androidMain/kotlin/com/velord/infrastructure/navigation/compose/vanilla/graph/BottomNavigationGraphVanilla.kt
+++ b/infrastructure/navigation/src/androidMain/kotlin/com/velord/infrastructure/navigation/compose/vanilla/graph/BottomNavigationGraphVanilla.kt
@@ -7,6 +7,7 @@ import com.velord.infrastructure.navigation.compose.vanilla.navigator.BottomNavi
import com.velord.ui.feature.bottomnavigation.viewmodel.BottomNavigationUiAction
import com.velord.ui.feature.bottomnavigation.viewmodel.BottomNavigationVM
import com.velord.ui.feature.setting.SettingScreen
+import com.velord.ui.sharedviewmodel.LanguageVM
import com.velord.ui.sharedviewmodel.ThemeVM
import org.koin.compose.viewmodel.koinViewModel
@@ -17,16 +18,18 @@ internal fun NavGraphBuilder.setupBottomNavigationGraphVanilla(
setupDemoGraphVanilla(navigator)
composable {
- val viewModel = koinViewModel()
+ val themeViewModel = koinViewModel()
+ val languageViewModel = koinViewModel()
val bottomNavVM = koinViewModel()
SettingScreen(
- viewModel = viewModel,
+ themeViewModel = themeViewModel,
+ languageViewModel = languageViewModel,
onGraphCompleted = {
bottomNavVM.onAction(BottomNavigationUiAction.GraphCompletedHandling)
},
onBackClick = {
bottomNavVM.onAction(BottomNavigationUiAction.BackRequest)
- }
+ },
)
}
}
diff --git a/infrastructure/navigation/src/androidMain/kotlin/com/velord/infrastructure/navigation/compose/vanilla/graph/MainGraphVanilla.kt b/infrastructure/navigation/src/androidMain/kotlin/com/velord/infrastructure/navigation/compose/vanilla/graph/MainGraphVanilla.kt
index 8f9490df..4ee9da4b 100644
--- a/infrastructure/navigation/src/androidMain/kotlin/com/velord/infrastructure/navigation/compose/vanilla/graph/MainGraphVanilla.kt
+++ b/infrastructure/navigation/src/androidMain/kotlin/com/velord/infrastructure/navigation/compose/vanilla/graph/MainGraphVanilla.kt
@@ -6,20 +6,23 @@ import com.velord.infrastructure.navigation.compose.vanilla.GraphVanilla
import com.velord.infrastructure.navigation.compose.vanilla.navigator.SupremeNavigatorVanilla
import com.velord.ui.feature.bottomnavigation.screen.compose.BottomNavigationScreen
import com.velord.ui.feature.setting.SettingScreen
+import com.velord.ui.sharedviewmodel.LanguageVM
import com.velord.ui.sharedviewmodel.ThemeVM
import org.koin.compose.viewmodel.koinViewModel
-internal fun NavGraphBuilder.setupMainGraphVanilla(navigator: SupremeNavigatorVanilla) {
+internal fun NavGraphBuilder.setupMainGraphVanilla(navigator: SupremeNavigatorVanilla) {
composable {
BottomNavigationScreen(navigator = navigator)
}
composable {
- val viewModel = koinViewModel()
+ val themeViewModel = koinViewModel()
+ val languageViewModel = koinViewModel()
SettingScreen(
- viewModel = viewModel,
+ themeViewModel = themeViewModel,
+ languageViewModel = languageViewModel,
onGraphCompleted = {
- // Handle back navigation if needed
+ // Handle back navigation if needed
},
onBackClick = navigator::goBack,
)
diff --git a/infrastructure/navigation/src/androidMain/kotlin/com/velord/infrastructure/navigation/jetpack/inDevelopment/InDevelopmentFragment.kt b/infrastructure/navigation/src/androidMain/kotlin/com/velord/infrastructure/navigation/jetpack/inDevelopment/InDevelopmentFragment.kt
index b092234e..0000f2a5 100644
--- a/infrastructure/navigation/src/androidMain/kotlin/com/velord/infrastructure/navigation/jetpack/inDevelopment/InDevelopmentFragment.kt
+++ b/infrastructure/navigation/src/androidMain/kotlin/com/velord/infrastructure/navigation/jetpack/inDevelopment/InDevelopmentFragment.kt
@@ -18,12 +18,11 @@ import androidx.compose.ui.unit.dp
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import com.velord.core.navigation.fragment.util.activityNavController
-import com.velord.core.resource.Res
-import com.velord.core.resource.open_new
+import com.velord.core.resource.AppString
+import com.velord.core.resource.stringResource
import com.velord.core.ui.util.setContentWithTheme
import com.velord.infrastructure.util.fragment.viewLifecycleScope
import kotlinx.coroutines.launch
-import org.jetbrains.compose.resources.stringResource
/**
* Jetpack diagnostic destination for exercising repeated graph navigation.
@@ -79,7 +78,7 @@ private fun Content(
.align(Alignment.BottomCenter)
.padding(32.dp)
) {
- Text(text = stringResource(Res.string.open_new))
+ Text(text = stringResource(AppString.open_new))
}
}
}
diff --git a/infrastructure/navigation/src/androidMain/kotlin/com/velord/infrastructure/navigation/jetpack/inDevelopment/TestFragment.kt b/infrastructure/navigation/src/androidMain/kotlin/com/velord/infrastructure/navigation/jetpack/inDevelopment/TestFragment.kt
index d39e2a06..715b9289 100644
--- a/infrastructure/navigation/src/androidMain/kotlin/com/velord/infrastructure/navigation/jetpack/inDevelopment/TestFragment.kt
+++ b/infrastructure/navigation/src/androidMain/kotlin/com/velord/infrastructure/navigation/jetpack/inDevelopment/TestFragment.kt
@@ -5,8 +5,7 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
-import com.velord.core.resource.Res
-import com.velord.core.resource.test
+import com.velord.core.resource.AppString
import com.velord.core.ui.util.setContentWithTheme
import com.velord.infrastructure.navigation.TestScreen
@@ -17,6 +16,6 @@ class TestFragment : Fragment() {
container: ViewGroup?,
savedInstanceState: Bundle?
): View = setContentWithTheme {
- TestScreen(Res.string.test)
+ TestScreen(AppString.test)
}
}
diff --git a/infrastructure/navigation/src/commonMain/kotlin/com/velord/infrastructure/navigation/TestScreen.kt b/infrastructure/navigation/src/commonMain/kotlin/com/velord/infrastructure/navigation/TestScreen.kt
index c1fd4d31..a5926b84 100644
--- a/infrastructure/navigation/src/commonMain/kotlin/com/velord/infrastructure/navigation/TestScreen.kt
+++ b/infrastructure/navigation/src/commonMain/kotlin/com/velord/infrastructure/navigation/TestScreen.kt
@@ -10,11 +10,10 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import com.velord.core.resource.Res
-import com.velord.core.resource.settings
+import com.velord.core.resource.AppString
+import com.velord.core.resource.AppStringResource
+import com.velord.core.resource.stringResource
import com.velord.core.ui.compose.preview.PreviewCombined
-import org.jetbrains.compose.resources.StringResource
-import org.jetbrains.compose.resources.stringResource
import kotlin.time.Clock
/**
@@ -22,7 +21,7 @@ import kotlin.time.Clock
*/
@Composable
fun TestScreen(
- title: StringResource,
+ title: AppStringResource,
modifier: Modifier = Modifier,
onClick: () -> Unit = {},
) {
@@ -35,7 +34,7 @@ fun TestScreen(
@Composable
private fun Content(
- title: StringResource,
+ title: AppStringResource,
modifier: Modifier,
onClick: () -> Unit = {},
) {
@@ -68,7 +67,7 @@ private fun Content(
@Composable
private fun Preview() {
Content(
- title = Res.string.settings,
+ title = AppString.settings,
modifier = Modifier.fillMaxSize()
)
}
diff --git a/infrastructure/navigation/src/commonMain/kotlin/com/velord/infrastructure/navigation/compose/nav3/graph/BottomNavigationGraphNav3.kt b/infrastructure/navigation/src/commonMain/kotlin/com/velord/infrastructure/navigation/compose/nav3/graph/BottomNavigationGraphNav3.kt
index 53359176..fe7df7a4 100644
--- a/infrastructure/navigation/src/commonMain/kotlin/com/velord/infrastructure/navigation/compose/nav3/graph/BottomNavigationGraphNav3.kt
+++ b/infrastructure/navigation/src/commonMain/kotlin/com/velord/infrastructure/navigation/compose/nav3/graph/BottomNavigationGraphNav3.kt
@@ -6,6 +6,7 @@ import com.velord.infrastructure.navigation.compose.nav3.navigator.BottomNavigat
import com.velord.ui.feature.bottomnavigation.viewmodel.BottomNavigationUiAction
import com.velord.ui.feature.bottomnavigation.viewmodel.BottomNavigationVM
import com.velord.ui.feature.setting.SettingScreen
+import com.velord.ui.sharedviewmodel.LanguageVM
import com.velord.ui.sharedviewmodel.ThemeVM
import org.koin.compose.viewmodel.koinViewModel
@@ -16,16 +17,18 @@ internal fun EntryProviderScope.setupBottomNavigationGraphNav3(
setupDemoGraphNav3(navigator)
entry {
- val viewModel = koinViewModel()
+ val themeViewModel = koinViewModel()
+ val languageViewModel = koinViewModel()
val bottomNavVM = koinViewModel()
SettingScreen(
- viewModel = viewModel,
+ themeViewModel = themeViewModel,
+ languageViewModel = languageViewModel,
onGraphCompleted = {
bottomNavVM.onAction(BottomNavigationUiAction.GraphCompletedHandling)
},
onBackClick = {
bottomNavVM.onAction(BottomNavigationUiAction.BackRequest)
- }
+ },
)
}
}
diff --git a/infrastructure/navigation/src/commonMain/kotlin/com/velord/infrastructure/navigation/compose/nav3/graph/MainGraphNav3.kt b/infrastructure/navigation/src/commonMain/kotlin/com/velord/infrastructure/navigation/compose/nav3/graph/MainGraphNav3.kt
index f9829222..776b7fbf 100644
--- a/infrastructure/navigation/src/commonMain/kotlin/com/velord/infrastructure/navigation/compose/nav3/graph/MainGraphNav3.kt
+++ b/infrastructure/navigation/src/commonMain/kotlin/com/velord/infrastructure/navigation/compose/nav3/graph/MainGraphNav3.kt
@@ -5,6 +5,7 @@ import com.velord.infrastructure.navigation.compose.nav3.GraphNav3
import com.velord.infrastructure.navigation.compose.nav3.navigator.SupremeNavigatorNav3
import com.velord.ui.feature.bottomnavigation.screen.compose.BottomNavigationScreen
import com.velord.ui.feature.setting.SettingScreen
+import com.velord.ui.sharedviewmodel.LanguageVM
import com.velord.ui.sharedviewmodel.ThemeVM
import org.koin.compose.viewmodel.koinViewModel
@@ -14,9 +15,11 @@ internal fun EntryProviderScope.setupMainGraphNav3(navigator: Supreme
}
entry {
- val viewModel = koinViewModel()
+ val themeViewModel = koinViewModel()
+ val languageViewModel = koinViewModel()
SettingScreen(
- viewModel = viewModel,
+ themeViewModel = themeViewModel,
+ languageViewModel = languageViewModel,
onGraphCompleted = {
// Handle back navigation if needed
},
diff --git a/infrastructure/navigation/src/commonMain/kotlin/com/velord/infrastructure/navigation/compose/nav3/navigator/BottomNavigatorNav3.kt b/infrastructure/navigation/src/commonMain/kotlin/com/velord/infrastructure/navigation/compose/nav3/navigator/BottomNavigatorNav3.kt
index 8e175923..410c9ceb 100644
--- a/infrastructure/navigation/src/commonMain/kotlin/com/velord/infrastructure/navigation/compose/nav3/navigator/BottomNavigatorNav3.kt
+++ b/infrastructure/navigation/src/commonMain/kotlin/com/velord/infrastructure/navigation/compose/nav3/navigator/BottomNavigatorNav3.kt
@@ -18,7 +18,15 @@ internal class BottomNavigatorNav3(
}
override fun goTo(dest: DemoNavigationEvent) {
- val destination = when (dest) {
+ backStackNavigator.navigate(dest.toGraphNav3())
+ }
+
+ override fun goBack() {
+ backStackNavigator.goBack()
+ }
+
+ internal companion object {
+ fun DemoNavigationEvent.toGraphNav3(): GraphNav3.BottomTab.Demo = when (this) {
DemoNavigationEvent.Shape -> GraphNav3.BottomTab.Demo.ShapeDemoDestinationNav3
DemoNavigationEvent.Modifier -> GraphNav3.BottomTab.Demo.ModifierDestinationNav3
DemoNavigationEvent.FlowSummator ->
@@ -29,10 +37,5 @@ internal class BottomNavigatorNav3(
DemoNavigationEvent.Movie -> GraphNav3.BottomTab.Demo.MovieDestinationNav3
DemoNavigationEvent.Dialog -> GraphNav3.BottomTab.Demo.DialogDestinationNav3
}
- backStackNavigator.navigate(destination)
- }
-
- override fun goBack() {
- backStackNavigator.goBack()
}
}
diff --git a/infrastructure/navigation/src/commonMain/kotlin/com/velord/infrastructure/navigation/voyager/screen/SettingsVoyagerScreen.kt b/infrastructure/navigation/src/commonMain/kotlin/com/velord/infrastructure/navigation/voyager/screen/SettingsVoyagerScreen.kt
index 0afa745e..59ccc683 100644
--- a/infrastructure/navigation/src/commonMain/kotlin/com/velord/infrastructure/navigation/voyager/screen/SettingsVoyagerScreen.kt
+++ b/infrastructure/navigation/src/commonMain/kotlin/com/velord/infrastructure/navigation/voyager/screen/SettingsVoyagerScreen.kt
@@ -5,6 +5,7 @@ import cafe.adriel.voyager.core.screen.Screen
import com.velord.ui.feature.bottomnavigation.viewmodel.BottomNavigationUiAction
import com.velord.ui.feature.bottomnavigation.viewmodel.BottomNavigationVM
import com.velord.ui.feature.setting.SettingScreen
+import com.velord.ui.sharedviewmodel.LanguageVM
import com.velord.ui.sharedviewmodel.ThemeVM
import org.koin.compose.viewmodel.koinViewModel
@@ -12,16 +13,18 @@ internal object SettingsVoyagerScreen : Screen {
@Composable
override fun Content() {
- val viewModel = koinViewModel()
+ val themeViewModel = koinViewModel()
+ val languageViewModel = koinViewModel()
val bottomNavVM = koinViewModel()
SettingScreen(
- viewModel = viewModel,
+ themeViewModel = themeViewModel,
+ languageViewModel = languageViewModel,
onGraphCompleted = {
bottomNavVM.onAction(BottomNavigationUiAction.GraphCompletedHandling)
},
onBackClick = {
bottomNavVM.onAction(BottomNavigationUiAction.BackRequest)
- }
+ },
)
}
}
diff --git a/infrastructure/navigation/src/commonMain/kotlin/com/velord/infrastructure/navigation/voyager/screen/TestVoyagerScreen.kt b/infrastructure/navigation/src/commonMain/kotlin/com/velord/infrastructure/navigation/voyager/screen/TestVoyagerScreen.kt
index 3075fd05..d08562f2 100644
--- a/infrastructure/navigation/src/commonMain/kotlin/com/velord/infrastructure/navigation/voyager/screen/TestVoyagerScreen.kt
+++ b/infrastructure/navigation/src/commonMain/kotlin/com/velord/infrastructure/navigation/voyager/screen/TestVoyagerScreen.kt
@@ -3,14 +3,14 @@ package com.velord.infrastructure.navigation.voyager.screen
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import cafe.adriel.voyager.core.screen.Screen
+import com.velord.core.resource.AppStringResource
import com.velord.infrastructure.navigation.TestScreen
-import org.jetbrains.compose.resources.StringResource
/**
* Voyager adapter for the shared navigation diagnostic surface.
*/
internal data class TestVoyagerScreen(
- val title: StringResource,
+ val title: AppStringResource,
val modifier: Modifier = Modifier,
val onClick: () -> Unit = {},
) : Screen {
diff --git a/model/src/androidMain/kotlin/com/velord/model/setting/ThemeCapabilities.kt b/model/src/androidMain/kotlin/com/velord/model/setting/ThemeCapability.kt
similarity index 66%
rename from model/src/androidMain/kotlin/com/velord/model/setting/ThemeCapabilities.kt
rename to model/src/androidMain/kotlin/com/velord/model/setting/ThemeCapability.kt
index 17665064..150a6efa 100644
--- a/model/src/androidMain/kotlin/com/velord/model/setting/ThemeCapabilities.kt
+++ b/model/src/androidMain/kotlin/com/velord/model/setting/ThemeCapability.kt
@@ -2,7 +2,7 @@ package com.velord.model.setting
import android.os.Build
-internal actual val currentThemeCapabilities: ThemeCapabilities = ThemeCapabilities(
+internal actual val currentThemeCapability: ThemeCapability = ThemeCapability(
isSystemOsSwitchAvailable = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1,
- isSystemDynamicColorAvailable = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
-)
\ No newline at end of file
+ isSystemDynamicColorAvailable = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S,
+)
diff --git a/model/src/commonMain/kotlin/com/velord/model/localization/LanguageCode.kt b/model/src/commonMain/kotlin/com/velord/model/localization/LanguageCode.kt
new file mode 100644
index 00000000..4538c360
--- /dev/null
+++ b/model/src/commonMain/kotlin/com/velord/model/localization/LanguageCode.kt
@@ -0,0 +1,12 @@
+package com.velord.model.localization
+
+import kotlinx.serialization.Serializable
+
+@JvmInline
+@Serializable
+value class LanguageCode(val value: String) {
+
+ init {
+ require(value.isNotBlank()) { "Language code must not be blank" }
+ }
+}
diff --git a/model/src/commonMain/kotlin/com/velord/model/localization/LocalizationDocument.kt b/model/src/commonMain/kotlin/com/velord/model/localization/LocalizationDocument.kt
new file mode 100644
index 00000000..cf01c159
--- /dev/null
+++ b/model/src/commonMain/kotlin/com/velord/model/localization/LocalizationDocument.kt
@@ -0,0 +1,51 @@
+package com.velord.model.localization
+
+import com.velord.model.setting.LanguagePreference
+
+@JvmInline
+value class LocalizationStringMap(val value: Map) {
+
+ val keys: Set get() = value.keys
+
+ operator fun get(key: String): String? = value[key]
+}
+
+data class LocalizationDocument(
+ val schemaVersion: Int,
+ val defaultLanguage: LanguageCode,
+ val languageRoster: Map,
+) {
+
+ fun resolveLanguage(
+ preference: LanguagePreference,
+ currentDeviceLanguage: LanguageCode,
+ ): LanguageCode {
+ val requestedLanguage = if (preference.isDefault) {
+ currentDeviceLanguage
+ } else {
+ preference.languageCode
+ }
+ val requestedTag = requestedLanguage.value
+ val baseTag = requestedTag
+ .substringBefore('-')
+ .substringBefore('_')
+ return findLanguage(requestedTag)
+ ?: findLanguage(baseTag)
+ ?: defaultLanguage
+ }
+
+ fun findLanguage(languageTag: String): LanguageCode? = languageRoster
+ .keys
+ .firstOrNull { language -> language.value.equals(languageTag, ignoreCase = true) }
+
+ companion object {
+ val DEFAULT = LocalizationDocument(
+ schemaVersion = 1,
+ defaultLanguage = LanguagePreference.ENGLISH.languageCode,
+ languageRoster = mapOf(
+ LanguagePreference.ENGLISH.languageCode to LocalizationStringMap(emptyMap()),
+ ),
+ )
+ }
+}
+
diff --git a/model/src/commonMain/kotlin/com/velord/model/localization/LocalizationState.kt b/model/src/commonMain/kotlin/com/velord/model/localization/LocalizationState.kt
new file mode 100644
index 00000000..65079835
--- /dev/null
+++ b/model/src/commonMain/kotlin/com/velord/model/localization/LocalizationState.kt
@@ -0,0 +1,17 @@
+package com.velord.model.localization
+
+import com.velord.model.setting.LanguagePreference
+
+data class LocalizationState(
+ val document: LocalizationDocument,
+ val language: LanguageCode,
+) {
+ companion object {
+ val DEFAULT = LocalizationState(
+ document = LocalizationDocument.DEFAULT,
+ language = LanguagePreference.ENGLISH.languageCode,
+ )
+ }
+}
+
+
diff --git a/model/src/commonMain/kotlin/com/velord/model/setting/AppSetting.kt b/model/src/commonMain/kotlin/com/velord/model/setting/AppSetting.kt
index 21b55535..7e0205b2 100644
--- a/model/src/commonMain/kotlin/com/velord/model/setting/AppSetting.kt
+++ b/model/src/commonMain/kotlin/com/velord/model/setting/AppSetting.kt
@@ -7,13 +7,15 @@ import kotlinx.serialization.Serializable
data class AppSetting(
val isAppFirstLaunch: Boolean,
val theme: ThemeConfig,
- val movieFilters: List = MovieFilterOption.ALL
+ val movieFilters: List = MovieFilterOption.ALL,
+ val language: LanguagePreference = LanguagePreference.DEFAULT,
) {
companion object {
val DEFAULT = AppSetting(
isAppFirstLaunch = false,
theme = ThemeConfig.DEFAULT,
- movieFilters = MovieFilterOption.ALL
+ movieFilters = MovieFilterOption.ALL,
+ language = LanguagePreference.DEFAULT,
)
}
}
diff --git a/model/src/commonMain/kotlin/com/velord/model/setting/AppThemeConfig.kt b/model/src/commonMain/kotlin/com/velord/model/setting/AppThemeConfig.kt
index 2c412bb2..44d8de1c 100644
--- a/model/src/commonMain/kotlin/com/velord/model/setting/AppThemeConfig.kt
+++ b/model/src/commonMain/kotlin/com/velord/model/setting/AppThemeConfig.kt
@@ -6,15 +6,15 @@ import kotlinx.serialization.Serializable
data class AppThemeConfig(
val config: ThemeConfig,
val isSystemOsSwitchAvailable: Boolean,
- val isSystemDynamicColorAvailable: Boolean
+ val isSystemDynamicColorAvailable: Boolean,
) {
companion object {
operator fun invoke(config: ThemeConfig): AppThemeConfig = AppThemeConfig(
config = config,
- isSystemOsSwitchAvailable = currentThemeCapabilities.isSystemOsSwitchAvailable,
- isSystemDynamicColorAvailable = currentThemeCapabilities.isSystemDynamicColorAvailable
+ isSystemOsSwitchAvailable = currentThemeCapability.isSystemOsSwitchAvailable,
+ isSystemDynamicColorAvailable = currentThemeCapability.isSystemDynamicColorAvailable,
)
val DEFAULT = AppThemeConfig(ThemeConfig.DEFAULT)
}
-}
\ No newline at end of file
+}
diff --git a/model/src/commonMain/kotlin/com/velord/model/setting/LanguagePreference.kt b/model/src/commonMain/kotlin/com/velord/model/setting/LanguagePreference.kt
new file mode 100644
index 00000000..102c9d6c
--- /dev/null
+++ b/model/src/commonMain/kotlin/com/velord/model/setting/LanguagePreference.kt
@@ -0,0 +1,20 @@
+package com.velord.model.setting
+
+import com.velord.model.localization.LanguageCode
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+@Serializable
+enum class LanguagePreference(val languageCode: LanguageCode) {
+
+ @SerialName("default")
+ DEFAULT(LanguageCode("default")),
+
+ @SerialName("en")
+ ENGLISH(LanguageCode("en")),
+
+ @SerialName("es")
+ SPANISH(LanguageCode("es"));
+
+ val isDefault: Boolean get() = this == DEFAULT
+}
diff --git a/model/src/commonMain/kotlin/com/velord/model/setting/ThemeCapabilities.kt b/model/src/commonMain/kotlin/com/velord/model/setting/ThemeCapabilities.kt
deleted file mode 100644
index f5c7df31..00000000
--- a/model/src/commonMain/kotlin/com/velord/model/setting/ThemeCapabilities.kt
+++ /dev/null
@@ -1,11 +0,0 @@
-package com.velord.model.setting
-
-import kotlinx.serialization.Serializable
-
-@Serializable
-internal data class ThemeCapabilities(
- val isSystemOsSwitchAvailable: Boolean,
- val isSystemDynamicColorAvailable: Boolean
-)
-
-internal expect val currentThemeCapabilities: ThemeCapabilities
\ No newline at end of file
diff --git a/model/src/commonMain/kotlin/com/velord/model/setting/ThemeCapability.kt b/model/src/commonMain/kotlin/com/velord/model/setting/ThemeCapability.kt
new file mode 100644
index 00000000..f71ac8db
--- /dev/null
+++ b/model/src/commonMain/kotlin/com/velord/model/setting/ThemeCapability.kt
@@ -0,0 +1,11 @@
+package com.velord.model.setting
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+internal data class ThemeCapability(
+ val isSystemOsSwitchAvailable: Boolean,
+ val isSystemDynamicColorAvailable: Boolean,
+)
+
+internal expect val currentThemeCapability: ThemeCapability
diff --git a/model/src/desktopMain/kotlin/com/velord/model/setting/ThemeCapabilities.kt b/model/src/desktopMain/kotlin/com/velord/model/setting/ThemeCapabilities.kt
deleted file mode 100644
index 7f592316..00000000
--- a/model/src/desktopMain/kotlin/com/velord/model/setting/ThemeCapabilities.kt
+++ /dev/null
@@ -1,6 +0,0 @@
-package com.velord.model.setting
-
-internal actual val currentThemeCapabilities: ThemeCapabilities = ThemeCapabilities(
- isSystemOsSwitchAvailable = true,
- isSystemDynamicColorAvailable = false
-)
\ No newline at end of file
diff --git a/model/src/desktopMain/kotlin/com/velord/model/setting/ThemeCapability.kt b/model/src/desktopMain/kotlin/com/velord/model/setting/ThemeCapability.kt
new file mode 100644
index 00000000..acc1b78d
--- /dev/null
+++ b/model/src/desktopMain/kotlin/com/velord/model/setting/ThemeCapability.kt
@@ -0,0 +1,6 @@
+package com.velord.model.setting
+
+internal actual val currentThemeCapability: ThemeCapability = ThemeCapability(
+ isSystemOsSwitchAvailable = true,
+ isSystemDynamicColorAvailable = false,
+)
diff --git a/scripts/firebase/publish-localization.ps1 b/scripts/firebase/publish-localization.ps1
new file mode 100644
index 00000000..cc60de00
--- /dev/null
+++ b/scripts/firebase/publish-localization.ps1
@@ -0,0 +1,219 @@
+[CmdletBinding()]
+param(
+ [string]$ProjectId = $env:FIREBASE_PROJECT_ID,
+ [string]$LocalizationPath = "core/core-resource/src/commonMain/composeResources/files/localization.json",
+ [switch]$ValidateOnly,
+ [switch]$Publish
+)
+
+$ErrorActionPreference = "Stop"
+
+function Write-Utf8WithoutBom {
+ param(
+ [Parameter(Mandatory = $true)]
+ [string]$Path,
+ [Parameter(Mandatory = $true)]
+ [string]$Content
+ )
+
+ $encoding = New-Object System.Text.UTF8Encoding($false)
+ [System.IO.File]::WriteAllText($Path, $Content, $encoding)
+}
+
+function Get-Placeholders {
+ param([string]$Value)
+
+ if ($null -eq $Value) {
+ return @()
+ }
+
+ return @(
+ [regex]::Matches($Value, '%\d+\$[sd]') |
+ ForEach-Object { $_.Value } |
+ Sort-Object
+ )
+}
+
+function Assert-LocalizationDocument {
+ param([Parameter(Mandatory = $true)]$Document)
+
+ if ($Document.schemaVersion -ne 1) {
+ throw "localization.json must use schemaVersion 1."
+ }
+
+ if ($null -eq $Document.languageRoster) {
+ throw "localization.json must contain languageRoster."
+ }
+
+ $languageProperties = @($Document.languageRoster.PSObject.Properties)
+ if ($languageProperties.Count -eq 0) {
+ throw "localization.json must contain at least one language."
+ }
+
+ $defaultLanguage = [string]$Document.defaultLanguage
+ if ([string]::IsNullOrWhiteSpace($defaultLanguage)) {
+ throw "localization.json must contain defaultLanguage."
+ }
+
+ $defaultLanguageProperty = $Document.languageRoster.PSObject.Properties[$defaultLanguage]
+ if ($null -eq $defaultLanguageProperty) {
+ throw "localization.json must contain default language '$defaultLanguage'."
+ }
+
+ $defaultStrings = $defaultLanguageProperty.Value
+ $defaultKeys = @($defaultStrings.PSObject.Properties.Name | Sort-Object)
+ if ($defaultKeys.Count -eq 0) {
+ throw "localization.json must contain strings."
+ }
+
+ foreach ($languageProperty in $languageProperties) {
+ $language = $languageProperty.Name
+ $strings = $languageProperty.Value
+ $languageKeys = @($strings.PSObject.Properties.Name | Sort-Object)
+ $keyDifference = @(Compare-Object -ReferenceObject $defaultKeys -DifferenceObject $languageKeys)
+
+ if ($keyDifference.Count -ne 0) {
+ $details = ($keyDifference | ForEach-Object { "{0} ({1})" -f $_.InputObject, $_.SideIndicator }) -join ", "
+ throw "Localization keys for '$language' do not match '$defaultLanguage': $details"
+ }
+
+ foreach ($key in $defaultKeys) {
+ $defaultValue = [string]$defaultStrings.$key
+ $translatedValue = [string]$strings.$key
+
+ if ([string]::IsNullOrEmpty($defaultValue) -or [string]::IsNullOrEmpty($translatedValue)) {
+ throw "Localization value must not be empty for '$language/$key'."
+ }
+
+ $defaultPlaceholders = @(Get-Placeholders $defaultValue)
+ $translatedPlaceholders = @(Get-Placeholders $translatedValue)
+ $placeholderDifference = @(
+ Compare-Object -ReferenceObject $defaultPlaceholders -DifferenceObject $translatedPlaceholders
+ )
+
+ if ($placeholderDifference.Count -ne 0) {
+ throw "Placeholder mismatch for '$language/$key'. Default=[$($defaultPlaceholders -join ', ')] Translation=[$($translatedPlaceholders -join ', ')]"
+ }
+ }
+ }
+
+ $languageRoster = @($languageProperties.Name | Sort-Object)
+ Write-Host "Localization validation passed: $($defaultKeys.Count) keys in $($languageRoster.Count) languages [$($languageRoster -join ', ')], default '$defaultLanguage'."
+}
+
+$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..\..")).Path
+$resolvedLocalizationPath = if ([System.IO.Path]::IsPathRooted($LocalizationPath)) {
+ $LocalizationPath
+} else {
+ Join-Path $repoRoot $LocalizationPath
+}
+
+if (-not (Test-Path $resolvedLocalizationPath)) {
+ throw "Localization file not found: $resolvedLocalizationPath"
+}
+
+$localizationRaw = Get-Content -Raw -Path $resolvedLocalizationPath
+try {
+ $localization = $localizationRaw | ConvertFrom-Json
+} catch {
+ throw "localization.json is not valid JSON: $($_.Exception.Message)"
+}
+
+Assert-LocalizationDocument $localization
+
+if ($ValidateOnly) {
+ Write-Host "Validation-only run complete. Firebase was not contacted."
+ return
+}
+
+if ([string]::IsNullOrWhiteSpace($ProjectId)) {
+ throw "Firebase project ID is missing. Pass -ProjectId or set FIREBASE_PROJECT_ID."
+}
+
+$firebase = Get-Command firebase -ErrorAction SilentlyContinue
+if ($null -eq $firebase) {
+ throw "Firebase CLI was not found. Install firebase-tools and run 'firebase login' once before using this script."
+}
+
+$compactLocalization = $localization | ConvertTo-Json -Depth 100 -Compress
+
+$tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ("compose-screen-example-remote-config-" + [Guid]::NewGuid().ToString("N"))
+New-Item -ItemType Directory -Path $tempDir | Out-Null
+
+$currentTemplatePath = Join-Path $tempDir "current.remoteconfig.template.json"
+$patchedTemplatePath = Join-Path $tempDir "remoteconfig.template.json"
+$firebaseConfigPath = Join-Path $tempDir "firebase.json"
+
+$keepTemp = -not $Publish
+
+try {
+ Write-Host "Fetching current Remote Config template from Firebase project '$ProjectId'..."
+ & firebase remoteconfig:get --project $ProjectId -o $currentTemplatePath
+ if ($LASTEXITCODE -ne 0) {
+ throw "Firebase CLI failed to download the current Remote Config template."
+ }
+
+ $templateRaw = Get-Content -Raw -Path $currentTemplatePath
+ $template = $templateRaw | ConvertFrom-Json
+
+ if ($null -eq $template.parameters) {
+ $template | Add-Member -NotePropertyName parameters -NotePropertyValue ([pscustomobject]@{})
+ }
+
+ $localizationParameter = $template.parameters.localization
+ if ($null -eq $localizationParameter) {
+ $localizationParameter = [pscustomobject]@{}
+ $template.parameters | Add-Member -NotePropertyName localization -NotePropertyValue $localizationParameter
+ }
+
+ $localizationParameter | Add-Member -NotePropertyName defaultValue -NotePropertyValue ([pscustomobject]@{
+ value = $compactLocalization
+ }) -Force
+ $localizationParameter | Add-Member -NotePropertyName valueType -NotePropertyValue "JSON" -Force
+
+ if ($null -eq $localizationParameter.description -or [string]::IsNullOrWhiteSpace([string]$localizationParameter.description)) {
+ $localizationParameter | Add-Member -NotePropertyName description -NotePropertyValue "ComposeScreenExample localization document" -Force
+ }
+
+ if ($null -ne $template.PSObject.Properties["etag"]) {
+ $template.PSObject.Properties.Remove("etag")
+ }
+ if ($null -ne $template.PSObject.Properties["version"]) {
+ $template.PSObject.Properties.Remove("version")
+ }
+
+ $patchedTemplateJson = $template | ConvertTo-Json -Depth 100
+ Write-Utf8WithoutBom -Path $patchedTemplatePath -Content $patchedTemplateJson
+
+ $firebaseConfig = [ordered]@{
+ remoteconfig = [ordered]@{
+ template = "remoteconfig.template.json"
+ }
+ } | ConvertTo-Json -Depth 10
+ Write-Utf8WithoutBom -Path $firebaseConfigPath -Content $firebaseConfig
+
+ if (-not $Publish) {
+ Write-Host "Dry run complete. Nothing was published."
+ Write-Host "Generated template: $patchedTemplatePath"
+ Write-Host "Run again with -Publish when ready:"
+ Write-Host ".\scripts\firebase\publish-localization.ps1 -Publish -ProjectId "
+ return
+ }
+
+ Write-Host "Publishing patched Remote Config template..."
+ Push-Location $tempDir
+ try {
+ & firebase deploy --only remoteconfig --project $ProjectId --config firebase.json --non-interactive
+ if ($LASTEXITCODE -ne 0) {
+ throw "Firebase CLI failed to publish Remote Config."
+ }
+ } finally {
+ Pop-Location
+ }
+
+ Write-Host "Published Remote Config parameter 'localization' from: $resolvedLocalizationPath"
+} finally {
+ if (-not $keepTemp -and (Test-Path $tempDir)) {
+ Remove-Item -Recurse -Force $tempDir
+ }
+}
diff --git a/settings.gradle.kts b/settings.gradle.kts
index d1a55df3..0989b6a8 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -73,6 +73,7 @@ include(":data:backend")
include(":data:datastore")
include(":data:appstate")
include(":data:db")
+include(":data:localization")
include(":data:gateway")
include(":data:os")
// Domain
diff --git a/ui/feature-bottomnavigation/build.gradle.kts b/ui/feature-bottomnavigation/build.gradle.kts
index 5fe30113..39712069 100644
--- a/ui/feature-bottomnavigation/build.gradle.kts
+++ b/ui/feature-bottomnavigation/build.gradle.kts
@@ -20,6 +20,7 @@ kotlin {
implementation(projects.core.coreResource)
implementation(projects.core.coreUi)
implementation(projects.domain.usecaseEvent)
+ implementation(projects.domain.usecaseSetting)
implementation(projects.ui.sharedviewmodel)
// Core
implementation(libs.kotlin.coroutine.core)
diff --git a/ui/feature-bottomnavigation/src/androidMain/kotlin/com/velord/ui/feature/bottomnavigation/screen/BottomNavigationJetpackScreen.kt b/ui/feature-bottomnavigation/src/androidMain/kotlin/com/velord/ui/feature/bottomnavigation/screen/BottomNavigationJetpackScreen.kt
index 27e9f65f..bc481fae 100644
--- a/ui/feature-bottomnavigation/src/androidMain/kotlin/com/velord/ui/feature/bottomnavigation/screen/BottomNavigationJetpackScreen.kt
+++ b/ui/feature-bottomnavigation/src/androidMain/kotlin/com/velord/ui/feature/bottomnavigation/screen/BottomNavigationJetpackScreen.kt
@@ -8,15 +8,14 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import co.touchlab.kermit.Logger
-import com.velord.core.resource.Res
-import com.velord.core.resource.press_again_to_exit
+import com.velord.core.resource.AppString
+import com.velord.core.resource.stringResource
import com.velord.core.ui.compose.preview.PreviewCombined
import com.velord.multiplebackstackapplier.utils.compose.SnackBarOnBackPressHandler
import com.velord.ui.feature.bottomnavigation.navigation.BottomNavigationItem
import com.velord.ui.feature.bottomnavigation.screen.component.BottomBar
import com.velord.ui.feature.bottomnavigation.viewmodel.BottomNavigationJetpackUiAction
import com.velord.ui.feature.bottomnavigation.viewmodel.BottomNavigationJetpackVM
-import org.jetbrains.compose.resources.stringResource
private val log = Logger.withTag(TAG)
@@ -34,7 +33,7 @@ internal fun BottomNavigationJetpackScreen(viewModel: BottomNavigationJetpackVM)
log.d { "BottomNavScreen: ${uiState.value.backHandlingState}" }
if (uiState.value.backHandlingState.isEnabled) {
- val str = stringResource(Res.string.press_again_to_exit)
+ val str = stringResource(AppString.press_again_to_exit)
SnackBarOnBackPressHandler(
message = str,
modifier = Modifier.padding(horizontal = 8.dp),
diff --git a/ui/feature-bottomnavigation/src/androidMain/kotlin/com/velord/ui/feature/bottomnavigation/viewmodel/BottomNavigationJetpackVM.kt b/ui/feature-bottomnavigation/src/androidMain/kotlin/com/velord/ui/feature/bottomnavigation/viewmodel/BottomNavigationJetpackVM.kt
index e8ae690e..4dbb3bdb 100644
--- a/ui/feature-bottomnavigation/src/androidMain/kotlin/com/velord/ui/feature/bottomnavigation/viewmodel/BottomNavigationJetpackVM.kt
+++ b/ui/feature-bottomnavigation/src/androidMain/kotlin/com/velord/ui/feature/bottomnavigation/viewmodel/BottomNavigationJetpackVM.kt
@@ -1,29 +1,27 @@
package com.velord.ui.feature.bottomnavigation.viewmodel
import androidx.navigation.NavDestination
+import com.velord.core.resource.AppString
import com.velord.core.resource.R
-import com.velord.core.resource.Res
-import com.velord.core.resource.bottom_navigation_first_back_press
import com.velord.model.ToastConfig
import com.velord.model.ToastDuration
import com.velord.ui.feature.bottomnavigation.navigation.BottomNavBackHandlingState
import com.velord.ui.feature.bottomnavigation.navigation.BottomNavEventService
import com.velord.ui.feature.bottomnavigation.navigation.BottomNavigationItem
import com.velord.ui.feature.bottomnavigation.navigation.TabState
-import com.velord.ui.sharedviewmodel.CoroutineScopeVM
+import com.velord.ui.sharedviewmodel.LocalizationVM
import com.velord.usecase.event.RequestAppExitUC
import com.velord.usecase.event.ShowToastUC
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
-import org.jetbrains.compose.resources.getString
class BottomNavigationJetpackVM(
private val bottomNavEventService: BottomNavEventService,
private val requestAppExitUC: RequestAppExitUC,
private val showToastUC: ShowToastUC,
-) : CoroutineScopeVM() {
+) : LocalizationVM() {
val uiStateFlow = MutableStateFlow(
BottomNavigationJetpackUiState(
@@ -61,7 +59,7 @@ class BottomNavigationJetpackVM(
private fun onBackDoubleClick() = launch { requestAppExitUC() }
private fun onShowBackPressToast(tag: String) = launch {
- val message = getString(Res.string.bottom_navigation_first_back_press, tag)
+ val message = getString(AppString.bottom_navigation_first_back_press, tag)
val toastConfig = ToastConfig(message = message, duration = ToastDuration.Short)
showToastUC(toastConfig)
}
@@ -99,14 +97,11 @@ class BottomNavigationJetpackVM(
when (action) {
is BottomNavigationJetpackUiAction.TabClick -> onTabClick(action.newTab)
is BottomNavigationJetpackUiAction.BackDoubleClick -> onBackDoubleClick()
- is BottomNavigationJetpackUiAction.ShowBackPressToast ->
- onShowBackPressToast(action.tag)
+ is BottomNavigationJetpackUiAction.ShowBackPressToast -> onShowBackPressToast(action.tag)
is BottomNavigationJetpackUiAction.UpdateBackHandling ->
onUpdateBackHandling(action.currentNavigationDestination)
- is BottomNavigationJetpackUiAction.GraphCompletedHandling ->
- onGraphCompletedHandling()
- is BottomNavigationJetpackUiAction.GraphTakeResponsibility ->
- onGraphTakeResponsibility()
+ is BottomNavigationJetpackUiAction.GraphCompletedHandling -> onGraphCompletedHandling()
+ is BottomNavigationJetpackUiAction.GraphTakeResponsibility -> onGraphTakeResponsibility()
}
}
diff --git a/ui/feature-bottomnavigation/src/commonMain/kotlin/com/velord/ui/feature/bottomnavigation/navigation/BottomNavigationItem.kt b/ui/feature-bottomnavigation/src/commonMain/kotlin/com/velord/ui/feature/bottomnavigation/navigation/BottomNavigationItem.kt
index e1c3798e..9dc6b4fe 100644
--- a/ui/feature-bottomnavigation/src/commonMain/kotlin/com/velord/ui/feature/bottomnavigation/navigation/BottomNavigationItem.kt
+++ b/ui/feature-bottomnavigation/src/commonMain/kotlin/com/velord/ui/feature/bottomnavigation/navigation/BottomNavigationItem.kt
@@ -5,11 +5,8 @@ import androidx.compose.material.icons.outlined.Camera
import androidx.compose.material.icons.outlined.Hexagon
import androidx.compose.material.icons.outlined.Settings
import androidx.compose.ui.graphics.vector.ImageVector
-import com.velord.core.resource.Res
-import com.velord.core.resource.camera
-import com.velord.core.resource.demo
-import com.velord.core.resource.settings
-import org.jetbrains.compose.resources.StringResource
+import com.velord.core.resource.AppString
+import com.velord.core.resource.AppStringResource
enum class BottomNavigationItem {
Camera,
@@ -26,9 +23,9 @@ enum class BottomNavigationItem {
val isDemo: Boolean get() = this == Demo
val isSetting: Boolean get() = this == Setting
- val label: StringResource get() = when (this) {
- Camera -> Res.string.camera
- Demo -> Res.string.demo
- Setting -> Res.string.settings
+ val label: AppStringResource get() = when (this) {
+ Camera -> AppString.camera
+ Demo -> AppString.demo
+ Setting -> AppString.settings
}
}
diff --git a/ui/feature-bottomnavigation/src/commonMain/kotlin/com/velord/ui/feature/bottomnavigation/screen/component/Content.kt b/ui/feature-bottomnavigation/src/commonMain/kotlin/com/velord/ui/feature/bottomnavigation/screen/component/Content.kt
index 25045a79..9f2c0558 100644
--- a/ui/feature-bottomnavigation/src/commonMain/kotlin/com/velord/ui/feature/bottomnavigation/screen/component/Content.kt
+++ b/ui/feature-bottomnavigation/src/commonMain/kotlin/com/velord/ui/feature/bottomnavigation/screen/component/Content.kt
@@ -14,10 +14,10 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.unit.dp
+import com.velord.core.resource.stringResource
import com.velord.core.ui.compose.component.AnimatableLabeledIcon
import com.velord.core.ui.compose.preview.PreviewCombined
import com.velord.ui.feature.bottomnavigation.navigation.BottomNavigationItem
-import org.jetbrains.compose.resources.stringResource
@Composable
internal fun Content(
diff --git a/ui/feature-bottomnavigation/src/commonMain/kotlin/com/velord/ui/feature/bottomnavigation/screen/component/ScreenSetup.kt b/ui/feature-bottomnavigation/src/commonMain/kotlin/com/velord/ui/feature/bottomnavigation/screen/component/ScreenSetup.kt
index 6b9e0209..e0d81a81 100644
--- a/ui/feature-bottomnavigation/src/commonMain/kotlin/com/velord/ui/feature/bottomnavigation/screen/component/ScreenSetup.kt
+++ b/ui/feature-bottomnavigation/src/commonMain/kotlin/com/velord/ui/feature/bottomnavigation/screen/component/ScreenSetup.kt
@@ -8,10 +8,9 @@ import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import co.touchlab.kermit.Logger
-import com.velord.core.resource.Res
-import com.velord.core.resource.press_again_to_exit
+import com.velord.core.resource.AppString
+import com.velord.core.resource.stringResource
import com.velord.ui.feature.bottomnavigation.viewmodel.BottomNavigationBackBehavior
-import org.jetbrains.compose.resources.stringResource
private val log = Logger.withTag("LogBackStack")
@@ -37,7 +36,7 @@ internal fun ScreenSetup(
log.d { "ScreenSetup: BackHandler Registered. Behavior=$backBehavior" }
- val str = stringResource(Res.string.press_again_to_exit)
+ val str = stringResource(AppString.press_again_to_exit)
PlatformBackHandler(
backBehavior = backBehavior,
message = str,
diff --git a/ui/feature-bottomnavigation/src/commonMain/kotlin/com/velord/ui/feature/bottomnavigation/screen/voyager/VoyagerBottomNavigationTab.kt b/ui/feature-bottomnavigation/src/commonMain/kotlin/com/velord/ui/feature/bottomnavigation/screen/voyager/VoyagerBottomNavigationTab.kt
index fcb53261..393f618d 100644
--- a/ui/feature-bottomnavigation/src/commonMain/kotlin/com/velord/ui/feature/bottomnavigation/screen/voyager/VoyagerBottomNavigationTab.kt
+++ b/ui/feature-bottomnavigation/src/commonMain/kotlin/com/velord/ui/feature/bottomnavigation/screen/voyager/VoyagerBottomNavigationTab.kt
@@ -17,11 +17,8 @@ import cafe.adriel.voyager.navigator.tab.Tab
import cafe.adriel.voyager.navigator.tab.TabOptions
import cafe.adriel.voyager.transitions.SlideTransition
import com.velord.core.navigation.voyager.SharedScreenVoyager
-import com.velord.core.resource.Res
-import com.velord.core.resource.camera
-import com.velord.core.resource.demo
-import com.velord.core.resource.settings
-import org.jetbrains.compose.resources.stringResource
+import com.velord.core.resource.AppString
+import com.velord.core.resource.stringResource
/*
Each tab owns a nested Voyager Navigator. The bottom-navigation parent provides this callback so
@@ -41,10 +38,10 @@ sealed class VoyagerBottomNavigationTab : Tab {
override val options: TabOptions
@Composable
get() {
- val title = stringResource(Res.string.camera)
+ val title = stringResource(AppString.camera)
val icon = rememberVectorPainter(image = Icons.Outlined.Camera)
- return remember {
+ return remember(title) {
TabOptions(
index = 0u,
title = title,
@@ -71,10 +68,10 @@ sealed class VoyagerBottomNavigationTab : Tab {
override val options: TabOptions
@Composable
get() {
- val title = stringResource(Res.string.demo)
+ val title = stringResource(AppString.demo)
val icon = rememberVectorPainter(image = Icons.Outlined.Hexagon)
- return remember {
+ return remember(title) {
TabOptions(
index = 1u,
title = title,
@@ -101,10 +98,10 @@ sealed class VoyagerBottomNavigationTab : Tab {
override val options: TabOptions
@Composable
get() {
- val title = stringResource(Res.string.settings)
+ val title = stringResource(AppString.settings)
val icon = rememberVectorPainter(image = Icons.Outlined.Settings)
- return remember {
+ return remember(title) {
TabOptions(
index = 2u,
title = title,
diff --git a/ui/feature-bottomnavigation/src/commonMain/kotlin/com/velord/ui/feature/bottomnavigation/viewmodel/BottomNavigationVM.kt b/ui/feature-bottomnavigation/src/commonMain/kotlin/com/velord/ui/feature/bottomnavigation/viewmodel/BottomNavigationVM.kt
index 19be9611..9dc0daae 100644
--- a/ui/feature-bottomnavigation/src/commonMain/kotlin/com/velord/ui/feature/bottomnavigation/viewmodel/BottomNavigationVM.kt
+++ b/ui/feature-bottomnavigation/src/commonMain/kotlin/com/velord/ui/feature/bottomnavigation/viewmodel/BottomNavigationVM.kt
@@ -1,14 +1,13 @@
package com.velord.ui.feature.bottomnavigation.viewmodel
-import com.velord.core.resource.Res
-import com.velord.core.resource.press_again_to_exit
+import com.velord.core.resource.AppString
import com.velord.model.ToastConfig
import com.velord.model.ToastDuration
import com.velord.ui.feature.bottomnavigation.navigation.BottomNavBackHandlingState
import com.velord.ui.feature.bottomnavigation.navigation.BottomNavEventService
import com.velord.ui.feature.bottomnavigation.navigation.BottomNavigationItem
import com.velord.ui.feature.bottomnavigation.navigation.TabState
-import com.velord.ui.sharedviewmodel.CoroutineScopeVM
+import com.velord.ui.sharedviewmodel.LocalizationVM
import com.velord.usecase.event.RequestAppExitUC
import com.velord.usecase.event.ShowToastUC
import kotlinx.coroutines.delay
@@ -20,7 +19,6 @@ import kotlinx.coroutines.flow.filterNot
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
-import org.jetbrains.compose.resources.getString
import kotlin.time.Duration.Companion.milliseconds
private const val CONFIRM_EXIT_THROTTLE = 2000L
@@ -28,8 +26,8 @@ private const val CONFIRM_EXIT_THROTTLE = 2000L
class BottomNavigationVM(
private val bottomNavEventService: BottomNavEventService,
private val requestAppExitUC: RequestAppExitUC,
- private val showToastUC: ShowToastUC
-) : CoroutineScopeVM() {
+ private val showToastUC: ShowToastUC,
+) : LocalizationVM() {
val uiStateFlow = MutableStateFlow(BottomNavigationUiState.from(bottomNavEventService))
val onTabClickEvent = MutableSharedFlow()
@@ -102,7 +100,7 @@ class BottomNavigationVM(
onBackDoubleClick()
} else {
setConfirmExitRequested(true)
- val message = getString(Res.string.press_again_to_exit)
+ val message = getString(AppString.press_again_to_exit)
val toastConfig = ToastConfig(
message = message,
duration = ToastDuration.Short,
@@ -117,7 +115,7 @@ class BottomNavigationVM(
private fun onUpdateBackHandling(
startDestinationRoster: List,
- currentRoute: String?
+ currentRoute: String?,
) {
val isStart = startDestinationRoster.contains(currentRoute)
val newState = uiStateFlow.value.backHandlingState.copy(
@@ -128,7 +126,7 @@ class BottomNavigationVM(
uiStateFlow.value.backHandlingState.isGrantedToProceed
} else {
false
- }
+ },
)
updateBackHandlingState(newState)
}
diff --git a/ui/feature-bottomnavigation/src/commonTest/kotlin/com/velord/ui/feature/bottomnavigation/viewmodel/BottomNavigationBackBehaviorTest.kt b/ui/feature-bottomnavigation/src/commonTest/kotlin/com/velord/ui/feature/bottomnavigation/viewmodel/BottomNavigationBackBehaviorTest.kt
index 08208e0c..60c679e8 100644
--- a/ui/feature-bottomnavigation/src/commonTest/kotlin/com/velord/ui/feature/bottomnavigation/viewmodel/BottomNavigationBackBehaviorTest.kt
+++ b/ui/feature-bottomnavigation/src/commonTest/kotlin/com/velord/ui/feature/bottomnavigation/viewmodel/BottomNavigationBackBehaviorTest.kt
@@ -15,7 +15,7 @@ class BottomNavigationBackBehaviorTest {
)
private val rootBehaviorWithGrant = mapOf(
BottomNavigationItem.Camera to BottomNavigationBackBehavior.ReturnToDefaultTab,
- BottomNavigationItem.Demo to BottomNavigationBackBehavior.DelegateToNavigator,
+ BottomNavigationItem.Demo to BottomNavigationBackBehavior.ConfirmExit,
BottomNavigationItem.Setting to BottomNavigationBackBehavior.ConfirmExit,
)
diff --git a/ui/feature-bottomnavigation/src/commonTest/kotlin/com/velord/ui/feature/bottomnavigation/viewmodel/BottomNavigationTestFixture.kt b/ui/feature-bottomnavigation/src/commonTest/kotlin/com/velord/ui/feature/bottomnavigation/viewmodel/BottomNavigationTestFixture.kt
index cd166537..b083b33a 100644
--- a/ui/feature-bottomnavigation/src/commonTest/kotlin/com/velord/ui/feature/bottomnavigation/viewmodel/BottomNavigationTestFixture.kt
+++ b/ui/feature-bottomnavigation/src/commonTest/kotlin/com/velord/ui/feature/bottomnavigation/viewmodel/BottomNavigationTestFixture.kt
@@ -5,6 +5,7 @@ import com.velord.ui.feature.bottomnavigation.navigation.BottomNavEventService
import com.velord.ui.feature.bottomnavigation.navigation.BottomNavigationItem
import com.velord.ui.feature.bottomnavigation.navigation.TabState
import com.velord.usecase.event.RequestAppExitUC
+import com.velord.usecase.event.ShowToastUC
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
@@ -16,21 +17,25 @@ internal const val TEST_TIMEOUT_MS = 1_000L
internal fun createBottomNavigationVM(
service: BottomNavEventService = BottomNavEventService(),
requestAppExitUC: RequestAppExitUC = RequestAppExitUC {},
+ showToastUC: ShowToastUC = ShowToastUC {},
): BottomNavigationVM = BottomNavigationVM(
bottomNavEventService = service,
requestAppExitUC = requestAppExitUC,
+ showToastUC = showToastUC,
)
internal fun createBottomNavigationUiState(
currentTab: BottomNavigationItem,
isAtRoot: Boolean,
isGranted: Boolean,
+ isConfirmExitRequested: Boolean = false,
) = BottomNavigationUiState(
tabState = TabState(previous = currentTab, current = currentTab),
backHandlingState = BottomNavBackHandlingState(
isAtStartGraphDestination = isAtRoot,
isGrantedToProceed = isGranted,
),
+ isConfirmExitRequested = isConfirmExitRequested,
)
internal suspend fun BottomNavigationVM.awaitUiState(
diff --git a/ui/feature-camerarecording/build.gradle.kts b/ui/feature-camerarecording/build.gradle.kts
index b21c4466..ba329a15 100644
--- a/ui/feature-camerarecording/build.gradle.kts
+++ b/ui/feature-camerarecording/build.gradle.kts
@@ -21,6 +21,7 @@ kotlin {
// Module Domain
implementation(projects.domain.usecaseCamera)
implementation(projects.domain.usecaseEvent)
+ implementation(projects.domain.usecaseSetting)
// Module UI
implementation(projects.ui.sharedviewmodel)
// Kotlin
diff --git a/ui/feature-camerarecording/src/androidMain/kotlin/com/velord/ui/feature/camerarecording/CameraRecordingFragment.kt b/ui/feature-camerarecording/src/androidMain/kotlin/com/velord/ui/feature/camerarecording/CameraRecordingFragment.kt
index b0f8d21d..e7a13d4c 100644
--- a/ui/feature-camerarecording/src/androidMain/kotlin/com/velord/ui/feature/camerarecording/CameraRecordingFragment.kt
+++ b/ui/feature-camerarecording/src/androidMain/kotlin/com/velord/ui/feature/camerarecording/CameraRecordingFragment.kt
@@ -110,6 +110,7 @@ class CameraRecordingFragment : Fragment() {
private fun checkRecordVideoPermission() {
checkRecordVideoPermission(
+ localization = viewModel.localizationStateFlow.value,
actionLauncher = requestRecordVideoPermissionLauncher,
onGranted = {
val action = CameraRecordingUiAction.UpdatePermissionGrantState(
diff --git a/ui/feature-camerarecording/src/commonMain/kotlin/com/velord/ui/feature/camerarecording/component/Content.kt b/ui/feature-camerarecording/src/commonMain/kotlin/com/velord/ui/feature/camerarecording/component/Content.kt
index 0b74c3b9..d71e7bc4 100644
--- a/ui/feature-camerarecording/src/commonMain/kotlin/com/velord/ui/feature/camerarecording/component/Content.kt
+++ b/ui/feature-camerarecording/src/commonMain/kotlin/com/velord/ui/feature/camerarecording/component/Content.kt
@@ -5,14 +5,13 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
-import com.velord.core.resource.Res
-import com.velord.core.resource.camera
+import com.velord.core.resource.AppString
+import com.velord.core.resource.stringResource
import com.velord.core.ui.compose.component.PlatformScreenHeader
import com.velord.core.ui.compose.preview.PreviewCombined
import com.velord.ui.feature.camerarecording.component.button.CameraSettingButton
import com.velord.ui.feature.camerarecording.viewModel.CameraRecordingUiAction
import com.velord.ui.feature.camerarecording.viewModel.CameraRecordingUiState
-import org.jetbrains.compose.resources.stringResource
@Composable
internal fun Content(
@@ -25,7 +24,7 @@ internal fun Content(
HeaderShroud()
PlatformScreenHeader(
modifier = Modifier.statusBarsPadding(),
- title = stringResource(Res.string.camera),
+ title = stringResource(AppString.camera),
onBackClick = onBackClick,
)
PermissionInfo(
diff --git a/ui/feature-camerarecording/src/commonMain/kotlin/com/velord/ui/feature/camerarecording/component/PermissionInfo.kt b/ui/feature-camerarecording/src/commonMain/kotlin/com/velord/ui/feature/camerarecording/component/PermissionInfo.kt
index 0636ba28..67c83dbe 100644
--- a/ui/feature-camerarecording/src/commonMain/kotlin/com/velord/ui/feature/camerarecording/component/PermissionInfo.kt
+++ b/ui/feature-camerarecording/src/commonMain/kotlin/com/velord/ui/feature/camerarecording/component/PermissionInfo.kt
@@ -20,14 +20,12 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp
-import com.velord.core.resource.Res
-import com.velord.core.resource.can_not_get_permission_for_camera
-import com.velord.core.resource.can_not_get_permission_for_mic
+import com.velord.core.resource.AppString
+import com.velord.core.resource.AppStringResource
+import com.velord.core.resource.stringResource
import com.velord.core.ui.compose.preview.PreviewCombined
import com.velord.infrastructure.util.permission.PermissionGrantState
import com.velord.ui.feature.camerarecording.viewModel.CameraRecordingUiState
-import org.jetbrains.compose.resources.StringResource
-import org.jetbrains.compose.resources.stringResource
@Composable
internal fun PermissionInfo(
@@ -41,7 +39,7 @@ internal fun PermissionInfo(
if (uiState.permissionState.camera.isForbidden) {
PermissionIsNotGrantedState(
icon = Icons.Filled.CameraAlt,
- label = Res.string.can_not_get_permission_for_camera,
+ label = AppString.can_not_get_permission_for_camera,
onClick = onCheckPermissionClick,
)
}
@@ -49,7 +47,7 @@ internal fun PermissionInfo(
Spacer(modifier = Modifier.size(32.dp))
PermissionIsNotGrantedState(
icon = Icons.Filled.PermCameraMic,
- label = Res.string.can_not_get_permission_for_mic,
+ label = AppString.can_not_get_permission_for_mic,
onClick = onCheckPermissionClick,
)
}
@@ -59,7 +57,7 @@ internal fun PermissionInfo(
@Composable
private fun PermissionIsNotGrantedState(
icon: ImageVector,
- label: StringResource,
+ label: AppStringResource,
onClick: () -> Unit,
) {
Row(
diff --git a/ui/feature-camerarecording/src/commonMain/kotlin/com/velord/ui/feature/camerarecording/component/button/OpenVideoFolderButton.kt b/ui/feature-camerarecording/src/commonMain/kotlin/com/velord/ui/feature/camerarecording/component/button/OpenVideoFolderButton.kt
index 5f0565f0..cdf2c663 100644
--- a/ui/feature-camerarecording/src/commonMain/kotlin/com/velord/ui/feature/camerarecording/component/button/OpenVideoFolderButton.kt
+++ b/ui/feature-camerarecording/src/commonMain/kotlin/com/velord/ui/feature/camerarecording/component/button/OpenVideoFolderButton.kt
@@ -6,11 +6,10 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
-import com.velord.core.resource.Res
-import com.velord.core.resource.open_video_folder
+import com.velord.core.resource.AppString
+import com.velord.core.resource.stringResource
import com.velord.core.ui.compose.preview.PreviewCombined
import com.velord.ui.feature.camerarecording.viewModel.CameraRecordingUiState
-import org.jetbrains.compose.resources.stringResource
@Composable
internal fun OpenVideoFolderButton(
@@ -26,7 +25,7 @@ internal fun OpenVideoFolderButton(
) {
Icon(
imageVector = Icons.Filled.FolderOpen,
- contentDescription = stringResource(Res.string.open_video_folder),
+ contentDescription = stringResource(AppString.open_video_folder),
)
}
}
diff --git a/ui/feature-camerarecording/src/commonMain/kotlin/com/velord/ui/feature/camerarecording/component/button/StartStopRecordingButton.kt b/ui/feature-camerarecording/src/commonMain/kotlin/com/velord/ui/feature/camerarecording/component/button/StartStopRecordingButton.kt
index d512978a..ef937104 100644
--- a/ui/feature-camerarecording/src/commonMain/kotlin/com/velord/ui/feature/camerarecording/component/button/StartStopRecordingButton.kt
+++ b/ui/feature-camerarecording/src/commonMain/kotlin/com/velord/ui/feature/camerarecording/component/button/StartStopRecordingButton.kt
@@ -18,12 +18,10 @@ import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp
-import com.velord.core.resource.Res
-import com.velord.core.resource.start_recording
-import com.velord.core.resource.stop_recording
+import com.velord.core.resource.AppString
+import com.velord.core.resource.stringResource
import com.velord.core.ui.compose.preview.PreviewCombined
import com.velord.ui.feature.camerarecording.viewModel.CameraRecordingUiState
-import org.jetbrains.compose.resources.stringResource
@Composable
internal fun StartStopRecordingButton(
@@ -51,9 +49,9 @@ private fun CameraRecordingUiState.hasRequiredPermission(): Boolean {
@Composable
private fun startStopRecordingLabel(isRecording: Boolean): String = if (isRecording) {
- stringResource(Res.string.stop_recording)
+ stringResource(AppString.stop_recording)
} else {
- stringResource(Res.string.start_recording)
+ stringResource(AppString.start_recording)
}
@Composable
diff --git a/ui/feature-camerarecording/src/commonMain/kotlin/com/velord/ui/feature/camerarecording/component/switcher/audio/CameraAudioLabel.kt b/ui/feature-camerarecording/src/commonMain/kotlin/com/velord/ui/feature/camerarecording/component/switcher/audio/CameraAudioLabel.kt
index 640d6628..d4db25f4 100644
--- a/ui/feature-camerarecording/src/commonMain/kotlin/com/velord/ui/feature/camerarecording/component/switcher/audio/CameraAudioLabel.kt
+++ b/ui/feature-camerarecording/src/commonMain/kotlin/com/velord/ui/feature/camerarecording/component/switcher/audio/CameraAudioLabel.kt
@@ -7,10 +7,9 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
-import com.velord.core.resource.Res
-import com.velord.core.resource.record_audio_on
+import com.velord.core.resource.AppString
+import com.velord.core.resource.stringResource
import com.velord.core.ui.compose.preview.PreviewCombined
-import org.jetbrains.compose.resources.stringResource
@Composable
internal fun CameraAudioLabel(label: String) {
@@ -27,5 +26,5 @@ internal fun CameraAudioLabel(label: String) {
@PreviewCombined
@Composable
private fun Preview() {
- CameraAudioLabel(label = stringResource(Res.string.record_audio_on))
+ CameraAudioLabel(label = stringResource(AppString.record_audio_on))
}
diff --git a/ui/feature-camerarecording/src/commonMain/kotlin/com/velord/ui/feature/camerarecording/component/switcher/audio/CameraAudioSwitch.kt b/ui/feature-camerarecording/src/commonMain/kotlin/com/velord/ui/feature/camerarecording/component/switcher/audio/CameraAudioSwitch.kt
index bf70a580..f073fbf9 100644
--- a/ui/feature-camerarecording/src/commonMain/kotlin/com/velord/ui/feature/camerarecording/component/switcher/audio/CameraAudioSwitch.kt
+++ b/ui/feature-camerarecording/src/commonMain/kotlin/com/velord/ui/feature/camerarecording/component/switcher/audio/CameraAudioSwitch.kt
@@ -18,10 +18,9 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
-import com.velord.core.resource.Res
-import com.velord.core.resource.record_audio_on
+import com.velord.core.resource.AppString
+import com.velord.core.resource.stringResource
import com.velord.core.ui.compose.preview.PreviewCombined
-import org.jetbrains.compose.resources.stringResource
@Composable
internal fun CameraAudioSwitch(
@@ -74,7 +73,7 @@ private fun Preview() {
CameraAudioSwitch(
isEnabled = true,
isRecording = false,
- contentDescription = stringResource(Res.string.record_audio_on),
+ contentDescription = stringResource(AppString.record_audio_on),
onClick = {},
)
}
diff --git a/ui/feature-camerarecording/src/commonMain/kotlin/com/velord/ui/feature/camerarecording/component/switcher/audio/CameraAudioSwitcher.kt b/ui/feature-camerarecording/src/commonMain/kotlin/com/velord/ui/feature/camerarecording/component/switcher/audio/CameraAudioSwitcher.kt
index 7a2d4a2d..7ab12b08 100644
--- a/ui/feature-camerarecording/src/commonMain/kotlin/com/velord/ui/feature/camerarecording/component/switcher/audio/CameraAudioSwitcher.kt
+++ b/ui/feature-camerarecording/src/commonMain/kotlin/com/velord/ui/feature/camerarecording/component/switcher/audio/CameraAudioSwitcher.kt
@@ -7,12 +7,10 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
-import com.velord.core.resource.Res
-import com.velord.core.resource.record_audio_off
-import com.velord.core.resource.record_audio_on
+import com.velord.core.resource.AppString
+import com.velord.core.resource.stringResource
import com.velord.core.ui.compose.preview.PreviewCombined
import com.velord.ui.feature.camerarecording.viewModel.CameraRecordingUiState
-import org.jetbrains.compose.resources.stringResource
@Composable
internal fun CameraAudioSwitcher(
@@ -23,9 +21,9 @@ internal fun CameraAudioSwitcher(
val isEnabled = uiState.isAudioEnabled
val isRecording = uiState.isRecordingStarted
val label = if (isEnabled) {
- stringResource(Res.string.record_audio_on)
+ stringResource(AppString.record_audio_on)
} else {
- stringResource(Res.string.record_audio_off)
+ stringResource(AppString.record_audio_off)
}
Column(
diff --git a/ui/feature-camerarecording/src/commonMain/kotlin/com/velord/ui/feature/camerarecording/component/switcher/lens/CameraLensLabel.kt b/ui/feature-camerarecording/src/commonMain/kotlin/com/velord/ui/feature/camerarecording/component/switcher/lens/CameraLensLabel.kt
index d6730abc..3f7a5be4 100644
--- a/ui/feature-camerarecording/src/commonMain/kotlin/com/velord/ui/feature/camerarecording/component/switcher/lens/CameraLensLabel.kt
+++ b/ui/feature-camerarecording/src/commonMain/kotlin/com/velord/ui/feature/camerarecording/component/switcher/lens/CameraLensLabel.kt
@@ -7,10 +7,9 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shadow
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
-import com.velord.core.resource.Res
-import com.velord.core.resource.front
+import com.velord.core.resource.AppString
+import com.velord.core.resource.stringResource
import com.velord.core.ui.compose.preview.PreviewCombined
-import org.jetbrains.compose.resources.stringResource
@Composable
internal fun CameraLensLabel(
@@ -53,7 +52,7 @@ private fun lensLabelShadow(
@Composable
private fun Preview() {
CameraLensLabel(
- text = stringResource(Res.string.front),
+ text = stringResource(AppString.front),
isSelected = true,
accentColor = Color(0xFFA87BF7),
)
diff --git a/ui/feature-camerarecording/src/commonMain/kotlin/com/velord/ui/feature/camerarecording/component/switcher/lens/CameraLensLabelRow.kt b/ui/feature-camerarecording/src/commonMain/kotlin/com/velord/ui/feature/camerarecording/component/switcher/lens/CameraLensLabelRow.kt
index d24bc970..1d9a8259 100644
--- a/ui/feature-camerarecording/src/commonMain/kotlin/com/velord/ui/feature/camerarecording/component/switcher/lens/CameraLensLabelRow.kt
+++ b/ui/feature-camerarecording/src/commonMain/kotlin/com/velord/ui/feature/camerarecording/component/switcher/lens/CameraLensLabelRow.kt
@@ -11,11 +11,9 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
-import com.velord.core.resource.Res
-import com.velord.core.resource.front
-import com.velord.core.resource.rear
+import com.velord.core.resource.AppString
+import com.velord.core.resource.stringResource
import com.velord.core.ui.compose.preview.PreviewCombined
-import org.jetbrains.compose.resources.stringResource
@Composable
internal fun CameraLensLabelRow(
@@ -27,7 +25,7 @@ internal fun CameraLensLabelRow(
modifier = Modifier.padding(top = 4.dp),
) {
CameraLensLabel(
- text = stringResource(Res.string.front),
+ text = stringResource(AppString.front),
isSelected = isFront,
accentColor = accentColor,
)
@@ -40,7 +38,7 @@ internal fun CameraLensLabelRow(
),
)
CameraLensLabel(
- text = stringResource(Res.string.rear),
+ text = stringResource(AppString.rear),
isSelected = isFront.not(),
accentColor = accentColor,
)
diff --git a/ui/feature-camerarecording/src/commonMain/kotlin/com/velord/ui/feature/camerarecording/component/switcher/lens/CameraLensSwitchSurface.kt b/ui/feature-camerarecording/src/commonMain/kotlin/com/velord/ui/feature/camerarecording/component/switcher/lens/CameraLensSwitchSurface.kt
index 48459a61..a7ffbdda 100644
--- a/ui/feature-camerarecording/src/commonMain/kotlin/com/velord/ui/feature/camerarecording/component/switcher/lens/CameraLensSwitchSurface.kt
+++ b/ui/feature-camerarecording/src/commonMain/kotlin/com/velord/ui/feature/camerarecording/component/switcher/lens/CameraLensSwitchSurface.kt
@@ -19,11 +19,10 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.unit.dp
-import com.velord.core.resource.Res
-import com.velord.core.resource.switch_camera_lens
+import com.velord.core.resource.AppString
+import com.velord.core.resource.stringResource
import com.velord.core.ui.compose.icon.DoubleArrowRightIcon
import com.velord.core.ui.compose.preview.PreviewCombined
-import org.jetbrains.compose.resources.stringResource
@Composable
internal fun CameraLensSwitchSurface(
@@ -93,7 +92,7 @@ private fun CameraLensSwitchThumb(
) {
Icon(
imageVector = DoubleArrowRightIcon,
- contentDescription = stringResource(Res.string.switch_camera_lens),
+ contentDescription = stringResource(AppString.switch_camera_lens),
modifier = Modifier.size(18.dp),
tint = lensSwitchIconTint(isFront = isFront),
)
diff --git a/ui/feature-camerarecording/src/commonMain/kotlin/com/velord/ui/feature/camerarecording/component/videoQuality/CameraVideoQualityChooser.kt b/ui/feature-camerarecording/src/commonMain/kotlin/com/velord/ui/feature/camerarecording/component/videoQuality/CameraVideoQualityChooser.kt
index d9196813..5b581730 100644
--- a/ui/feature-camerarecording/src/commonMain/kotlin/com/velord/ui/feature/camerarecording/component/videoQuality/CameraVideoQualityChooser.kt
+++ b/ui/feature-camerarecording/src/commonMain/kotlin/com/velord/ui/feature/camerarecording/component/videoQuality/CameraVideoQualityChooser.kt
@@ -6,12 +6,11 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import com.velord.core.resource.Res
-import com.velord.core.resource.video_quality
+import com.velord.core.resource.AppString
+import com.velord.core.resource.stringResource
import com.velord.core.ui.compose.preview.PreviewCombined
import com.velord.model.camera.config.CameraVideoQuality
import com.velord.ui.feature.camerarecording.viewModel.CameraRecordingUiState
-import org.jetbrains.compose.resources.stringResource
@Composable
internal fun CameraVideoQualityChooser(
@@ -20,7 +19,7 @@ internal fun CameraVideoQualityChooser(
modifier: Modifier = Modifier,
) {
val expandedState = remember { mutableStateOf(false) }
- val label = stringResource(Res.string.video_quality)
+ val label = stringResource(AppString.video_quality)
Box(
modifier = modifier,
diff --git a/ui/feature-camerarecording/src/commonMain/kotlin/com/velord/ui/feature/camerarecording/component/videoQuality/CameraVideoQualityMenuItem.kt b/ui/feature-camerarecording/src/commonMain/kotlin/com/velord/ui/feature/camerarecording/component/videoQuality/CameraVideoQualityMenuItem.kt
index ccd705f5..b2156410 100644
--- a/ui/feature-camerarecording/src/commonMain/kotlin/com/velord/ui/feature/camerarecording/component/videoQuality/CameraVideoQualityMenuItem.kt
+++ b/ui/feature-camerarecording/src/commonMain/kotlin/com/velord/ui/feature/camerarecording/component/videoQuality/CameraVideoQualityMenuItem.kt
@@ -13,12 +13,11 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
-import com.velord.core.resource.Res
-import com.velord.core.resource.selected
+import com.velord.core.resource.AppString
+import com.velord.core.resource.stringResource
import com.velord.core.ui.compose.preview.PreviewCombined
import com.velord.model.camera.config.CameraVideoQuality
import com.velord.ui.feature.camerarecording.util.label
-import org.jetbrains.compose.resources.stringResource
@Composable
internal fun CameraVideoQualityMenuItem(
@@ -40,7 +39,7 @@ internal fun CameraVideoQualityMenuItem(
if (isSelected) {
Icon(
imageVector = Icons.Filled.Check,
- contentDescription = stringResource(Res.string.selected),
+ contentDescription = stringResource(AppString.selected),
modifier = Modifier.size(18.dp),
tint = MaterialTheme.colorScheme.secondary,
)
diff --git a/ui/feature-camerarecording/src/commonMain/kotlin/com/velord/ui/feature/camerarecording/component/videoQuality/CameraVideoQualityTrigger.kt b/ui/feature-camerarecording/src/commonMain/kotlin/com/velord/ui/feature/camerarecording/component/videoQuality/CameraVideoQualityTrigger.kt
index 0623bc71..35cacc9a 100644
--- a/ui/feature-camerarecording/src/commonMain/kotlin/com/velord/ui/feature/camerarecording/component/videoQuality/CameraVideoQualityTrigger.kt
+++ b/ui/feature-camerarecording/src/commonMain/kotlin/com/velord/ui/feature/camerarecording/component/videoQuality/CameraVideoQualityTrigger.kt
@@ -17,12 +17,11 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
-import com.velord.core.resource.Res
-import com.velord.core.resource.video_quality
+import com.velord.core.resource.AppString
+import com.velord.core.resource.stringResource
import com.velord.core.ui.compose.preview.PreviewCombined
import com.velord.model.camera.config.CameraVideoQuality
import com.velord.ui.feature.camerarecording.util.shortLabel
-import org.jetbrains.compose.resources.stringResource
@Composable
internal fun CameraVideoQualityTrigger(
@@ -66,7 +65,7 @@ private fun Preview() {
CameraVideoQualityTrigger(
quality = CameraVideoQuality.FullHd,
isEnabled = true,
- contentDescription = stringResource(Res.string.video_quality),
+ contentDescription = stringResource(AppString.video_quality),
onClick = {},
)
}
diff --git a/ui/feature-camerarecording/src/commonMain/kotlin/com/velord/ui/feature/camerarecording/util/CameraVideoQualityLabel.kt b/ui/feature-camerarecording/src/commonMain/kotlin/com/velord/ui/feature/camerarecording/util/CameraVideoQualityLabel.kt
index b2bb2fda..d24e1b9e 100644
--- a/ui/feature-camerarecording/src/commonMain/kotlin/com/velord/ui/feature/camerarecording/util/CameraVideoQualityLabel.kt
+++ b/ui/feature-camerarecording/src/commonMain/kotlin/com/velord/ui/feature/camerarecording/util/CameraVideoQualityLabel.kt
@@ -1,25 +1,19 @@
package com.velord.ui.feature.camerarecording.util
-import com.velord.core.resource.Res
-import com.velord.core.resource.video_quality_full_hd
-import com.velord.core.resource.video_quality_full_hd_short
-import com.velord.core.resource.video_quality_hd
-import com.velord.core.resource.video_quality_sd
-import com.velord.core.resource.video_quality_ultra_hd
-import com.velord.core.resource.video_quality_ultra_hd_short
+import com.velord.core.resource.AppString
+import com.velord.core.resource.AppStringResource
import com.velord.model.camera.config.CameraVideoQuality
-import org.jetbrains.compose.resources.StringResource
-internal val CameraVideoQuality.shortLabel: StringResource get() = when (this) {
- CameraVideoQuality.Sd -> Res.string.video_quality_sd
- CameraVideoQuality.Hd -> Res.string.video_quality_hd
- CameraVideoQuality.FullHd -> Res.string.video_quality_full_hd_short
- CameraVideoQuality.UltraHd -> Res.string.video_quality_ultra_hd_short
+internal val CameraVideoQuality.shortLabel: AppStringResource get() = when (this) {
+ CameraVideoQuality.Sd -> AppString.video_quality_sd
+ CameraVideoQuality.Hd -> AppString.video_quality_hd
+ CameraVideoQuality.FullHd -> AppString.video_quality_full_hd_short
+ CameraVideoQuality.UltraHd -> AppString.video_quality_ultra_hd_short
}
-internal val CameraVideoQuality.label: StringResource get() = when (this) {
- CameraVideoQuality.Sd -> Res.string.video_quality_sd
- CameraVideoQuality.Hd -> Res.string.video_quality_hd
- CameraVideoQuality.FullHd -> Res.string.video_quality_full_hd
- CameraVideoQuality.UltraHd -> Res.string.video_quality_ultra_hd
+internal val CameraVideoQuality.label: AppStringResource get() = when (this) {
+ CameraVideoQuality.Sd -> AppString.video_quality_sd
+ CameraVideoQuality.Hd -> AppString.video_quality_hd
+ CameraVideoQuality.FullHd -> AppString.video_quality_full_hd
+ CameraVideoQuality.UltraHd -> AppString.video_quality_ultra_hd
}
diff --git a/ui/feature-camerarecording/src/commonMain/kotlin/com/velord/ui/feature/camerarecording/viewModel/CameraRecordingVM.kt b/ui/feature-camerarecording/src/commonMain/kotlin/com/velord/ui/feature/camerarecording/viewModel/CameraRecordingVM.kt
index 38423c40..0f9feac2 100644
--- a/ui/feature-camerarecording/src/commonMain/kotlin/com/velord/ui/feature/camerarecording/viewModel/CameraRecordingVM.kt
+++ b/ui/feature-camerarecording/src/commonMain/kotlin/com/velord/ui/feature/camerarecording/viewModel/CameraRecordingVM.kt
@@ -1,7 +1,6 @@
package com.velord.ui.feature.camerarecording.viewModel
-import com.velord.core.resource.Res
-import com.velord.core.resource.recording_restarted_after_camera_switch
+import com.velord.core.resource.AppString
import com.velord.infrastructure.util.permission.PermissionGrantState
import com.velord.model.ToastConfig
import com.velord.model.ToastDuration
@@ -11,7 +10,7 @@ import com.velord.model.camera.config.CameraLens
import com.velord.model.camera.config.CameraRecordingConfig
import com.velord.model.camera.config.CameraVideoQuality
import com.velord.ui.feature.camerarecording.CameraRecordingNavigationEvent
-import com.velord.ui.sharedviewmodel.CoroutineScopeVM
+import com.velord.ui.sharedviewmodel.LocalizationVM
import com.velord.usecase.camera.CreateCameraSessionUC
import com.velord.usecase.camera.GetCameraSessionUC
import com.velord.usecase.camera.GetCameraStateUC
@@ -25,7 +24,6 @@ import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
-import org.jetbrains.compose.resources.getString
class CameraRecordingVM(
private val createCameraSession: CreateCameraSessionUC,
@@ -37,7 +35,7 @@ class CameraRecordingVM(
private val toggleCameraLens: ToggleCameraLensUC,
private val openCameraVideoFolder: OpenCameraVideoFolderUC,
private val showToastUC: ShowToastUC,
-) : CoroutineScopeVM() {
+) : LocalizationVM() {
val uiStateFlow = MutableStateFlow(CameraRecordingUiState.DEFAULT)
val checkPermissionEvent = MutableSharedFlow()
@@ -107,7 +105,7 @@ class CameraRecordingVM(
}
private suspend fun showRecordingRestartedWarning() {
- val message = getString(Res.string.recording_restarted_after_camera_switch)
+ val message = getString(AppString.recording_restarted_after_camera_switch)
val toastConfig = ToastConfig(message = message, duration = ToastDuration.Long)
showToastUC(toastConfig)
}
diff --git a/ui/feature-demo-dialog/src/commonMain/kotlin/com/velord/ui/feature/demo/dialog/DialogDemoScreen.kt b/ui/feature-demo-dialog/src/commonMain/kotlin/com/velord/ui/feature/demo/dialog/DialogDemoScreen.kt
index fa991d9e..02f3b1d6 100644
--- a/ui/feature-demo-dialog/src/commonMain/kotlin/com/velord/ui/feature/demo/dialog/DialogDemoScreen.kt
+++ b/ui/feature-demo-dialog/src/commonMain/kotlin/com/velord/ui/feature/demo/dialog/DialogDemoScreen.kt
@@ -20,10 +20,8 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
-import com.velord.core.resource.Res
-import com.velord.core.resource.dialog_demo
-import com.velord.core.resource.show_one_button_dialog
-import com.velord.core.resource.show_two_buttons_dialog
+import com.velord.core.resource.AppString
+import com.velord.core.resource.stringResource
import com.velord.core.ui.compose.component.PlatformScreenHeader
import com.velord.core.ui.compose.preview.PreviewCombined
import com.velord.ui.feature.demo.dialog.component.DialogActions
@@ -31,7 +29,6 @@ import com.velord.ui.feature.demo.dialog.component.DialogAnimations
import com.velord.ui.feature.demo.dialog.component.DialogPredefinedAnimation
import com.velord.ui.feature.demo.dialog.component.OneButtonDialog
import com.velord.ui.feature.demo.dialog.component.TwoButtonDialog
-import org.jetbrains.compose.resources.stringResource
@Composable
fun DialogDemoScreen(
@@ -81,15 +78,15 @@ private fun Content(
horizontalAlignment = Alignment.CenterHorizontally,
) {
PlatformScreenHeader(
- title = stringResource(Res.string.dialog_demo),
+ title = stringResource(AppString.dialog_demo),
onBackClick = onBackClick
)
OpenButton(
- text = stringResource(Res.string.show_two_buttons_dialog),
+ text = stringResource(AppString.show_two_buttons_dialog),
onClick = { onAction(DialogDemoUiAction.OpenTwoButtonDialogClick) }
)
OpenButton(
- text = stringResource(Res.string.show_one_button_dialog),
+ text = stringResource(AppString.show_one_button_dialog),
onClick = { onAction(DialogDemoUiAction.OpenOneButtonDialogClick) },
)
}
diff --git a/ui/feature-demo-dialog/src/commonMain/kotlin/com/velord/ui/feature/demo/dialog/component/Dialog.kt b/ui/feature-demo-dialog/src/commonMain/kotlin/com/velord/ui/feature/demo/dialog/component/Dialog.kt
index 8af05cba..9fd36d5e 100644
--- a/ui/feature-demo-dialog/src/commonMain/kotlin/com/velord/ui/feature/demo/dialog/component/Dialog.kt
+++ b/ui/feature-demo-dialog/src/commonMain/kotlin/com/velord/ui/feature/demo/dialog/component/Dialog.kt
@@ -7,13 +7,9 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
-import com.velord.core.resource.Res
-import com.velord.core.resource.app_name
-import com.velord.core.resource.cancel
-import com.velord.core.resource.confirm
-import com.velord.core.resource.ok
+import com.velord.core.resource.AppString
+import com.velord.core.resource.stringResource
import com.velord.core.ui.compose.preview.PreviewCombined
-import org.jetbrains.compose.resources.stringResource
@Composable
fun OneButtonDialog(
@@ -25,7 +21,7 @@ fun OneButtonDialog(
color: DialogColors = DialogDefaults.colors(),
style: DialogTextStyles = DialogDefaults.textStyles(),
message: String? = null,
- positiveText: String = stringResource(Res.string.ok),
+ positiveText: String = stringResource(AppString.ok),
content: @Composable () -> Unit = {
Content(
title = title,
@@ -59,8 +55,8 @@ fun TwoButtonDialog(
color: DialogColors = DialogDefaults.colors(),
style: DialogTextStyles = DialogDefaults.textStyles(),
message: String? = null,
- positiveText: String = stringResource(Res.string.confirm),
- negativeText: String = stringResource(Res.string.cancel),
+ positiveText: String = stringResource(AppString.confirm),
+ negativeText: String = stringResource(AppString.cancel),
content: @Composable () -> Unit = {
Content(
title = title,
@@ -123,8 +119,8 @@ private fun WithTwoButtonsPreview() {
MaterialTheme {
TwoButtonDialog(
isVisibleState = remember { mutableStateOf(true) },
- title = stringResource(Res.string.app_name),
- message = stringResource(Res.string.app_name),
+ title = stringResource(AppString.app_name),
+ message = stringResource(AppString.app_name),
action = DialogActions.TwoButton {}
)
}
@@ -136,8 +132,8 @@ private fun OneButtonPreview() {
MaterialTheme {
OneButtonDialog(
isVisibleState = remember { mutableStateOf(true) },
- title = stringResource(Res.string.app_name),
- message = stringResource(Res.string.app_name),
+ title = stringResource(AppString.app_name),
+ message = stringResource(AppString.app_name),
action = DialogActions.OneButton {}
)
}
diff --git a/ui/feature-demo-hintphonenumber/src/commonMain/kotlin/com/velord/ui/feature/demo/hintphonenumber/HintPhoneNumberScreen.kt b/ui/feature-demo-hintphonenumber/src/commonMain/kotlin/com/velord/ui/feature/demo/hintphonenumber/HintPhoneNumberScreen.kt
index 9bb853d1..d57b7c12 100644
--- a/ui/feature-demo-hintphonenumber/src/commonMain/kotlin/com/velord/ui/feature/demo/hintphonenumber/HintPhoneNumberScreen.kt
+++ b/ui/feature-demo-hintphonenumber/src/commonMain/kotlin/com/velord/ui/feature/demo/hintphonenumber/HintPhoneNumberScreen.kt
@@ -16,14 +16,10 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
-import com.velord.core.resource.Res
-import com.velord.core.resource.hint_phone_number
-import com.velord.core.resource.invoke_again
-import com.velord.core.resource.phone_number_hint_not_available_on_desktop
-import com.velord.core.resource.waiting
+import com.velord.core.resource.AppString
+import com.velord.core.resource.stringResource
import com.velord.core.ui.compose.component.PlatformScreenHeader
import com.velord.core.ui.compose.preview.PreviewCombined
-import org.jetbrains.compose.resources.stringResource
@Composable
fun HintPhoneNumberScreen(onBackClick: (() -> Unit)? = null) {
@@ -31,7 +27,7 @@ fun HintPhoneNumberScreen(onBackClick: (() -> Unit)? = null) {
mutableStateOf(null)
}
- val unavailableMessage = stringResource(Res.string.phone_number_hint_not_available_on_desktop)
+ val unavailableMessage = stringResource(AppString.phone_number_hint_not_available_on_desktop)
val register = rememberPhoneNumberHintLauncher { result ->
phoneState.value = when (result) {
is PhoneNumberHintResult.Hint -> result.phoneNumber
@@ -63,10 +59,10 @@ private fun Content(
horizontalAlignment = Alignment.CenterHorizontally
) {
PlatformScreenHeader(
- title = stringResource(Res.string.hint_phone_number),
+ title = stringResource(AppString.hint_phone_number),
onBackClick = onBackClick
)
- val defStr = stringResource(Res.string.waiting)
+ val defStr = stringResource(AppString.waiting)
val str = phone ?: defStr
Text(
text = str,
@@ -82,7 +78,7 @@ private fun Content(
)
) {
Text(
- text = stringResource(Res.string.invoke_again),
+ text = stringResource(AppString.invoke_again),
color = MaterialTheme.colorScheme.onPrimary,
style = MaterialTheme.typography.bodyMedium,
)
diff --git a/ui/feature-demo-shape/build.gradle.kts b/ui/feature-demo-shape/build.gradle.kts
index 38f7204e..01325827 100644
--- a/ui/feature-demo-shape/build.gradle.kts
+++ b/ui/feature-demo-shape/build.gradle.kts
@@ -12,6 +12,7 @@ kotlin {
sourceSets {
commonMain.dependencies {
// Module Core
+ implementation(projects.core.coreResource)
implementation(projects.core.coreUi)
// Compose
implementation(libs.compose.foundation)
diff --git a/ui/feature-demo-shape/src/commonMain/kotlin/com/velord/ui/feature/demo/shape/ShapeDemoScreen.kt b/ui/feature-demo-shape/src/commonMain/kotlin/com/velord/ui/feature/demo/shape/ShapeDemoScreen.kt
index 0ab13993..acb06a6d 100644
--- a/ui/feature-demo-shape/src/commonMain/kotlin/com/velord/ui/feature/demo/shape/ShapeDemoScreen.kt
+++ b/ui/feature-demo-shape/src/commonMain/kotlin/com/velord/ui/feature/demo/shape/ShapeDemoScreen.kt
@@ -12,6 +12,8 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
+import com.velord.core.resource.AppString
+import com.velord.core.resource.stringResource
import com.velord.core.ui.compose.component.PlatformScreenHeader
import com.velord.core.ui.compose.preview.PreviewCombined
import com.velord.ui.feature.demo.shape.component.PervasiveArcFromBottomShapeDemo
@@ -30,11 +32,11 @@ private fun Content(onBackClick: () -> Unit) {
.fillMaxSize()
.background(MaterialTheme.colorScheme.surface)
.statusBarsPadding()
- .verticalScroll(rememberScrollState())
+ .verticalScroll(rememberScrollState()),
) {
PlatformScreenHeader(
- title = "Shape Demo",
- onBackClick = onBackClick
+ title = stringResource(AppString.shape_demo),
+ onBackClick = onBackClick,
)
TicketShapeDemo()
PervasiveArcFromBottomShapeDemo()
diff --git a/ui/feature-demo/build.gradle.kts b/ui/feature-demo/build.gradle.kts
index 392d9c5f..8f4e149e 100644
--- a/ui/feature-demo/build.gradle.kts
+++ b/ui/feature-demo/build.gradle.kts
@@ -17,6 +17,7 @@ kotlin {
api(projects.infrastructure.config)
// Module Domain
implementation(projects.domain.usecaseEvent)
+ implementation(projects.domain.usecaseSetting)
// Module Core
implementation(projects.core.coreResource)
implementation(projects.core.coreUi)
diff --git a/ui/feature-demo/src/commonMain/kotlin/com/velord/ui/feature/demo/DemoScreen.kt b/ui/feature-demo/src/commonMain/kotlin/com/velord/ui/feature/demo/DemoScreen.kt
index e506c61e..aa2ac374 100644
--- a/ui/feature-demo/src/commonMain/kotlin/com/velord/ui/feature/demo/DemoScreen.kt
+++ b/ui/feature-demo/src/commonMain/kotlin/com/velord/ui/feature/demo/DemoScreen.kt
@@ -17,19 +17,11 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
-import com.velord.core.resource.Res
-import com.velord.core.resource.demo
-import com.velord.core.resource.open_dialog
-import com.velord.core.resource.open_flow_summator
-import com.velord.core.resource.open_hint_phone_number
-import com.velord.core.resource.open_modifier_demo
-import com.velord.core.resource.open_morph_demo
-import com.velord.core.resource.open_movie
-import com.velord.core.resource.open_shape_demo
+import com.velord.core.resource.AppString
+import com.velord.core.resource.stringResource
import com.velord.core.ui.compose.component.PlatformScreenHeader
import com.velord.core.ui.compose.preview.PreviewCombined
import com.velord.core.ui.util.ObserveSharedFlow
-import org.jetbrains.compose.resources.stringResource
@Composable
fun DemoScreen(
@@ -69,36 +61,36 @@ private fun Content(
horizontalAlignment = Alignment.CenterHorizontally,
) {
PlatformScreenHeader(
- title = stringResource(Res.string.demo),
+ title = stringResource(AppString.demo),
onBackClick = onBackClick,
)
OpenButton(
- text = stringResource(Res.string.open_shape_demo),
+ text = stringResource(AppString.open_shape_demo),
onClick = { onAction(DemoUiAction.OpenShapeClick) },
)
OpenButton(
- text = stringResource(Res.string.open_modifier_demo),
+ text = stringResource(AppString.open_modifier_demo),
onClick = { onAction(DemoUiAction.OpenModifierClick) },
)
OpenButton(
- text = stringResource(Res.string.open_flow_summator),
+ text = stringResource(AppString.open_flow_summator),
onClick = { onAction(DemoUiAction.OpenSummatorClick) },
)
OpenButton(
- text = stringResource(Res.string.open_morph_demo),
+ text = stringResource(AppString.open_morph_demo),
onClick = { onAction(DemoUiAction.OpenMorphClick) },
)
OpenButton(
- text = stringResource(Res.string.open_hint_phone_number),
+ text = stringResource(AppString.open_hint_phone_number),
onClick = { onAction(DemoUiAction.OpenHintPhoneNumberClick) },
)
OpenButton(
- text = stringResource(Res.string.open_movie),
+ text = stringResource(AppString.open_movie),
onClick = { onAction(DemoUiAction.OpenMovieClick) },
)
OpenButton(
- text = stringResource(Res.string.open_dialog),
+ text = stringResource(AppString.open_dialog),
onClick = { onAction(DemoUiAction.OpenDialogClick) },
)
}
diff --git a/ui/feature-demo/src/commonMain/kotlin/com/velord/ui/feature/demo/DemoVM.kt b/ui/feature-demo/src/commonMain/kotlin/com/velord/ui/feature/demo/DemoVM.kt
index d37555a1..7a3de7aa 100644
--- a/ui/feature-demo/src/commonMain/kotlin/com/velord/ui/feature/demo/DemoVM.kt
+++ b/ui/feature-demo/src/commonMain/kotlin/com/velord/ui/feature/demo/DemoVM.kt
@@ -1,15 +1,13 @@
package com.velord.ui.feature.demo
-import com.velord.core.resource.Res
-import com.velord.core.resource.this_demo_is_deprecated
+import com.velord.core.resource.AppString
import com.velord.infrastructure.config.BuildConfigResolver
import com.velord.model.ToastConfig
import com.velord.model.ToastDuration
-import com.velord.ui.sharedviewmodel.CoroutineScopeVM
+import com.velord.ui.sharedviewmodel.LocalizationVM
import com.velord.usecase.event.ShowToastUC
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.launch
-import org.jetbrains.compose.resources.getString
sealed interface DemoUiAction {
data object OpenShapeClick : DemoUiAction
@@ -24,7 +22,7 @@ sealed interface DemoUiAction {
class DemoVM(
private val buildConfigResolver: BuildConfigResolver,
private val showToastUC: ShowToastUC,
-) : CoroutineScopeVM() {
+) : LocalizationVM() {
val navigationEvent = MutableSharedFlow()
private val actionFlow = MutableSharedFlow()
@@ -69,7 +67,7 @@ class DemoVM(
private suspend fun checkJetpackLib() {
val lib = buildConfigResolver.getNavigationLib()
if (lib.isJetpack) {
- val message = getString(Res.string.this_demo_is_deprecated, lib.name)
+ val message = getString(AppString.this_demo_is_deprecated, lib.name)
val toastConfig = ToastConfig(message = message, duration = ToastDuration.Long)
showToastUC(toastConfig)
}
diff --git a/ui/feature-flowsummator/src/commonMain/kotlin/com/velord/ui/feature/flowsummator/FlowSummatorScreen.kt b/ui/feature-flowsummator/src/commonMain/kotlin/com/velord/ui/feature/flowsummator/FlowSummatorScreen.kt
index 93be1c34..af3ead54 100644
--- a/ui/feature-flowsummator/src/commonMain/kotlin/com/velord/ui/feature/flowsummator/FlowSummatorScreen.kt
+++ b/ui/feature-flowsummator/src/commonMain/kotlin/com/velord/ui/feature/flowsummator/FlowSummatorScreen.kt
@@ -33,15 +33,11 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
-import com.velord.core.resource.Res
-import com.velord.core.resource.flow_summator
-import com.velord.core.resource.info_description_flow_summator
-import com.velord.core.resource.launch
-import com.velord.core.resource.please_enter_the_number
+import com.velord.core.resource.AppString
+import com.velord.core.resource.stringResource
import com.velord.core.ui.compose.component.PlatformScreenHeader
import com.velord.core.ui.compose.preview.PreviewCombined
import kotlinx.coroutines.launch
-import org.jetbrains.compose.resources.stringResource
@Composable
fun FlowSummatorScreen(
@@ -78,7 +74,7 @@ private fun Content(
horizontalAlignment = Alignment.CenterHorizontally
) {
PlatformScreenHeader(
- title = stringResource(Res.string.flow_summator),
+ title = stringResource(AppString.flow_summator),
onBackClick = onBackClick
)
Title()
@@ -99,7 +95,7 @@ private fun Content(
@Composable
private fun Title() {
Text(
- text = stringResource(Res.string.flow_summator),
+ text = stringResource(AppString.flow_summator),
modifier = Modifier.padding(top = 32.dp),
color = MaterialTheme.colorScheme.onSurface,
style = MaterialTheme.typography.bodyLarge,
@@ -116,7 +112,7 @@ private fun Start(
modifier = Modifier.padding(top = 32.dp),
enabled = isEnabled,
) {
- Text(text = stringResource(Res.string.launch))
+ Text(text = stringResource(AppString.launch))
}
}
@@ -131,7 +127,7 @@ private fun EnterField(
modifier = Modifier
.padding(top = 16.dp)
.padding(horizontal = 32.dp),
- placeholder = { Text(text = stringResource(Res.string.please_enter_the_number)) },
+ placeholder = { Text(text = stringResource(AppString.please_enter_the_number)) },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
)
}
@@ -168,7 +164,7 @@ private fun Info() {
contentWindowInsets = { WindowInsets(top = 0.dp) },
) {
Text(
- text = stringResource(Res.string.info_description_flow_summator),
+ text = stringResource(AppString.info_description_flow_summator),
modifier = Modifier
.padding(horizontal = 16.dp)
.padding(bottom = 8.dp)
diff --git a/ui/feature-movie/src/commonMain/kotlin/com/velord/ui/feature/movie/component/MovieAction.kt b/ui/feature-movie/src/commonMain/kotlin/com/velord/ui/feature/movie/component/MovieAction.kt
index 0f7a0c92..d192862b 100644
--- a/ui/feature-movie/src/commonMain/kotlin/com/velord/ui/feature/movie/component/MovieAction.kt
+++ b/ui/feature-movie/src/commonMain/kotlin/com/velord/ui/feature/movie/component/MovieAction.kt
@@ -19,11 +19,9 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
-import com.velord.core.resource.Res
-import com.velord.core.resource.filter_by
-import com.velord.core.resource.sort_by
+import com.velord.core.resource.AppString
+import com.velord.core.resource.stringResource
import com.velord.core.ui.compose.preview.PreviewCombined
-import org.jetbrains.compose.resources.stringResource
@Composable
internal fun BoxScope.MovieAction(
@@ -48,7 +46,7 @@ internal fun BoxScope.MovieAction(
ExtendedFloatingActionButton(
onClick = onSortClick,
text = {
- Text(stringResource(Res.string.sort_by))
+ Text(stringResource(AppString.sort_by))
},
icon = {
Icon(
@@ -61,7 +59,7 @@ internal fun BoxScope.MovieAction(
ExtendedFloatingActionButton(
onClick = onFilterClick,
text = {
- Text(stringResource(Res.string.filter_by))
+ Text(stringResource(AppString.filter_by))
},
icon = {
Icon(
diff --git a/ui/feature-movie/src/commonMain/kotlin/com/velord/ui/feature/movie/component/MovieBottomSheet.kt b/ui/feature-movie/src/commonMain/kotlin/com/velord/ui/feature/movie/component/MovieBottomSheet.kt
index 749530e8..cad708d9 100644
--- a/ui/feature-movie/src/commonMain/kotlin/com/velord/ui/feature/movie/component/MovieBottomSheet.kt
+++ b/ui/feature-movie/src/commonMain/kotlin/com/velord/ui/feature/movie/component/MovieBottomSheet.kt
@@ -34,10 +34,8 @@ import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.withLink
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
-import com.velord.core.resource.Res
-import com.velord.core.resource.info_description_movie
-import com.velord.core.resource.info_description_movie_url
-import com.velord.core.resource.range
+import com.velord.core.resource.AppString
+import com.velord.core.resource.stringResource
import com.velord.core.ui.compose.preview.PreviewCombined
import com.velord.model.movie.FilterType
import com.velord.model.movie.MovieFilterOption
@@ -47,7 +45,6 @@ import com.velord.ui.feature.movie.viewModel.MovieUiAction
import com.velord.ui.feature.movie.viewModel.MovieUiState
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
-import org.jetbrains.compose.resources.stringResource
@Composable
internal fun MovieBottomSheet(
@@ -207,7 +204,7 @@ private fun Filter(
Text(text = minStr)
val rangeStr = option.getRangeStr(sliderRangeState)
- Text(text = stringResource(Res.string.range, rangeStr))
+ Text(text = stringResource(AppString.range, rangeStr))
Text(text = maxStr)
}
@@ -228,7 +225,7 @@ private fun Info(
isShowing = isShowing,
onHide = { scope.hide(onHide) },
content = {
- val url = stringResource(Res.string.info_description_movie_url)
+ val url = stringResource(AppString.info_description_movie_url)
Text(
buildAnnotatedString {
withLink(
@@ -252,7 +249,7 @@ private fun Info(
modifier = Modifier.padding(16.dp)
)
Text(
- text = stringResource(Res.string.info_description_movie),
+ text = stringResource(AppString.info_description_movie),
modifier = Modifier
.padding(horizontal = 16.dp)
.padding(bottom = 8.dp)
diff --git a/ui/feature-movie/src/commonMain/kotlin/com/velord/ui/feature/movie/component/MovieHeader.kt b/ui/feature-movie/src/commonMain/kotlin/com/velord/ui/feature/movie/component/MovieHeader.kt
index 7db05144..ec85be25 100644
--- a/ui/feature-movie/src/commonMain/kotlin/com/velord/ui/feature/movie/component/MovieHeader.kt
+++ b/ui/feature-movie/src/commonMain/kotlin/com/velord/ui/feature/movie/component/MovieHeader.kt
@@ -27,9 +27,9 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
+import com.velord.core.resource.stringResource
import com.velord.core.ui.compose.preview.PreviewCombined
import com.velord.ui.feature.movie.model.MoviePage
-import org.jetbrains.compose.resources.stringResource
private const val HEADER_ANIMATION_DURATION = 700
diff --git a/ui/feature-movie/src/commonMain/kotlin/com/velord/ui/feature/movie/component/MoviePager.kt b/ui/feature-movie/src/commonMain/kotlin/com/velord/ui/feature/movie/component/MoviePager.kt
index 65bd5a26..afb34810 100644
--- a/ui/feature-movie/src/commonMain/kotlin/com/velord/ui/feature/movie/component/MoviePager.kt
+++ b/ui/feature-movie/src/commonMain/kotlin/com/velord/ui/feature/movie/component/MoviePager.kt
@@ -26,8 +26,8 @@ import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
-import com.velord.core.resource.Res
-import com.velord.core.resource.all_movies
+import com.velord.core.resource.AppString
+import com.velord.core.resource.stringResource
import com.velord.core.ui.compose.preview.PreviewCombined
import com.velord.model.movie.Movie
import com.velord.model.movie.SortType
@@ -36,7 +36,6 @@ import com.velord.ui.feature.movie.viewModel.AllMovieVM
import com.velord.ui.feature.movie.viewModel.FavoriteMovieUiAction
import com.velord.ui.feature.movie.viewModel.FavoriteMovieVM
import com.velord.ui.feature.movie.viewModel.MovieUiState
-import org.jetbrains.compose.resources.stringResource
import kotlin.time.Clock
@Composable
@@ -135,7 +134,7 @@ private fun RefreshPage(
if (isDataExausted) {
Box(modifier = Modifier.fillMaxWidth()) {
Text(
- text = stringResource(Res.string.all_movies),
+ text = stringResource(AppString.all_movies),
modifier = Modifier
.align(Alignment.Center)
.padding(16.dp)
diff --git a/ui/feature-movie/src/commonMain/kotlin/com/velord/ui/feature/movie/model/MovieFilterOptionUI.kt b/ui/feature-movie/src/commonMain/kotlin/com/velord/ui/feature/movie/model/MovieFilterOptionUI.kt
index 0fa173a2..e8c4ffc3 100644
--- a/ui/feature-movie/src/commonMain/kotlin/com/velord/ui/feature/movie/model/MovieFilterOptionUI.kt
+++ b/ui/feature-movie/src/commonMain/kotlin/com/velord/ui/feature/movie/model/MovieFilterOptionUI.kt
@@ -1,16 +1,14 @@
package com.velord.ui.feature.movie.model
import androidx.compose.runtime.State
-import com.velord.core.resource.Res
-import com.velord.core.resource.filter_by_rating
-import com.velord.core.resource.filter_by_vote_count
+import com.velord.core.resource.AppString
+import com.velord.core.resource.AppStringResource
import com.velord.model.movie.FilterType
import com.velord.model.movie.MovieFilterOption
-import org.jetbrains.compose.resources.StringResource
data class MovieFilterOptionUI(
val type: FilterType,
- val name: StringResource
+ val name: AppStringResource
) {
fun getMinMaxStr(
@@ -37,8 +35,8 @@ data class MovieFilterOptionUI(
fun fromDomain(option: MovieFilterOption): MovieFilterOptionUI = MovieFilterOptionUI(
type = option.type,
name = when (option.type) {
- is FilterType.Rating -> Res.string.filter_by_rating
- is FilterType.VoteCount -> Res.string.filter_by_vote_count
+ is FilterType.Rating -> AppString.filter_by_rating
+ is FilterType.VoteCount -> AppString.filter_by_vote_count
}
)
}
diff --git a/ui/feature-movie/src/commonMain/kotlin/com/velord/ui/feature/movie/model/MoviePage.kt b/ui/feature-movie/src/commonMain/kotlin/com/velord/ui/feature/movie/model/MoviePage.kt
index 258b2f49..0b90fbb2 100644
--- a/ui/feature-movie/src/commonMain/kotlin/com/velord/ui/feature/movie/model/MoviePage.kt
+++ b/ui/feature-movie/src/commonMain/kotlin/com/velord/ui/feature/movie/model/MoviePage.kt
@@ -4,21 +4,19 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Favorite
import androidx.compose.material.icons.rounded.List
import androidx.compose.ui.graphics.vector.ImageVector
-import com.velord.core.resource.Res
-import com.velord.core.resource.all
-import com.velord.core.resource.favorite
-import org.jetbrains.compose.resources.StringResource
+import com.velord.core.resource.AppString
+import com.velord.core.resource.AppStringResource
enum class MoviePage(
- val titleRes: StringResource,
+ val titleRes: AppStringResource,
val imageRes: ImageVector
) {
All(
- titleRes = Res.string.all,
+ titleRes = AppString.all,
imageRes = Icons.Rounded.List
),
Favorite(
- titleRes = Res.string.favorite,
+ titleRes = AppString.favorite,
imageRes = Icons.Rounded.Favorite
)
}
diff --git a/ui/feature-movie/src/commonMain/kotlin/com/velord/ui/feature/movie/model/MovieSortOptionUI.kt b/ui/feature-movie/src/commonMain/kotlin/com/velord/ui/feature/movie/model/MovieSortOptionUI.kt
index 72433766..5fd3b683 100644
--- a/ui/feature-movie/src/commonMain/kotlin/com/velord/ui/feature/movie/model/MovieSortOptionUI.kt
+++ b/ui/feature-movie/src/commonMain/kotlin/com/velord/ui/feature/movie/model/MovieSortOptionUI.kt
@@ -1,16 +1,14 @@
package com.velord.ui.feature.movie.model
-import com.velord.core.resource.Res
-import com.velord.core.resource.sort_by_date_ascending
-import com.velord.core.resource.sort_by_date_descending
+import com.velord.core.resource.AppString
+import com.velord.core.resource.AppStringResource
import com.velord.model.movie.MovieSortOption
import com.velord.model.movie.SortType
-import org.jetbrains.compose.resources.StringResource
data class MovieSortOptionUI(
val type: SortType,
val isSelected: Boolean,
- val name: StringResource
+ val name: AppStringResource
) {
fun toDomain(): MovieSortOption = MovieSortOption(
@@ -23,8 +21,8 @@ data class MovieSortOptionUI(
type = option.type,
isSelected = option.isSelected,
name = when (option.type) {
- SortType.DateAscending -> Res.string.sort_by_date_ascending
- SortType.DateDescending -> Res.string.sort_by_date_descending
+ SortType.DateAscending -> AppString.sort_by_date_ascending
+ SortType.DateDescending -> AppString.sort_by_date_descending
}
)
}
diff --git a/ui/feature-setting/build.gradle.kts b/ui/feature-setting/build.gradle.kts
index a919c22d..6fe4a7c7 100644
--- a/ui/feature-setting/build.gradle.kts
+++ b/ui/feature-setting/build.gradle.kts
@@ -1,7 +1,7 @@
plugins {
alias(libs.plugins.convention.multiplatform.library)
- alias(libs.plugins.multiplatform.compose)
alias(libs.plugins.kotlin.compose)
+ alias(libs.plugins.multiplatform.compose)
}
kotlin {
diff --git a/ui/feature-setting/src/androidMain/kotlin/com/velord/ui/feature/setting/SettingFragment.kt b/ui/feature-setting/src/androidMain/kotlin/com/velord/ui/feature/setting/SettingFragment.kt
index 0b010eb4..a5fbb317 100644
--- a/ui/feature-setting/src/androidMain/kotlin/com/velord/ui/feature/setting/SettingFragment.kt
+++ b/ui/feature-setting/src/androidMain/kotlin/com/velord/ui/feature/setting/SettingFragment.kt
@@ -12,12 +12,14 @@ import com.velord.core.ui.util.setContentWithTheme
import com.velord.ui.feature.bottomnavigation.screen.addTestCallback
import com.velord.ui.feature.bottomnavigation.viewmodel.BottomNavigationJetpackUiAction
import com.velord.ui.feature.bottomnavigation.viewmodel.BottomNavigationJetpackVM
+import com.velord.ui.sharedviewmodel.LanguageVM
import com.velord.ui.sharedviewmodel.ThemeVM
import org.koin.androidx.viewmodel.ext.android.viewModel
class SettingFragment : Fragment() {
- private val viewModel by activityViewModels()
+ private val themeViewModel by activityViewModels()
+ private val languageViewModel by viewModel()
private val viewModelBottom by viewModel()
override fun onCreateView(
@@ -26,7 +28,8 @@ class SettingFragment : Fragment() {
savedInstanceState: Bundle?
): View = setContentWithTheme {
SettingScreen(
- viewModel = viewModel,
+ themeViewModel = themeViewModel,
+ languageViewModel = languageViewModel,
onGraphCompleted = {
viewModelBottom.onAction(BottomNavigationJetpackUiAction.GraphCompletedHandling)
},
diff --git a/ui/feature-setting/src/commonMain/kotlin/com/velord/ui/feature/setting/SettingScreen.kt b/ui/feature-setting/src/commonMain/kotlin/com/velord/ui/feature/setting/SettingScreen.kt
index efe6ec9f..f710f76b 100644
--- a/ui/feature-setting/src/commonMain/kotlin/com/velord/ui/feature/setting/SettingScreen.kt
+++ b/ui/feature-setting/src/commonMain/kotlin/com/velord/ui/feature/setting/SettingScreen.kt
@@ -6,58 +6,69 @@ import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.selection.selectable
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.RadioButton
import androidx.compose.material3.Surface
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp
-import com.velord.core.resource.Res
-import com.velord.core.resource.abide_to_os_theme
-import com.velord.core.resource.disable_os_theme_switcher_first
-import com.velord.core.resource.not_available_on_android_11
-import com.velord.core.resource.os_does_not_support_theme_switching
-import com.velord.core.resource.settings
-import com.velord.core.resource.use_dark_theme
-import com.velord.core.resource.use_system_dynamic_theme
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.velord.core.resource.AppString
+import com.velord.core.resource.stringResource
import com.velord.core.ui.compose.component.PlatformScreenHeader
import com.velord.core.ui.compose.preview.PreviewCombined
+import com.velord.core.ui.compose.preview.previewLocalizationState
import com.velord.core.ui.util.LocalTheme
+import com.velord.model.setting.LanguagePreference
+import com.velord.ui.sharedviewmodel.LanguageUiAction
+import com.velord.ui.sharedviewmodel.LanguageUiState
+import com.velord.ui.sharedviewmodel.LanguageVM
import com.velord.ui.sharedviewmodel.ThemeUiAction
import com.velord.ui.sharedviewmodel.ThemeVM
-import org.jetbrains.compose.resources.stringResource
@Composable
fun SettingScreen(
- viewModel: ThemeVM,
+ themeViewModel: ThemeVM,
+ languageViewModel: LanguageVM,
onGraphCompleted: () -> Unit,
onBackClick: (() -> Unit),
) {
+ val languageUiState by languageViewModel.uiStateFlow.collectAsStateWithLifecycle()
+
SideEffect {
// Simulate we completed back stack handling
onGraphCompleted()
}
Content(
- onThemeAction = viewModel::onAction,
+ languageUiState = languageUiState,
+ onThemeAction = themeViewModel::onAction,
+ onLanguageAction = languageViewModel::onAction,
onBackClick = onBackClick,
)
}
@Composable
internal fun Content(
+ languageUiState: LanguageUiState,
onThemeAction: (ThemeUiAction) -> Unit,
- onBackClick: (() -> Unit)? = null,
+ onLanguageAction: (LanguageUiAction) -> Unit,
+ onBackClick: (() -> Unit),
) {
Surface {
Column(
@@ -67,39 +78,119 @@ internal fun Content(
.verticalScroll(rememberScrollState()),
) {
PlatformScreenHeader(
- title = stringResource(Res.string.settings),
+ title = stringResource(AppString.settings),
onBackClick = onBackClick,
)
- Column(
- modifier = Modifier
- .fillMaxWidth()
- .padding(top = 16.dp)
- .padding(horizontal = 8.dp)
- .clip(MaterialTheme.shapes.medium)
- .background(MaterialTheme.colorScheme.primaryContainer)
- .padding(16.dp),
- verticalArrangement = Arrangement.spacedBy(8.dp)
- ) {
- AbideToOsSwitcher(onThemeAction)
- DynamicThemeSwitcher(onThemeAction)
- DarkThemeSwitcher(onThemeAction)
- }
+ ThemeSettings(onThemeAction)
+ LanguageSettings(
+ languageUiState = languageUiState,
+ onLanguageAction = onLanguageAction,
+ )
}
}
}
+@Composable
+private fun ThemeSettings(onThemeAction: (ThemeUiAction) -> Unit) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(top = 16.dp)
+ .padding(horizontal = 8.dp)
+ .clip(MaterialTheme.shapes.medium)
+ .background(MaterialTheme.colorScheme.primaryContainer)
+ .padding(16.dp),
+ verticalArrangement = Arrangement.spacedBy(8.dp),
+ ) {
+ AbideToOsSwitcher(onThemeAction)
+ DynamicThemeSwitcher(onThemeAction)
+ DarkThemeSwitcher(onThemeAction)
+ }
+}
+
+@Composable
+private fun LanguageSettings(
+ languageUiState: LanguageUiState,
+ onLanguageAction: (LanguageUiAction) -> Unit,
+) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(top = 16.dp)
+ .padding(horizontal = 8.dp)
+ .clip(MaterialTheme.shapes.medium)
+ .background(MaterialTheme.colorScheme.primaryContainer)
+ .padding(16.dp),
+ verticalArrangement = Arrangement.spacedBy(8.dp),
+ ) {
+ Text(
+ text = stringResource(AppString.language),
+ style = MaterialTheme.typography.titleMedium,
+ )
+
+ LanguageOption(
+ title = stringResource(AppString.language_default),
+ isSelected = languageUiState.preference.isDefault,
+ onClick = {
+ onLanguageAction(LanguageUiAction.Select(LanguagePreference.DEFAULT))
+ },
+ )
+
+ LanguagePreference.entries
+ .filterNot { it.isDefault }
+ .filter { it.languageCode in languageUiState.localization.document.languageRoster }
+ .forEach { preference ->
+ LanguageOption(
+ title = stringResource(
+ resource = AppString.language_name,
+ language = preference.languageCode,
+ ),
+ isSelected = languageUiState.preference == preference,
+ onClick = {
+ onLanguageAction(LanguageUiAction.Select(preference))
+ },
+ )
+ }
+ }
+}
+
+@Composable
+private fun LanguageOption(
+ title: String,
+ isSelected: Boolean,
+ onClick: () -> Unit,
+) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .selectable(
+ selected = isSelected,
+ onClick = onClick,
+ )
+ .padding(vertical = 4.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
+ ) {
+ RadioButton(
+ selected = isSelected,
+ onClick = null,
+ )
+ Text(text = title)
+ }
+}
+
@Composable
private fun AbideToOsSwitcher(onThemeAction: (ThemeUiAction) -> Unit) {
val themeSwitcher = LocalTheme.current
val isEnabled = themeSwitcher.isSystemOsSwitchAvailable
ThemeSwitcher(
- title = stringResource(Res.string.abide_to_os_theme),
+ title = stringResource(AppString.abide_to_os_theme),
isChecked = themeSwitcher.config.abideToOs,
isEnabled = isEnabled,
- textWhenNotEnabled = stringResource(Res.string.os_does_not_support_theme_switching),
- onChange = { onThemeAction(ThemeUiAction.AbideToOsThemeSwitch) }
+ textWhenNotEnabled = stringResource(AppString.os_does_not_support_theme_switching),
+ onChange = { onThemeAction(ThemeUiAction.AbideToOsThemeSwitch) },
)
}
@@ -110,9 +201,9 @@ private fun DynamicThemeSwitcher(onThemeAction: (ThemeUiAction) -> Unit) {
val themeSwitcher = LocalTheme.current
val disabledText = StringBuilder()
- val disableOsStr = stringResource(Res.string.disable_os_theme_switcher_first)
+ val disableOsStr = stringResource(AppString.disable_os_theme_switcher_first)
if (themeSwitcher.isSystemDynamicColorAvailable.not()) {
- val android11Str = stringResource(Res.string.not_available_on_android_11)
+ val android11Str = stringResource(AppString.not_available_on_android_11)
disabledText.append(android11Str)
}
if (themeSwitcher.config.abideToOs) {
@@ -123,12 +214,12 @@ private fun DynamicThemeSwitcher(onThemeAction: (ThemeUiAction) -> Unit) {
val isNotAbideToOs = themeSwitcher.config.abideToOs.not()
val isEnabled = isDynamicColorAvailable && isNotAbideToOs
ThemeSwitcher(
- title = stringResource(Res.string.use_system_dynamic_theme),
+ title = stringResource(AppString.use_system_dynamic_theme),
isChecked = themeSwitcher.config.useDynamicColor,
isEnabled = isEnabled,
textWhenNotEnabled = disabledText.toString(),
modifier = Modifier.padding(top = 8.dp),
- onChange = { onThemeAction(ThemeUiAction.DynamicThemeSwitch) }
+ onChange = { onThemeAction(ThemeUiAction.DynamicThemeSwitch) },
)
}
@@ -142,12 +233,12 @@ private fun DarkThemeSwitcher(onThemeAction: (ThemeUiAction) -> Unit) {
val isNotAbideToOs = themeSwitcher.config.abideToOs.not()
val isEnabled = isNotAvailableSystemOsSwitch || isNotAbideToOs
ThemeSwitcher(
- title = stringResource(Res.string.use_dark_theme),
+ title = stringResource(AppString.use_dark_theme),
isChecked = themeSwitcher.config.useDarkTheme,
isEnabled = isEnabled,
- textWhenNotEnabled = stringResource(Res.string.disable_os_theme_switcher_first),
+ textWhenNotEnabled = stringResource(AppString.disable_os_theme_switcher_first),
modifier = Modifier.padding(top = 8.dp),
- onChange = { onThemeAction(ThemeUiAction.DarkThemeSwitch) }
+ onChange = { onThemeAction(ThemeUiAction.DarkThemeSwitch) },
)
}
@@ -158,24 +249,26 @@ private fun ThemeSwitcher(
isEnabled: Boolean,
textWhenNotEnabled: String,
modifier: Modifier = Modifier,
- onChange: () -> Unit
+ onChange: () -> Unit,
) {
Column(
modifier = modifier,
- verticalArrangement = Arrangement.spacedBy(4.dp)
+ verticalArrangement = Arrangement.spacedBy(4.dp),
) {
Text(text = title)
AnimatedVisibility(visible = isEnabled.not()) {
Text(
text = textWhenNotEnabled,
- modifier = Modifier.animateContentSize(animationSpec = tween(durationMillis = 800)),
+ modifier = Modifier.animateContentSize(
+ animationSpec = tween(durationMillis = 800),
+ ),
color = MaterialTheme.colorScheme.error,
)
}
Switch(
checked = isChecked,
onCheckedChange = { onChange() },
- enabled = isEnabled
+ enabled = isEnabled,
)
}
}
@@ -185,12 +278,20 @@ private fun SwitcherDivider() {
HorizontalDivider(
modifier = Modifier.clip(MaterialTheme.shapes.large),
thickness = 2.5.dp,
- color =MaterialTheme.colorScheme.onSurfaceVariant
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
@PreviewCombined
@Composable
private fun Preview() {
- Content(onThemeAction = {})
+ Content(
+ languageUiState = LanguageUiState(
+ localization = previewLocalizationState,
+ preference = LanguagePreference.DEFAULT,
+ ),
+ onThemeAction = {},
+ onLanguageAction = {},
+ onBackClick = {},
+ )
}
diff --git a/ui/feature-splash/build.gradle.kts b/ui/feature-splash/build.gradle.kts
index fa99f2a2..b2deccb2 100644
--- a/ui/feature-splash/build.gradle.kts
+++ b/ui/feature-splash/build.gradle.kts
@@ -13,7 +13,6 @@ kotlin {
commonMain.dependencies {
// Module
implementation(projects.model)
- implementation(projects.core.coreResource)
implementation(projects.core.coreUi)
implementation(projects.ui.sharedviewmodel)
implementation(libs.kotlin.coroutine.core)
diff --git a/ui/sharedviewmodel/build.gradle.kts b/ui/sharedviewmodel/build.gradle.kts
index 7f775f83..3ff52d70 100644
--- a/ui/sharedviewmodel/build.gradle.kts
+++ b/ui/sharedviewmodel/build.gradle.kts
@@ -11,17 +11,20 @@ kotlin {
commonMain.dependencies {
// Module
api(projects.model)
+ api(projects.core.coreResource)
+ // Module Infrastructure
api(projects.infrastructure.config)
api(projects.infrastructure.util)
+ // Module Domain
api(projects.domain.usecaseEvent)
api(projects.domain.usecaseSetting)
-
// Template
implementation(libs.bundles.kotlin.core)
// Lib
api(libs.kotlin.coroutine.core)
api(libs.androidx.lifecycle.viewmodel.core)
implementation(libs.kermit)
+ implementation(libs.koin.core)
}
}
}
diff --git a/ui/sharedviewmodel/src/commonMain/kotlin/com/velord/ui/sharedviewmodel/LanguageVM.kt b/ui/sharedviewmodel/src/commonMain/kotlin/com/velord/ui/sharedviewmodel/LanguageVM.kt
new file mode 100644
index 00000000..a04eef30
--- /dev/null
+++ b/ui/sharedviewmodel/src/commonMain/kotlin/com/velord/ui/sharedviewmodel/LanguageVM.kt
@@ -0,0 +1,73 @@
+package com.velord.ui.sharedviewmodel
+
+import com.velord.model.localization.LocalizationState
+import com.velord.model.setting.LanguagePreference
+import com.velord.usecase.setting.GetLanguagePreferenceUC
+import com.velord.usecase.setting.SetLanguagePreferenceUC
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.launch
+
+data class LanguageUiState(
+ val localization: LocalizationState,
+ val preference: LanguagePreference,
+)
+
+sealed interface LanguageUiAction {
+ data class Select(val preference: LanguagePreference) : LanguageUiAction
+}
+
+class LanguageVM(
+ private val getLanguagePreferenceUC: GetLanguagePreferenceUC,
+ private val setLanguagePreferenceUC: SetLanguagePreferenceUC,
+) : LocalizationVM() {
+
+ val uiStateFlow = MutableStateFlow(
+ LanguageUiState(
+ localization = localizationStateFlow.value,
+ preference = LanguagePreference.DEFAULT,
+ ),
+ )
+ private val actionFlow = MutableSharedFlow()
+
+ init {
+ observe()
+ }
+
+ fun onAction(action: LanguageUiAction) {
+ launch {
+ actionFlow.emit(action)
+ }
+ }
+
+ private fun onSelect(preference: LanguagePreference) = launch {
+ setLanguagePreferenceUC(preference)
+ }
+
+ private fun observe() {
+ launch {
+ val languagePreferenceFlow = getLanguagePreferenceUC()
+ val combinedUiStateFlow = combine(
+ localizationStateFlow,
+ languagePreferenceFlow,
+ ) { localization, preference ->
+ LanguageUiState(
+ localization = localization,
+ preference = preference,
+ )
+ }
+
+ combinedUiStateFlow.collect { uiState ->
+ uiStateFlow.value = uiState
+ }
+ }
+ launch {
+ actionFlow.collect { action ->
+ when (action) {
+ is LanguageUiAction.Select -> onSelect(action.preference)
+ }
+ }
+ }
+ }
+}
diff --git a/ui/sharedviewmodel/src/commonMain/kotlin/com/velord/ui/sharedviewmodel/LocalizationVM.kt b/ui/sharedviewmodel/src/commonMain/kotlin/com/velord/ui/sharedviewmodel/LocalizationVM.kt
new file mode 100644
index 00000000..e5854386
--- /dev/null
+++ b/ui/sharedviewmodel/src/commonMain/kotlin/com/velord/ui/sharedviewmodel/LocalizationVM.kt
@@ -0,0 +1,17 @@
+package com.velord.ui.sharedviewmodel
+
+import com.velord.core.resource.AppStringResource
+import com.velord.usecase.setting.GetLocalizationStateUC
+import org.koin.core.context.GlobalContext
+import com.velord.core.resource.getString as resolveString
+
+@UiContractExempt(UiContractExemptionReason.Base)
+open class LocalizationVM(
+ getLocalizationStateUC: Lazy = lazy { GlobalContext.get().get() }
+) : CoroutineScopeVM() {
+
+ val localizationStateFlow by lazy { getLocalizationStateUC.value() }
+
+ protected fun getString(resource: AppStringResource, vararg formatArgs: Any): String =
+ resolveString(localizationStateFlow.value, resource, *formatArgs)
+}
diff --git a/ui/widget-counter/build.gradle.kts b/ui/widget-counter/build.gradle.kts
index 65a27a5f..508f6c50 100644
--- a/ui/widget-counter/build.gradle.kts
+++ b/ui/widget-counter/build.gradle.kts
@@ -5,3 +5,12 @@ plugins {
android {
namespace = "com.velord.ui.widget.counter"
}
+
+dependencies {
+ // Module Core
+ implementation(projects.core.coreResource)
+ // Module Domain
+ implementation(projects.domain.usecaseSetting)
+ // Koin
+ implementation(libs.koin.core)
+}
diff --git a/ui/widget-counter/src/main/kotlin/com/velord/ui/widget/counter/CounterWidget.kt b/ui/widget-counter/src/main/kotlin/com/velord/ui/widget/counter/CounterWidget.kt
index 94894ee4..0326ee72 100644
--- a/ui/widget-counter/src/main/kotlin/com/velord/ui/widget/counter/CounterWidget.kt
+++ b/ui/widget-counter/src/main/kotlin/com/velord/ui/widget/counter/CounterWidget.kt
@@ -1,6 +1,7 @@
package com.velord.ui.widget.counter
import android.content.Context
+import androidx.compose.runtime.CompositionLocalProvider
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.intPreferencesKey
@@ -14,7 +15,10 @@ import androidx.glance.appwidget.provideContent
import androidx.glance.appwidget.state.updateAppWidgetState
import androidx.glance.state.GlanceStateDefinition
import androidx.glance.state.PreferencesGlanceStateDefinition
+import com.velord.core.resource.LocalLocalizationState
import com.velord.core.ui.compose.glance.GlanceWidgetThemeSustainer
+import com.velord.usecase.setting.GetLocalizationStateUC
+import org.koin.core.context.GlobalContext
class CounterWidget :
GlanceAppWidget(errorUiLayout = R.layout.counter_widget_error_layout),
@@ -26,8 +30,18 @@ class CounterWidget :
override val name: Class = CounterWidget::class.java
override val useDarkThemePreferenceKey: Preferences.Key = CounterWidget.useDarkThemePreferenceKey
- override suspend fun provideGlance(context: Context, id: GlanceId) =
- provideContent { CounterWidgetScreen() }
+ override suspend fun provideGlance(context: Context, id: GlanceId) {
+ val localization = GlobalContext
+ .get()
+ .get()()
+ .value
+
+ provideContent {
+ CompositionLocalProvider(LocalLocalizationState provides localization) {
+ CounterWidgetScreen()
+ }
+ }
+ }
companion object {
internal val actionParameterKey = ActionParameters.Key("countWidgetKey")
diff --git a/ui/widget-counter/src/main/kotlin/com/velord/ui/widget/counter/CounterWidgetScreen.kt b/ui/widget-counter/src/main/kotlin/com/velord/ui/widget/counter/CounterWidgetScreen.kt
index 001bcb97..cadc5b12 100644
--- a/ui/widget-counter/src/main/kotlin/com/velord/ui/widget/counter/CounterWidgetScreen.kt
+++ b/ui/widget-counter/src/main/kotlin/com/velord/ui/widget/counter/CounterWidgetScreen.kt
@@ -27,13 +27,9 @@ import androidx.glance.text.FontWeight
import androidx.glance.text.Text
import androidx.glance.text.TextDecoration
import androidx.glance.text.TextStyle
-import com.velord.core.resource.Res
-import com.velord.core.resource.count
-import com.velord.core.resource.counter_widget
-import com.velord.core.resource.minus_sign
-import com.velord.core.resource.plus_sign
+import com.velord.core.resource.AppString
+import com.velord.core.resource.stringResource
import com.velord.core.ui.compose.glance.MainGlanceTheme
-import org.jetbrains.compose.resources.stringResource
private fun createAction(count: Int): Action = actionRunCallback(
parameters = actionParametersOf(CounterWidget.actionParameterKey to count)
@@ -60,7 +56,7 @@ private fun Content(count: Int) {
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
- text = stringResource(Res.string.counter_widget),
+ text = stringResource(AppString.counter_widget),
modifier = GlanceModifier.padding(top = 8.dp),
style = TextStyle(
color = GlanceTheme.colors.onSurface,
@@ -76,7 +72,7 @@ private fun Content(count: Int) {
@Composable
private fun Counter(count: Int) {
Text(
- text = stringResource(Res.string.count, count),
+ text = stringResource(AppString.count, count),
modifier = GlanceModifier.padding(top = 8.dp),
style = TextStyle(
textDecoration = TextDecoration.Underline,
@@ -93,7 +89,7 @@ private fun Counter(count: Int) {
verticalAlignment = Alignment.CenterVertically,
) {
Button(
- text = stringResource(Res.string.plus_sign),
+ text = stringResource(AppString.plus_sign),
modifier = GlanceModifier
.width(100.dp)
.background(GlanceTheme.colors.surfaceVariant)
@@ -102,7 +98,7 @@ private fun Counter(count: Int) {
)
Spacer(modifier = GlanceModifier.width(32.dp))
Button(
- text = stringResource(Res.string.minus_sign),
+ text = stringResource(AppString.minus_sign),
modifier = GlanceModifier
.width(100.dp)
.background(GlanceTheme.colors.surfaceVariant)
diff --git a/ui/widget-refreshableimage/build.gradle.kts b/ui/widget-refreshableimage/build.gradle.kts
index 7724a042..0b66f28d 100644
--- a/ui/widget-refreshableimage/build.gradle.kts
+++ b/ui/widget-refreshableimage/build.gradle.kts
@@ -8,8 +8,13 @@ android {
}
dependencies {
- // Coil
+ // Module Core
+ implementation(projects.core.coreResource)
+ // Module Domain
+ implementation(projects.domain.usecaseSetting)
+ // Template
implementation(libs.bundles.coil)
- // AndroidX
+ // Other
+ implementation(libs.koin.core)
implementation(libs.androidx.core.ktx)
}
diff --git a/ui/widget-refreshableimage/src/main/kotlin/com/velord/ui/widget/refreshableimage/RefreshableImageWidget.kt b/ui/widget-refreshableimage/src/main/kotlin/com/velord/ui/widget/refreshableimage/RefreshableImageWidget.kt
index 1c004eab..a38b7032 100644
--- a/ui/widget-refreshableimage/src/main/kotlin/com/velord/ui/widget/refreshableimage/RefreshableImageWidget.kt
+++ b/ui/widget-refreshableimage/src/main/kotlin/com/velord/ui/widget/refreshableimage/RefreshableImageWidget.kt
@@ -2,6 +2,7 @@ package com.velord.ui.widget.refreshableimage
import android.content.Context
import android.graphics.BitmapFactory
+import androidx.compose.runtime.CompositionLocalProvider
import androidx.core.net.toUri
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey
@@ -17,8 +18,11 @@ import androidx.glance.appwidget.state.updateAppWidgetState
import androidx.glance.appwidget.updateAll
import androidx.glance.state.GlanceStateDefinition
import androidx.glance.state.PreferencesGlanceStateDefinition
+import com.velord.core.resource.LocalLocalizationState
import com.velord.core.ui.compose.glance.GlanceWidgetThemeSustainer
import com.velord.ui.widget.refreshableimage.model.ImageParameter
+import com.velord.usecase.setting.GetLocalizationStateUC
+import org.koin.core.context.GlobalContext
class RefreshableImageWidget :
GlanceAppWidget(errorUiLayout = R.layout.refreshable_image_widget_error_layout),
@@ -36,7 +40,18 @@ class RefreshableImageWidget :
override suspend fun provideGlance(
context: Context,
id: GlanceId
- ) = provideContent { RefreshableImageWidgetScreen() }
+ ) {
+ val localization = GlobalContext
+ .get()
+ .get()()
+ .value
+
+ provideContent {
+ CompositionLocalProvider(LocalLocalizationState provides localization) {
+ RefreshableImageWidgetScreen()
+ }
+ }
+ }
override suspend fun onDelete(context: Context, glanceId: GlanceId) {
super.onDelete(context, glanceId)
diff --git a/ui/widget-refreshableimage/src/main/kotlin/com/velord/ui/widget/refreshableimage/RefreshableImageWidgetScreen.kt b/ui/widget-refreshableimage/src/main/kotlin/com/velord/ui/widget/refreshableimage/RefreshableImageWidgetScreen.kt
index 6f8c5d62..24ee30b8 100644
--- a/ui/widget-refreshableimage/src/main/kotlin/com/velord/ui/widget/refreshableimage/RefreshableImageWidgetScreen.kt
+++ b/ui/widget-refreshableimage/src/main/kotlin/com/velord/ui/widget/refreshableimage/RefreshableImageWidgetScreen.kt
@@ -33,15 +33,11 @@ import androidx.glance.text.Text
import androidx.glance.text.TextDecoration
import androidx.glance.text.TextStyle
import co.touchlab.kermit.Logger
-import com.velord.core.resource.Res
-import com.velord.core.resource.downloaded_from
-import com.velord.core.resource.image_widget
-import com.velord.core.resource.refresh
-import com.velord.core.resource.widget_size
+import com.velord.core.resource.AppString
+import com.velord.core.resource.stringResource
import com.velord.core.ui.compose.glance.MainGlanceTheme
import com.velord.ui.widget.refreshableimage.util.createImageParameter
import com.velord.ui.widget.refreshableimage.util.getImageFilePath
-import org.jetbrains.compose.resources.stringResource
import kotlin.math.roundToInt
// On emulator redundant compositions with wrong LocalSize.current ruin all flow
@@ -95,7 +91,7 @@ private fun Content(
@Composable
private fun Title() {
Text(
- text = stringResource(Res.string.image_widget),
+ text = stringResource(AppString.image_widget),
modifier = GlanceModifier.padding(top = 16.dp),
style = TextStyle(
color = GlanceTheme.colors.onBackground,
@@ -120,7 +116,7 @@ private fun CurrentSize(
verticalAlignment = Alignment.CenterVertically,
) {
Text(
- text = stringResource(Res.string.widget_size, size.width.value, size.height.value),
+ text = stringResource(AppString.widget_size, size.width.value, size.height.value),
modifier = GlanceModifier.padding(8.dp),
style = TextStyle(
color = GlanceTheme.colors.onBackground,
@@ -138,7 +134,7 @@ private fun CurrentSize(
horizontalAlignment = Alignment.Start,
) {
Text(
- text = stringResource(Res.string.downloaded_from, url),
+ text = stringResource(AppString.downloaded_from, url),
modifier = GlanceModifier.padding(8.dp),
style = TextStyle(
textDecoration = TextDecoration.Underline,
@@ -171,7 +167,7 @@ private fun Refresh(url: String, isDownloadingNewImage: Boolean) {
verticalAlignment = Alignment.CenterVertically,
) {
Text(
- text = stringResource(Res.string.refresh),
+ text = stringResource(AppString.refresh),
modifier = GlanceModifier.padding(horizontal = 8.dp),
style = TextStyle(
color = GlanceTheme.colors.onSecondaryContainer,