Good morning. In a journey to extinguish XML I div...
# android
c
Good morning. In a journey to extinguish XML I dived into an adventure of localizing my code using kotlin instead. Everything started with a curious Swift code that did something like "string".localize. This really excited me. But I will appreciate some feedback from Android seniors about taking downs this road and problems that will kick me in the butt too late to avoid doing a major refactor. Feedback will be much appreciated. For a World with more Kotlin.
👀 1
About how I'm tryiong to do it. I tried enums but I couldn't leverage interfaces. I didn't wanted to use classes but I feel like need to go down the interface+classes road. I wanted to do something with objects but I failed to use them as I wanted. Boiling dow to what I want.
Copy code
val language = Languages.DEFAULT

fun String.localize() =
    when (language) {
        Languages.ENGLISH -> EnglishStrings()
    }
Would be how I would like to fetch my translation where "language" will be user defined setting that will use the systems default. The supported languages will be an enum this being helpful because the the "when" in the localize parameter will remind you to implement new branches as you grown your supported languages.
Copy code
enum class Languages {
    DEFAULT,
    ENGLISH
}
And now I'm, working on finding the best way to organizer this strings. I really wanted to leverage this implementation to have distributed string files per context, walking away from monolitical approaches because they lose maintenance with time, force you into poor and long names. I'm listening to everything, mainly what will not work 🙂
a
we did a bunch of looking into this as part of the compose project since we were requiring kotlin as a baseline and ultimately backed away from it for now. The costs of making any changes to how string localization is done are incredibly high and any new benefits need to be worth those costs. There are entire business categories built around workflows that export strings.xml as part of contract work, etc.
👍 1
👍🏼 1
(Which isn't to say we're not still interested in doing something here over the next few years, just that it's a ship that turns very slowly and we can't really afford churn here.)
👍🏽 1
👍🏼 1
👍 1
with that as a backdrop, API challenges to solve:
Int
makes a lousy key. "Type safety" for R constants is provided today by Android Lint alone and it's not as complete as a real type checker. A new solution would ideally use something that participates in the language's type system. However, the performance profile of
Int
here is incredibly good. Constants get inlined by the compiler and prevent a lot of overhead. A new solution must not regress the performance profile of working with localized strings either.
`@JvmInline`/`value class` is something we're keeping a very close eye on in this regard.
You need a
Context
to load strings, full stop. That makes
String.localize()
into something more like
String.localize(Context)
at a minimum, which starts getting more cumbersome. Context receivers is also something we're watching for the ergonomics here; Compose doesn't need it since it has access to context from the composition in the form of CompositionLocals. I'd be very wary of adding something that is incredibly easy to load from
@Composable
functions but very hard to load outside, as it starts to encourage some really twisted and confusing data flow when folks aren't sure how to get from something like a ViewModel to some data they need. You really really need an
Activity
context for this and not the application context, since a lot of developers cobble together some custom locale picking systems within their apps.
Then there's the idea of using an inline
String
constant with a trailing extension call. It's attractive but problematic in what it implies, since the key needs to be repeated at both point of use and in the table of alternatives. That sort of arrangement is prone to going out of sync when a well-meaning developer makes a change to the inline constant in the source code and now the key is different. Now you need another system to prevent or resolve that. Or you use something closer to the explicit indirection of the
R.string.confirm_new_account
style. I think we'd need to see something quite impressive to override the simple advantages of explicit keys like that
(What happens when the string is something very long, like a privacy policy or even just a tutorial explanation telling the user how to use a part of the app?)
👌🏽 1
a
@Cicero please take a look at https://github.com/adrielcafe/lyricist With Lyricist you can use a data class to declare your strings, and each instance of it represents a locale. It supports multi modules and uses KSP for basic code gen. It is built for Compose but the same idea should work with pure Kotlin. Hope it helps o/
👌🏽 1
c
I share most of the concerns you shared Mr. Powell. Sizable strings can be pretty unattractive. But there are ways to solve this using this strings as identifier. Problem is that then I end up in the same place with monolithical translation files. I'm thankful for your suggestion Mr. Café but I honestly can't have a dependency that is part of the core of my application. I already got a point where I don't need to handle with contexts and I can have multiple translations and support maintaining this changes trough interfaces. But it's ugly. I will take all this into consideration and explore how I can implement this.
👍 1
a
it's not that there aren't ways to solve it with strings as an identifier, it's that using inline string literals that have to match somewhere else is intrinsically error-prone. They aren't compile-time verified without significant additional static analysis and doing that analysis fully/correctly is nontrivial, to put it lightly. That leaves them throwing runtime exceptions or otherwise failing to work at runtime and using some other fallback behavior. If you intend to solve that part of the problem then you have to verify constants declared elsewhere and only refer to those constants inline in the source, as opposed to literals. At that point whether the backing key in memory is a string or an int or an opaque value class is kind of irrelevant, the usage ergonomics are similar.
1
and then there's the element of separate strings files being both inputs to and outputs from the separate business process of translating an app
c
I will update this as soon as I have time. I see your considerations Adam. What I see that would be required for a high quality solution would be tooling. I see the need of annotations and automatic file exports. In a way that it would compile all the specific languages together and generate this big concatenated file in a way that it could be consumed back by this tooling. Oh yea, writing this line sounded like one of the most unreasonable things I've ever wrote, like it's simple. I also don't see this string as identifier approach as a good solution, it was just the thing that got me into my "aha" moment. I would use variables and the way I've been looking into, I want something like ScreenOneStrings.TextFieldOneLabel. Problem number two is that if you do it the way I'm describing you end up indexing ALL possible translations and this would be a loss but I honestly wouldn't even account this comparing with the gains of organization. This is something I'm doing for fun. And I will take a look into your library @Adriel Café
One thing I see in your approach Adriel that I'm looking to use in mine is using interfaces to guarantee string localization implementation and double down in the organizational structure. But I see I can learn a lot of what you did, maybe even fork and play with your project
a
Interfaces instead of data classes works fine too. You can even create nested classes/interfaces to group strings by feature:
Copy code
interface Strings {
  val featureA: FeatureAStrings
  val featureB: FeatureBStrings 
}

strings.featureA.someText
c
on the top level now I have a system var called language which receives my AvailableLanguage.Default, this guys is an enum of languages. I'm using this with a general class for returning the respective screen strings which will chose from the available implementations, I'm using a when now based on this AvailableLanguage enum. Still feels hacky but I like how individual and specific my translation and string localization files are. I would be happy if I could have a different place for the extra language options and the english ones (or whatever development language of choice you have), hence the annotations, because then you could just drop this files wherever you want. But again, interfaces just to enforce that all new language implementations will have the expected fields and you can just do this automatically using the IDE. Which is cool.
I have a feeling this can be "easily" (as in the technology to solve exists and is accessible) tackled/done, it's just that it's not a real problem, it's just convenience that I see as a problem when you have projects with more than 200/300 strings with 4 or more language locals.
I wrote this abomination but it got my head scratching:
Copy code
enum class Languages {
    English,
    Portuguese,
    Spanish;
}

val systemLanguage = Languages.English.name

interface SupportedLanguages {
    val English: String
    val Portuguese: String
    val Spanish: String
    fun getLocalizedString() = when (systemLanguage) {
        this.English.javaClass.name -> English
        this.Portuguese.javaClass.name -> Portuguese
        this.Spanish.javaClass.name -> Spanish
        else -> "Unsoported language"
    }
}


sealed class LoginStrings {
    class Login(
        override val English: String = "Login",
        override val Portuguese: String = "Logar",
        override val Spanish: String = "Logar"
    ) : SupportedLanguages

    class LoginButtonLabel(): SupportedLanguages {
        override val English: String
            get() = TODO("Not yet implemented")
        override val Portuguese: String
            get() = TODO("Not yet implemented")
        override val Spanish: String
            get() = TODO("Not yet implemented")
    }
}

LoginStrings.Login().getLocalizedString()
It's far from practical but it does what I'm interested. Any tips on any approaches Google took on the matter?