Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Strings for language tag not found #10

Closed
Skeletonxf opened this issue Oct 21, 2022 · 4 comments
Closed

Strings for language tag not found #10

Skeletonxf opened this issue Oct 21, 2022 · 4 comments
Labels
bug Something isn't working

Comments

@Skeletonxf
Copy link

Hi, I'm trying to use this library without the KSP generation and following the README for writing the code myself. However, following these instructions lead to an issue where the library doesn't fall back to the default I want if it can't find a match for the system language setting.

Minimal example:

package com.example.lyricisttest

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.intl.Locale
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import cafe.adriel.lyricist.LyricistStrings
import cafe.adriel.lyricist.ProvideStrings
import cafe.adriel.lyricist.rememberStrings
import com.example.lyricisttest.ui.theme.LyricistTestTheme

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            LyricistTestTheme {
                val lyricist = rememberStrings(strings/*, Locales.ENGLISH*/)
                ProvideStrings(lyricist, LocalStrings) {
                    Surface(
                        modifier = Modifier.fillMaxSize(),
                        color = MaterialTheme.colors.background
                    ) {
                        Box(modifier = Modifier.fillMaxSize()) {
                            Text(
                                text = LocalStrings.current.helloWorld,
                                modifier = Modifier.align(Alignment.Center),
                                fontSize = 32.sp,
                            )
                            Text(
                                text = Locale.current.toLanguageTag(),
                                modifier = Modifier
                                    .align(Alignment.BottomCenter)
                                    .padding(bottom = 40.dp),
                                fontSize = 24.sp,
                            )
                        }
                    }
                }
            }
        }
    }
}

data class Strings(
    val helloWorld: String,
)

object Locales {
    const val ENGLISH = "en"
    const val SPANISH = "es"
}

@LyricistStrings(languageTag = Locales.ENGLISH, default = true)
val english = Strings(
    helloWorld = "Hello world"
)

@LyricistStrings(languageTag = Locales.SPANISH)
val spanish = Strings(
    helloWorld = "Hola mundo"
)

val strings = mapOf(
    Locales.ENGLISH to english,
    Locales.SPANISH to spanish,
)

val LocalStrings = staticCompositionLocalOf { english }

I would expect this app to display "Hello world" if the locale is English, and "Hola mundo" if the locale is Spanish, and fallback to English if the locale is something else. As long as the locale is English or Spanish this seems to be the case:

Screenshot_1666357623
Screenshot_1666357637
Screenshot_1666357649
Screenshot_1666357660

All four US and UK English and Spanish combinations work as expected.

However, if I change the system language to something else, Portuguese, instead of falling back to English the app crashes.

2022-10-21 14:07:51.546 28520-28520/? E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.example.lyricisttest, PID: 28520
    java.lang.IllegalStateException: Strings for language tag pt-PT not found
        at cafe.adriel.lyricist.Lyricist.getStrings(Lyricist.kt:25)
        at cafe.adriel.lyricist.LyricistUtilsKt.ProvideStrings(LyricistUtils.kt:25)
        at com.example.lyricisttest.ComposableSingletons$MainActivityKt$lambda-3$1.invoke(MainActivity.kt:29)
        at com.example.lyricisttest.ComposableSingletons$MainActivityKt$lambda-3$1.invoke(MainActivity.kt:27)
        at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:107)
        at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:34)
        at androidx.compose.material.MaterialTheme_androidKt.PlatformMaterialTheme(MaterialTheme.android.kt:23)
        at androidx.compose.material.MaterialThemeKt$MaterialTheme$1$1.invoke(MaterialTheme.kt:82)
        at androidx.compose.material.MaterialThemeKt$MaterialTheme$1$1.invoke(MaterialTheme.kt:81)
        at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:107)
        at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:34)
        at androidx.compose.runtime.CompositionLocalKt.CompositionLocalProvider(CompositionLocal.kt:228)
        at androidx.compose.material.TextKt.ProvideTextStyle(Text.kt:265)
        at androidx.compose.material.MaterialThemeKt$MaterialTheme$1.invoke(MaterialTheme.kt:81)
        at androidx.compose.material.MaterialThemeKt$MaterialTheme$1.invoke(MaterialTheme.kt:80)
        at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:107)
        at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:34)
        at androidx.compose.runtime.CompositionLocalKt.CompositionLocalProvider(CompositionLocal.kt:228)
        at androidx.compose.material.MaterialThemeKt.MaterialTheme(MaterialTheme.kt:72)
        at com.example.lyricisttest.ui.theme.ThemeKt.LyricistTestTheme(Theme.kt:38)
        at com.example.lyricisttest.ComposableSingletons$MainActivityKt$lambda-4$1.invoke(MainActivity.kt:27)
        at com.example.lyricisttest.ComposableSingletons$MainActivityKt$lambda-4$1.invoke(MainActivity.kt:26)
        at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:107)
        at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:34)
        at androidx.compose.ui.platform.ComposeView.Content(ComposeView.android.kt:404)
        at androidx.compose.ui.platform.AbstractComposeView$ensureCompositionCreated$1.invoke(ComposeView.android.kt:250)
        at androidx.compose.ui.platform.AbstractComposeView$ensureCompositionCreated$1.invoke(ComposeView.android.kt:249)
        at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:107)
        at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:34)
        at androidx.compose.runtime.CompositionLocalKt.CompositionLocalProvider(CompositionLocal.kt:228)
        at androidx.compose.ui.platform.CompositionLocalsKt.ProvideCommonCompositionLocals(CompositionLocals.kt:177)
        at androidx.compose.ui.platform.AndroidCompositionLocals_androidKt$ProvideAndroidCompositionLocals$3.invoke(AndroidCompositionLocals.android.kt:123)
        at androidx.compose.ui.platform.AndroidCompositionLocals_androidKt$ProvideAndroidCompositionLocals$3.invoke(AndroidCompositionLocals.android.kt:122)
        at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:107)
        at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:34)
        at androidx.compose.runtime.CompositionLocalKt.CompositionLocalProvider(CompositionLocal.kt:228)
        at androidx.compose.ui.platform.AndroidCompositionLocals_androidKt.ProvideAndroidCompositionLocals(AndroidCompositionLocals.android.kt:114)
        at androidx.compose.ui.platform.WrappedComposition$setContent$1$1$3.invoke(Wrapper.android.kt:157)
        at androidx.compose.ui.platform.WrappedComposition$setContent$1$1$3.invoke(Wrapper.android.kt:156)
2022-10-21 14:07:51.547 28520-28520/? E/AndroidRuntime:     at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:107)
        at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:34)
        at androidx.compose.runtime.CompositionLocalKt.CompositionLocalProvider(CompositionLocal.kt:228)
        at androidx.compose.ui.platform.WrappedComposition$setContent$1$1.invoke(Wrapper.android.kt:156)
        at androidx.compose.ui.platform.WrappedComposition$setContent$1$1.invoke(Wrapper.android.kt:140)
        at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:107)
        at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:34)
        at androidx.compose.runtime.ActualJvm_jvmKt.invokeComposable(ActualJvm.jvm.kt:78)
        at androidx.compose.runtime.ComposerImpl$doCompose$2$5.invoke(Composer.kt:3248)
        at androidx.compose.runtime.ComposerImpl$doCompose$2$5.invoke(Composer.kt:3238)
        at androidx.compose.runtime.SnapshotStateKt__DerivedStateKt.observeDerivedStateRecalculations(DerivedState.kt:341)
        at androidx.compose.runtime.SnapshotStateKt.observeDerivedStateRecalculations(Unknown Source:1)
        at androidx.compose.runtime.ComposerImpl.doCompose(Composer.kt:3238)
        at androidx.compose.runtime.ComposerImpl.composeContent$runtime_release(Composer.kt:3173)
        at androidx.compose.runtime.CompositionImpl.composeContent(Composition.kt:587)
        at androidx.compose.runtime.Recomposer.composeInitial$runtime_release(Recomposer.kt:950)
        at androidx.compose.runtime.CompositionImpl.setContent(Composition.kt:519)
        at androidx.compose.ui.platform.WrappedComposition$setContent$1.invoke(Wrapper.android.kt:140)
        at androidx.compose.ui.platform.WrappedComposition$setContent$1.invoke(Wrapper.android.kt:131)
        at androidx.compose.ui.platform.AndroidComposeView.setOnViewTreeOwnersAvailable(AndroidComposeView.android.kt:1060)
        at androidx.compose.ui.platform.WrappedComposition.setContent(Wrapper.android.kt:131)
        at androidx.compose.ui.platform.WrappedComposition.onStateChanged(Wrapper.android.kt:182)
        at androidx.lifecycle.LifecycleRegistry$ObserverWithState.dispatchEvent(LifecycleRegistry.java:360)
        at androidx.lifecycle.LifecycleRegistry.addObserver(LifecycleRegistry.java:202)
        at androidx.compose.ui.platform.WrappedComposition$setContent$1.invoke(Wrapper.android.kt:138)
        at androidx.compose.ui.platform.WrappedComposition$setContent$1.invoke(Wrapper.android.kt:131)
        at androidx.compose.ui.platform.AndroidComposeView.onAttachedToWindow(AndroidComposeView.android.kt:1147)
        at android.view.View.dispatchAttachedToWindow(View.java:19553)
        at android.view.ViewGroup.dispatchAttachedToWindow(ViewGroup.java:3430)
        at android.view.ViewGroup.dispatchAttachedToWindow(ViewGroup.java:3437)
        at android.view.ViewGroup.dispatchAttachedToWindow(ViewGroup.java:3437)
        at android.view.ViewGroup.dispatchAttachedToWindow(ViewGroup.java:3437)
        at android.view.ViewGroup.dispatchAttachedToWindow(ViewGroup.java:3437)
        at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:2028)
        at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:1721)
        at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:7598)
        at android.view.Choreographer$CallbackRecord.run(Choreographer.java:966)
        at android.view.Choreographer.doCallbacks(Choreographer.java:790)
        at android.view.Choreographer.doFrame(Choreographer.java:725)
        at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:951)
        at android.os.Handler.handleCallback(Handler.java:883)
        at android.os.Handler.dispatchMessage(Handler.java:100)
        at android.os.Looper.loop(Looper.java:214)
        at android.app.ActivityThread.main(ActivityThread.java:7356)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:930)

I can see the default argument for languageTag in rememberStrings is Locale.current.toLanguageTag(), which gets passed into the instance of Lyricist as the defaultLanguageTag. If I override that argument as in the commented code in the example above, I can force the language to English, which prevents the crash but also stops the localisation working for Spanish.

@Knoxvillekm
Copy link

  1. Parameter languageTag in rememberStrings set two properties defaultLanguageTag and languageTag.
  2. Locale.current.toLanguageTag() - return different value for platforms, like "en-US" (Windows) and "en_US" (IOS).

Working example for you:

val lyricist = rememberStrings(
    translations = strings,
    languageTag = Locales.ENGLISH
)
lyricist.languageTag = Locale.current.language

@Skeletonxf
Copy link
Author

While that may appear to work it goes against the guidelines for Compose.

A side-effect is any change that is visible to the rest of your app. For example, these actions are all dangerous side-effects:

Writing to a property of a shared object
Updating an observable in ViewModel
Updating shared preferences

Composable functions might be re-executed as often as every frame, such as when an animation is being rendered.

Setting the languageTag in the composable function like that is a side effect that will potentially rerun on every recomposition of the function, and after using ProvideStrings to make the Lyricist class control the strings for the entire app through a composition local it will affect the rest of the app too, making it a write to a property of a shared object. In particular, if there was some UI for the user to press a button to change the language used by the app (which made a write to lyricist.languageTag), this snippet would introduce a bug, as it would override that setting back to the current Locale whenever the containing function recomposed.

@afTrolle
Copy link

When I was looking into it i found two issues when working with composeables.

  • Fallback locale-tag might be the language tag, easily done wrong.
  • Also the Locale might change at any time, in theory any recomposition could be with a new locale.

Hopefully this shouldn't trigger any unnecessary recomposition except when the locale changes.

@Composable
fun CustomProvideStrings(
    content: @Composable () -> Unit,
) {
    val locale: Locale = Locale.current
    val tag = remember(locale) { locale.toLanguageTag() }
    val lyricist = remember(tag) {
        Lyricist("en"/* Default Tag*/, Strings).apply {
            languageTag = tag
        }
    }
    ProvideStrings(lyricist, content)
}

@DevNatan DevNatan added the bug Something isn't working label Oct 1, 2023
@adrielcafe
Copy link
Owner

Fixed on 1.6.0. I've replaced the languageTag param with defaultLanguageTag and currentLanguageTag. It won't crash anymore and will select the default language tag correctly.

val lyricist = rememberStrings(
    defaultLanguageTag = "es-US", // Default value is the one annotated with @LyricistStrings(default = true)
    currentLanguageTag = getCurrentLanguageTagFromLocalStorage(),
)

Thanks everyone!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

5 participants