I'm looking at i18n translations strategies, just ...
# javascript
s
I'm looking at i18n translations strategies, just wondering what everyone is using?
b
Have a look at how it's done in #kvision. Might give you some ideas. cc: @Robert Jaros
s
I've had a look at @Robert Jaros one and it's a clean abstraction. Was going to do something similar but I'd love to give it multiplatform capabilities so something similar might work into a solution.
b
Hmm, either port js libs to common kotlin or expect actual into alternatives on other platforms
You might also want to wait until #ksp is multiplatform (should be weeks after 1.5 is out) so you could also use annotation processor for this.
s
Or build a simple native multiplat imp from scratch. Wrapping something can be a PITA in the future and will require support to maintain. I think I'll do that. I'll write the native base and play with it. Then when I'm happy I can make use of #ksp and flesh out everything more.
👍 1
Feed the translations into it so I don't need to deal with fetching and storing for now. Just use it as the manager
Cheers! 🙂👍
r
There are two things to consider. One thing are the translations. In my opinion gettext format is really good and has many advantages. Reinventing the wheel is not a good idea.
The other thing is how to use these translations. KVision uses gettext.js because is purely Kotlin/JS.
But I would be very happy to have pure Kotlin solution for this :-)
K 1
s
Yep! Something along the lines of what you have. I was going to have a default object with a map that gets initialised with the default language. Then inject in other locales for those default keys. That should work across other platforms too.
t
Personally I prefer to have a message interface:
Copy code
fun interface Message<T> {
    suspend operator fun get(languageQualifier: LanguageQualifier) : T
}
and a translation interface:
Copy code
interface Translation {
    val hello : String get() = "Hello"
    // ...
}
and for regular messages I can just delegate to Translation interface
{lq -> translation(lq).hello}
NOTE: If you have modular app, you may use
Trasnslation
interface and
translation
function per module. If you want to load translations from server, you can may
Translation
implementations
@Serializable
and
translation
function
suspend
. You can also have some
Message
implementations serializable, but I haven't yet encountered a use case where I would recommend it. In future I will be looking to support my solution with code generation.
s
Do you write all your translations hardcoded? If you do this on Kotlin it will cause all that extra code to bloat the script. If you fetch a JSON file and just parse it the code to do the dereferencing so there's no need to update code directly for other languages. I was looking to do achieve something similar in regards to having the autocomplete. It would require a fancy generator so I'm going to opt for Const Keys or a Sealed Class instead. This approach maintains default Keys and references them in the code instead, you then maintain a single file on the codebase.
Copy code
fun transkribe(key: String, currentLocale: Locale = //default) =
   keyMap[locale].?let { lang -> 
      //return translated string
   } ?: run { "" }
}

value = transkribe(HELLO)
I delegate the population of the map to external sources using builder functions in the object. This way the map can be loaded with the translation file from any location without worrying about supporting it directly in code but as a config. It might be worth adding a parser for different feeds, such as Android XML and Swift in the future but for now I think this might work across all platforms with a standard template.
t
@Sean Keane "If you do this on Kotlin it will cause all that extra code to bloat the script" - No if I do not reference translation objects from JS code. That's why I mentioned
@Serializable
Translation
interface - you can send it to client when needed (also, that's why I used
suspend
in
Message
fun interface). You don't even need to hardcode translation strings, but having translation as interface allows you to create locale specific implementations that handles inflection nicely. Other option would be code-splitting, but that might be difficult in kotlin. For example - units conjugation. English:
Copy code
class EnglishUnitTranslation(
    val singular : String,
    val plural : String
) {
    inflated(count : Int) = if(count == 1) { singular } else { plural }
}
Simple, isn't it? Let's do polish:
Copy code
class PolishUnitTranslation(
    val singular : String, // ex. jednostka
    val pluralA : String, // ex. jednostki
    val pluralB : String, // ex. jednostek
) {
    inflated(count : Int) = when {
        count == 1 -> singular
        count >= 11 && count <= 20 -> pluralB
        count%10 >= 2 && count%10 <== 4 -> pluralA
        else -> pluralB
    }
}
Gettext distinguishes 12 "plural-forms" languages families - https://www.gnu.org/software/gettext/manual/html_node/Plural-forms.html I don't think generic i18n frameworks would give you this much flexibility. For example, this case you can simply implement with generic i18n framwork:
Copy code
fun helloMessage(user, unreadMessagesCount) = if(undreadMessagesCount > 0) { 
    helloMessage.substitude(
        "userReference" to referenceUser(user),
        "unreadMessagesCount" to unreadMessagesCount,
        "unreadMessagesUnit" to messageUnit.inflated($unreadMessagesCount)
} else {
    helloNoMessages.substitude(
        "userReference" to referenceUser(user)
    )
}
But if you want to use "Good morning", "Good evening", "Good afternoon" instead simple "Hello", this also may become locale specific and any i18n framework probably won't save you now 🙃
s
These are all great strategies. Thanks for clarifying @Tomasz Krakowiak and @christophsturm I'm thinking currently I might try and use an abstraction to begin with and put i18n functionality behind suspended getters. this way the function can take a Key and return the desired string. Later I can add something more gettext based and add those to other platforms.
@Tomasz Krakowiak Did you manage to get this to work?
Copy code
fun interface Message<T> {
    suspend operator fun get(languageQualifier: LanguageQualifier) : T
}
All I get are errors with this.
t
I actually currently use version without
suspend
. I guess `operator`s cannot be used with
suspend
. The easier solution would be to get rid of
operator
keyword. It's just syntactic sugar.
👍 1
d
It's fun interface that can't have suspend...