Localization

The localization concern analyzes incoming requests for language preferences and allows the application logic to serve content based on those.

Basic Usage

By default, the concern will read the Accept-Language header and provide the selected java.util.Locale as the current UI culture, which can be read with CurrentCulture.uiCulture().

Webservices

Kotlin
import org.genhttp.engine.internal.Host

import org.genhttp.modules.i18n.*
import org.genhttp.modules.layouting.*
import org.genhttp.modules.practices.*
import org.genhttp.modules.webservices.*

suspend fun main() {
    val localization = Localization.create()

    val app = Layout.create()
        .addService<LocalizedService>("service")
        .add(localization)

    Host.create()
        .handler(app)
        .defaults()
        .development()
        .run()
}

class LocalizedService {

    @ResourceMethod
    suspend fun getLocalized(): String = CurrentCulture.uiCulture().toLanguageTag()

}

Functional

Kotlin
import org.genhttp.engine.internal.Host

import org.genhttp.modules.functional.*
import org.genhttp.modules.i18n.*
import org.genhttp.modules.practices.*

suspend fun main() {
    val localization = Localization.create()

    val app = Inline.create()
        .get("/service/") { CurrentCulture.uiCulture().toLanguageTag() }
        .add(localization)

    Host.create()
        .handler(app)
        .defaults()
        .development()
        .run()
}

Controllers

Kotlin
import org.genhttp.engine.internal.Host

import org.genhttp.modules.controllers.*
import org.genhttp.modules.i18n.*
import org.genhttp.modules.layouting.*
import org.genhttp.modules.practices.*
import org.genhttp.modules.reflection.*

suspend fun main() {
    val localization = Localization.create()

    val app = Layout.create()
        .addController<LocalizedController>("service")
        .add(localization)

    Host.create()
        .handler(app)
        .defaults()
        .development()
        .run()
}

class LocalizedController {

    @ControllerAction(Method.Get)
    suspend fun index(): String = CurrentCulture.uiCulture().toLanguageTag()

}

Running and accessing this example app via http://localhost:8080/service/ in your browser will print the preferred locale of your client (such as de-DE). The selected language is also announced to the client with the Content-Language header of the response.

GenHTTP sets CultureInfo.CurrentCulture and CultureInfo.CurrentUICulture of the current thread. The JVM has no such per-thread culture, and a coroutine may continue on another thread after it suspended, so genhttp-kt keeps the selected locale in the coroutine context of the content (CultureContext): CurrentCulture.culture() and CurrentCulture.uiCulture() read it from any suspending function called while the request is handled, and fall back to the default locale of the JVM if none has been selected. The locale is also stored in the properties of the request (CurrentCulture.CultureProperty and CurrentCulture.UiCultureProperty), so functions that do not suspend can read it from there. The default locale of the JVM is never changed.

Language Negotiation

Typically your application will not support any language, so the client and the server need to negotiate a language that suits the client and the server can provide. Therefore, the client will send a list of supported languages and their ranking in the Accept-Language header, such as de,de-DE;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6,de-CH;q=0.5.

On the server side, you need to specify the cultures that are supported by your application, either statically or dynamically:

Kotlin
import org.genhttp.modules.i18n.*

import java.util.Locale

val localization = Localization.create()

// statically set supported cultures
localization.supports(Locale.GERMANY, Locale.FRANCE)

// dynamically evaluate support
localization.supports { culture -> culture.getDisplayName(Locale.ENGLISH).contains("ish") }

A language requested without a region is resolved to its default region (de becomes de-DE, en becomes en-US), so the locales you support should name a region as well.

The localization concern will then try to find the language requested by the client which is supported by the server and has the highest priority on the client-side. If there is no intersection between the two, the logic will fallback to the default locale that is used by the server environment. The default value can be overridden as needed:

Kotlin
import org.genhttp.modules.i18n.*

import java.util.Locale

val localization = Localization.create()
    .default(Locale.forLanguageTag("pl-PL"))

Parameter Source

As shown above, the server will analyze the Accept-Language header by default to check the languages requested by the client. For API or web applications you might want a different source instead:

Kotlin
val localization = Localization.create()

// read a query parameter, e.g. ?language=es
localization.fromQuery("language")

// read a cookie
localization.fromCookie("__my_app_language")

// read a custom header
localization.fromHeader("X-Custom-Language")

// read directly from the request, e.g. /de/...
localization.fromRequest { request -> request.header.target.current?.decode() }

If needed, you can specify multiple sources at the same time.

A function passed to fromRequest may also return the locales directly (as an Iterable<Locale>) instead of a language string. Kotlin chooses the variant by the return type of the lambda; on the JVM, the variant returning locales is named fromRequestCultures.

Consuming the Language

As shown above, the server will provide the selected locale as the current UI culture. This behavior can be adjusted as needed:

Kotlin
import org.genhttp.api.content.authentication.User

import org.genhttp.modules.authentication.getUser
import org.genhttp.modules.i18n.*

import java.util.Locale

val localization = Localization.create()

// set current culture instead (read with CurrentCulture.culture())
localization.setter(currentCulture = true, currentUiCulture = false)

// use a custom logic
localization.setter { request, culture -> request.getUser<MyAppUser>()?.language = culture }

class MyAppUser(override val displayName: String) : User {

    var language: Locale? = null

}

Running in Docker

On the JVM, the locale data is part of the Java runtime, so there is nothing to install in your Docker image when you use a regular JDK or JRE image. If you build a custom runtime with jlink, add the jdk.localedata module (--add-modules jdk.localedata), which provides the data of the locales other than US English.