compose-multiplatform icon indicating copy to clipboard operation
compose-multiplatform copied to clipboard

Provide a way to programmatically change the local `ResourceEnvironment` (`*Qualifier`)

Open AzimMuradov opened this issue 1 year ago • 8 comments

I need to have an ability to programmatically switch between sets of resources (based on their qualifiers). I need this feature to provide my users a way for changing the app language/interface size/theme/etc.

Fortunately, it should be quite easy to implement. The only thing needed is to make LocalComposeEnvironment public and maybe add some utility functions (WithLanguage/WithRegion/...).

Basically, I want to be able to programmatically and locally change ResourceEnvironment:

internal data class ResourceEnvironment(
    val language: LanguageQualifier,
    val region: RegionQualifier,
    val theme: ThemeQualifier,
    val density: DensityQualifier
)

If it's ok as a feature, I'm more than happy to help!

AzimMuradov avatar Jan 29 '24 13:01 AzimMuradov

You can override the density via the CompositionLocalProvider

CompositionLocalProvider(
    LocalDensity provides Density(1.5f)
) {
    //compose
}

for the other environment values the Compose doesn't provide providers yet. this should be implemented in the Compose and the library will use it

terrakok avatar Jan 29 '24 15:01 terrakok

I just wanted to voice my support for this enhancement. This is particularly helpful for allowing users to select their language in an application. I'm not sure if Compose will ever implement support for this because Android has alternative methods of changing the language from inside the application via configurations and/or per-app language preferences. Those existing methods do not work for iOS or Desktop and configuring the application language in-app is more prevalent on Desktop vs Android/iOS.

At the very least it would be helpful to add a function that allows passing in a language qualifier or something similar. That would allow 3rd party libraries to wrap the resources library and work around this limitation.

fun stringResource(resource: StringResource, language: LanguageQualifier)

ryanmitchener avatar Mar 07 '24 17:03 ryanmitchener

Very Important for us!

juhaodong avatar Mar 26 '24 09:03 juhaodong

We have an existing ios app. which is created with custom localization handling. That does not fit into composed resources. case so need to feed local from out side

aniluMango avatar Mar 26 '24 10:03 aniluMango

Hi, I tried to use Locale.setDefault(Locale.forLanguageTag("tag")) and on Desktop works correctly, obviuosly using the composeResources api with the specific language tag for values folder (values-en, etc...) I don't know if this can work on mobile platforms or on web also

N7ghtm4r3 avatar Apr 11 '24 21:04 N7ghtm4r3

This would be great for the web too. Currently, the only way for users to change the language of Compose website is by changing their system's language.

ShreyashKore avatar Jun 02 '24 11:06 ShreyashKore

Hi, I'm also looking to be able to change resources programmatically or have multiple instances with different configurations, similar to @ryanmitchener.

I'm developing a fullstack solution in Kotlin and want to use the same i18n solution (StringResource handling) for both client and server.

However, I'm currently blocked because the ResourceEnvironment constructor is internal. Would it be possible to make it public? I can't use Locale.setDefault() as I'm handling multiple requests simultaneously, which would cause a race condition.

There might be some headless issues that shows up here as well...

afTrolle avatar Jun 09 '24 16:06 afTrolle

Currently, the stringResource function relies on Locale.current to determine the appropriate string translation. However, Locale.current always returns the default system locale and doesn't allow for dynamic changes within the app. This makes it impossible to switch languages without changing the system locale.

Here's how the current implementation works:

  1. stringResource calls rememberResourceState:

    @Composable
    fun stringResource(resource: StringResource): String {
        // ...
        val str by rememberResourceState(resource, { "" }) { env ->
            loadString(resource, resourceReader, env)
        }
        // ...
    }
    
  2. rememberResourceState depends on LocalComposeEnvironment:

    @Composable
    internal actual fun <T> rememberResourceState(
        // ...
    ): State<T> {
        val environment = LocalComposeEnvironment.current.rememberEnvironment()
        // ...
    }
    
  3. LocalComposeEnvironment delegates to DefaultComposeEnvironment, which uses Locale.current:

    internal val DefaultComposeEnvironment = object : ComposeEnvironment {
        @Composable
        override fun rememberEnvironment(): ResourceEnvironment {
            val composeLocale = Locale.current // <-- Problem: Always system locale
            // ...
        }
    }
    

Proposed Modification:

To allow dynamic locale changes, I propose the following changes to ComposeEnvironment and DefaultComposeEnvironment:

--- a/ComposeEnvironment.kt
+++ b/ComposeEnvironment.kt
@@ -1,19 +1,35 @@
 internal interface ComposeEnvironment {
     @Composable
     fun rememberEnvironment(): ResourceEnvironment
+
+    fun setLocale(
+        locale: Locale,
+    )
 }
 
 internal val DefaultComposeEnvironment = object : ComposeEnvironment {
+    private lateinit var environment: MutableState<ResourceEnvironment>
+
     @Composable
     override fun rememberEnvironment(): ResourceEnvironment {
+        if (::environment.isInitialized) {
+            return environment.value
+        }
         val composeLocale = Locale.current
         val composeTheme = isSystemInDarkTheme()
         val composeDensity = LocalDensity.current
 
-        //cache ResourceEnvironment unless compose environment is changed
-        return remember(composeLocale, composeTheme, composeDensity) {
-            ResourceEnvironment(
+        environment = remember(composeLocale, composeTheme, composeDensity) {
+            mutableStateOf(
+                ResourceEnvironment(
                     LanguageQualifier(composeLocale.language),
                     RegionQualifier(composeLocale.region),
                     ThemeQualifier.selectByValue(composeTheme),
                     DensityQualifier.selectByDensity(composeDensity.density)
-            )
+                )
+            )
         }
+        return environment.value
     }
+
+    // function to change the environment
+    override fun setLocale(
+        locale: Locale,
+    ) {
+        environment.value = ResourceEnvironment(
+            LanguageQualifier(locale.language),
+            RegionQualifier(locale.region),
+            environment.value.theme,
+            environment.value.density
+        )
+    }
 }
+

This modification introduces two key changes:

  • setLocale function: This function allows directly updating the locale used by rememberEnvironment, enabling dynamic language switching within the app.
  • Mutable environment: Instead of recreating ResourceEnvironment on every recomposition, we now use a MutableState to hold the environment. This allows us to modify the locale within the environment without triggering unnecessary recomposition of the entire composable tree.

I tested it using the resources demo (Desktop for now) with the following code:

@Composable
fun LocaleController() {
    var localeTextField by remember { mutableStateOf("ar") }
    val locale = LocalComposeEnvironment.current

    OutlinedTextField(
        modifier = Modifier.padding(16.dp).fillMaxWidth(),
        value = localeTextField,
        onValueChange = { localeTextField = it },
        label = { Text("Locale") },
        enabled = true,
        colors = TextFieldDefaults.colors(
            disabledTextColor = MaterialTheme.colorScheme.onSurface,
            disabledContainerColor = MaterialTheme.colorScheme.surface,
            disabledLabelColor = MaterialTheme.colorScheme.onSurface,
        )
    )

    OutlinedButton(
        onClick = {
            locale.setLocale(Locale(localeTextField))
        },
        modifier = Modifier.padding(16.dp)
    ) {
        Text("Change Locale")
    }
}

This code snippet provides a simple UI with a text field to input the desired locale code and a button to trigger the locale change using the new setLocale function.

Screencast from 2024-06-24 07-04-29.webm

ahmedhosnypro avatar Jun 24 '24 04:06 ahmedhosnypro