Skip to content
Switching language

Switching language

By default an app uses the operating system’s language. To offer a language setting inside the app, call lang.SetLocale:

language := widget.NewSelect([]string{"System", "English", "Deutsch", "العربية"}, func(s string) {
	switch s {
	case "English":
		lang.SetLocale("en-US")
	case "Deutsch":
		lang.SetLocale("de-DE")
	case "العربية":
		lang.SetLocale("ar-EG") // also switches the layout to right-to-left
	default:
		lang.SetLocale("") // back to the system locale
	}
})

SetLocale takes a BCP 47 locale such as "fr", "de-DE" or "sr-Cyrl-RS". It changes translations, formats, sorting and text direction together. An empty locale goes back to the system locale.

When the locale changes, Bellui lays out and repaints every window, as for a theme change. Call it on the UI goroutine, for example from a widget callback. The choice is not saved: store it in the app’s preferences and call SetLocale again at startup.

Text that follows the language

Repainting is not the same as re-translating. A label made with widget.NewLabel(lang.L("Cancel")) received a finished string when it was built, and keeps it after a language change. For text that should change, use one of these instead.

Widgets that take a binding. A localized binding is a binding.String whose value is the translation. It translates again on every language change:

// A label whose text is re-translated on every language change.
welcome := widget.NewLabelWithData(
	binding.NewLocalizedString("Welcome, {{.Name}}!", map[string]any{"Name": "Ada"}),
)

// A plural that follows both the language and the count.
count := binding.NewInt()
_ = count.Set(1)
selected := widget.NewLabelWithData(
	// With no template data, {{.Count}} is the live count.
	binding.NewLocalizedPluralWithKey("files_selected", "{{.Count}} files selected", count),
)
more := widget.NewButton("+1", func() {
	n, _ := count.Get()
	_ = count.Set(n + 1)
})

A localized binding only listens for language changes while something is bound to it, so a binding you stop using does not leak.

Everything else. A button’s text, a window title or a menu item has no binding. lang.BindText calls a setter with the translation now and after every language change:

// Surfaces without data binding: a button label and the window title.
save := widget.NewButton("", func() {})
lang.BindText("button.save", save.SetText)
lang.BindText("Welcome, {{.Name}}!", w.SetTitle, map[string]any{"Name": "Ada"})

BindText returns a function that stops the updates. Call it when the widget is thrown away. For things that live as long as the app, such as the main window’s title, you can ignore it.

For anything else that depends on the locale, lang.AddLocaleChangeListener registers a function to call after each change.

The full program:

// Switching the app's language while it runs, with text that re-translates
// itself.
package main

import (
	"embed"

	"github.com/belluikit/bellui"
	"github.com/belluikit/bellui/app"
	"github.com/belluikit/bellui/container"
	"github.com/belluikit/bellui/data/binding"
	"github.com/belluikit/bellui/lang"
	"github.com/belluikit/bellui/widget"
)

//go:embed translations
var translations embed.FS

func main() {
	_ = lang.AddTranslationsFS(translations, "translations")

	a := app.New()
	w := a.NewWindow("")

	// A label whose text is re-translated on every language change.
	welcome := widget.NewLabelWithData(
		binding.NewLocalizedString("Welcome, {{.Name}}!", map[string]any{"Name": "Ada"}),
	)

	// A plural that follows both the language and the count.
	count := binding.NewInt()
	_ = count.Set(1)
	selected := widget.NewLabelWithData(
		// With no template data, {{.Count}} is the live count.
		binding.NewLocalizedPluralWithKey("files_selected", "{{.Count}} files selected", count),
	)
	more := widget.NewButton("+1", func() {
		n, _ := count.Get()
		_ = count.Set(n + 1)
	})

	// Surfaces without data binding: a button label and the window title.
	save := widget.NewButton("", func() {})
	lang.BindText("button.save", save.SetText)
	lang.BindText("Welcome, {{.Name}}!", w.SetTitle, map[string]any{"Name": "Ada"})

	language := widget.NewSelect([]string{"System", "English", "Deutsch", "العربية"}, func(s string) {
		switch s {
		case "English":
			lang.SetLocale("en-US")
		case "Deutsch":
			lang.SetLocale("de-DE")
		case "العربية":
			lang.SetLocale("ar-EG") // also switches the layout to right-to-left
		default:
			lang.SetLocale("") // back to the system locale
		}
	})

	w.SetContent(container.NewVBox(language, welcome, container.NewHBox(selected, more), save))
	bellui.ShowAndRun(w)
}