Skip to content

i18n

Internationalisation with Accept-Language detection and go-i18n translations.

Package: github.com/oliverandrich/burrow/contrib/i18n

Depends on: none

Setup

srv := burrow.NewServer(
    i18n.New(),
    // ... other apps
)

Adding Translations

Apps contribute translations by implementing the burrow.HasTranslations interface. The i18n app auto-discovers all registered apps during Configure() and loads their translation files:

//go:embed translations
var translationFS embed.FS

func (a *App) TranslationFS() fs.FS { return translationFS }

The returned fs.FS must contain a translations/ directory with TOML files (see Translation Files below).

Translation Files

Translation files are TOML files in a translations/ directory:

translations/
├── active.en.toml
└── active.de.toml

Example translations/active.en.toml:

[welcome]
other = "Welcome, {{.Name}}!"

[notes_count]
one = "{{.Count}} note"
other = "{{.Count}} notes"

Example translations/active.de.toml:

[welcome]
other = "Willkommen, {{.Name}}!"

[notes_count]
one = "{{.Count}} Notiz"
other = "{{.Count}} Notizen"

Using in Templates

The i18n app implements HasRequestFuncMap and provides these functions in all templates:

Function Description
{{ lang }} Current locale string (e.g., "en", "de")
{{ t "key" }} Simple translation lookup
{{ tData "key" .DataMap }} Translation with template data
{{ tPlural "key" .Count }} Pluralised translation
{{ define "notes/list" -}}
<html lang="{{ lang }}">
<body>
    <h1>{{ t "notes-title" }}</h1>
    <p>{{ tData "welcome" .WelcomeData }}</p>
    <span>{{ tPlural "notes_count" .Count }}</span>
</body>
{{- end }}

Go API

T — Simple Translation

import "github.com/oliverandrich/burrow/contrib/i18n"

msg := i18n.T(ctx, "welcome")
// "Welcome, {{.Name}}!" (raw, no data substitution)

TData — Translation with Template Data

msg := i18n.TData(ctx, "welcome", map[string]any{
    "Name": "Alice",
})
// "Welcome, Alice!"

TPlural — Pluralised Translation

msg := i18n.TPlural(ctx, "notes_count", 5)
// "5 notes"

msg = i18n.TPlural(ctx, "notes_count", 1)
// "1 note"

Locale — Current Locale

locale := i18n.Locale(ctx)
// "en", "de", etc.

All functions fall back to the message ID if no translation is found.

Language Matching & Fallback

The middleware reads the browser's Accept-Language header and matches it against the configured supported languages using Go's golang.org/x/text/language matcher. If no match is found, the default language is used.

Examples (with default config en,de):

Accept-Language Resolved Locale Reason
de-AT,de;q=0.9 de Regional variant matches base language
fr-FR,fr;q=0.9 en French not supported, falls back to default
de;q=0.8,en;q=0.9 en English has higher quality value
(empty) en No header, uses default

This means you never need to handle unsupported languages yourself — the matcher always resolves to one of the configured languages.

Auto-Discovery

During Configure(), the i18n app iterates all registered apps and loads translations from any that implement burrow.HasTranslations (see Adding Translations above). The auth app uses this pattern to contribute its English and German translations automatically.

Translating Validation Errors

When using validation, error messages default to English. TranslateValidationErrors translates them in-place using the current locale:

import "github.com/oliverandrich/burrow/contrib/i18n"

func (h *Handlers) Create(w http.ResponseWriter, r *http.Request) error {
    var req struct {
        Name  string `form:"name"  validate:"required"`
        Email string `form:"email" validate:"required,email"`
    }

    if err := burrow.Bind(r, &req); err != nil {
        var ve *burrow.ValidationError
        if errors.As(err, &ve) {
            i18n.TranslateValidationErrors(r.Context(), ve)
            return burrow.RenderTemplate(w, r, http.StatusUnprocessableEntity, "myapp/form", map[string]any{
                "Errors": ve,
            })
        }
        return err
    }

    // ...
}

Built-in Translation Keys

The i18n app ships with English and German translations for all built-in validation tags:

Key English Template
validation-required {{.Field}} is required
validation-email {{.Field}} must be a valid email address
validation-min {{.Field}} must be at least {{.Param}}
validation-max {{.Field}} must be at most {{.Param}}
validation-len {{.Field}} must be exactly {{.Param}} characters
validation-gte {{.Field}} must be greater than or equal to {{.Param}}
validation-lte {{.Field}} must be less than or equal to {{.Param}}
validation-url {{.Field}} must be a valid URL

Template variables: {{.Field}} is the field name, {{.Param}} is the tag parameter (e.g., "3" for min=3).

Overriding Translations

Your app's translation files are loaded after the built-in ones (last-loaded wins). To customise a validation message, define the same key in your TOML file:

# translations/active.en.toml
validation-required = "{{.Field}} cannot be blank"

Fallback Behaviour

  • No localiser in context: original English message is preserved
  • Unknown validation tag: the key validation-{tag} won't match any translation, so the original message is preserved

Configuration

Flag Env Var Default Description
--i18n-default-language I18N_DEFAULT_LANGUAGE en Default language
--i18n-supported-languages I18N_SUPPORTED_LANGUAGES en,de Comma-separated supported languages

Interfaces Implemented

Interface Description
burrow.App Required: Name(), Register()
Configurable Language configuration flags
HasMiddleware Locale detection middleware
HasRequestFuncMap Provides lang, t, tData, tPlural to templates