Search code examples
kotlin-jsi18next-browser-languagedetector

How to bind i18next-browser-languagedetector to Kotlin?


The Gradle project is set by the JS plugin:

plugins {
    kotlin("js") version("1.6.10")
}

and uses the LEGACY compilation backend:

kotlin {
    js(LEGACY) {
        // ...
    }
}

My goal is to use the following dependencies in Kotlin sources:

dependencies {
    implementation(npm("i18next", "21.6.11"))
    implementation(npm("react-i18next", "11.15.4"))
    implementation(npm("i18next-browser-languagedetector", "6.1.3"))
}

It was pretty easy to describe JS-Kotlin bridging for the first two dependencies:

@JsModule("i18next")
@JsNonModule
external val i18next: I18n

external interface I18n {
    fun use(module: dynamic): I18n
}

@JsModule("react-i18next")
@JsNonModule
external val reactI18next: ReactI18next

external interface ReactI18next {
    val initReactI18next: dynamic
}

Unfortunately, the last one - i18next-browser-languagedetector - is driving me some nuts with its configuration. Something like this:

@JsModule("i18next-browser-languagedetector")
@JsNonModule
external val LanguageDetector: dynamic

doesn't work - the actual LanguageDetector provided by the declaration above is {}, so i18next doesn't consume it in Kotlin code (the JS code throws You are passing a wrong module! Please check the object you are passing to i18next.use()):

i18next.use(LanguageDetector) // fails

Can anyone please help me with a declaration of a JS-Kotlin bridge for the LanguageDetector?


Solution

  • Well, by debugging a little bit I've managed to solve this JS-Kotlin bridging issue. The working solution is the following declaration:

    @JsModule("i18next-browser-languagedetector")
    @JsNonModule
    external val i18nextBrowserLanguageDetector: I18nextBrowserLanguageDetector
    
    external interface I18nextBrowserLanguageDetector {
        @JsName("default")
        val LanguageDetector: dynamic
    }
    

    Now it's possible to do first parts of the i18next initialization chain:

    i18next
        .use(i18nextBrowserLanguageDetector.LanguageDetector)
        .use(reactI18next.initReactI18next)
        // ...
    

    Unfortunately, it's difficult to say that I'm getting any intuition behind it (maybe because of my huge blind spots in JS) - so any additional clarification or explanations would be helpful still.

    My biggest concern is that LanguageDetector from the declaration above should be a class, but it seems like no way to use something else rather than dynamic property. When I try to lift up the @JsName("default") annotation to mark some class protocol with it, it doesn't compile:

    @JsModule("i18next-browser-languagedetector")
    @JsNonModule
    @JsName("default")
    external class LanguageDetector
    

    It's not possible to use a nested class inside of the interface as well in this case:

    @JsModule("i18next-browser-languagedetector")
    @JsNonModule
    external interface I18nextBrowserLanguageDetector {
        @JsName("default")
        class LanguageDetector
    }
    

    So while it seems to be solved, it's super-frustrating still.