Kraft Integration

Switch language, and every subscribed component re-renders.

Register a controller

I18nController is a Stream<I18n>. Register it on the app and the whole component tree can reach it.

val kraft = kraftApp {
    semanticUI()
    i18n(
        I18nController.create(
            locale = Locale("en"),
            fallback = Locale("en"),
        ) {
            install(AppI18nCatalog)
        }
    )
}

create persists the chosen language in localStorage (key kraft.i18n.lang by default). Boot precedence is: the stored value, else initialLang, which itself defaults to the browser's language. Use I18nController.inMemory(...) for tests and anywhere you do not want persistence.

Kraft's own form catalog is always installed first, at the lowest precedence, so validation messages translate even if an app forgets to provide anything. App catalogs install afterwards and win on shared keys.

Using translations in a component

class GreetingComponent(ctx: Ctx<Props>) : Component<Props>(ctx) {

    private val t by Translations

    override fun VDom.render() {
        h1 { +t.checkout.greeting(name = props.name) }
    }
}

The by Translations delegate subscribes to the controller's translation stream, so the component re-renders when the language changes and unsubscribes when it unmounts. There is a matching by Formatting delegate for the formatting surface.

kraftApp { } always installs a default controller, so by Translations works in an app that has not configured i18n — unresolved keys come back as the key itself. It is not a fallback inside the delegate: looking up a missing attribute throws, and the default registration is what stops that happening.

Switching language

i18nController.setLang(Locale("de"))

The controller maps the chosen tag through Locale.parse onto a fresh I18n snapshot and emits it. Every subscribed component re-renders in the new language, and the choice is persisted.

Form validation

Kraft's form rules resolve their messages through the same surface, so validation errors follow the selected language with no extra wiring:

UiInputField(value, onChange = { value = it }) {
    label("Email")
    accepts(notBlank(), isEmail())
}

Rules produce their message via the i18n surface rather than a hardcoded string, and an app overrides any of them by defining the same key in its own catalog — app catalogs install last and win.

Form rules were the first thing wired up, deliberately. They are the highest-volume user-facing strings in an app and the ones most likely to be seen in anger, so if the mechanism were awkward it would have shown up immediately.

Async rules

Validation rules may be suspending — a uniqueness check against the server, for example. The form controller exposes a status so the UI can disable submit while checks are in flight, and a guard so a form cannot be submitted while validation is still running.