Web & Template Integration
Internationalized web applications must dynamically determine the user’s preferred language for each incoming HTTP request without requiring developers to manually pass a locale or context parameter through every service, repository, and template call.
Rays solves this using ThreadLocaleContextHolder for request-scoped locale management and LocaleFuncMap for seamless integration with Go’s standard html/template package.
1. Request-Scoped Locale Management
How It Works
ThreadLocaleContextHolder is powered by thread-local storage (routine.ThreadLocal). When an HTTP request enters your application:
- A middleware clears any leftover thread state by calling
holder.Clear(). - The middleware determines the desired locale from the request (URL query parameter, cookie, or
Accept-Languageheader). - The middleware sets the locale via
holder.Set(locale). - All subsequent function calls on that goroutine—including validators, template renderers, and business services—automatically access that locale via
holder.Get().
Writing an HTTP Middleware
Here is an idiomatic HTTP middleware compatible with standard Go http.Handler chains (and adaptable to any Go web framework):
package middleware
import (
"net/http"
"github.com/BeamFoundry/rays/pkg/i18n"
"github.com/BeamFoundry/rays/pkg/i18n/locales"
)
// LocaleMiddleware resolves the client's locale per request.
func LocaleMiddleware(holder i18n.LocaleContextHolder) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 1. Reset thread-local value to avoid leakage from pooled goroutines
holder.Clear()
// 2. Resolve preferred language (query param, cookie, or Accept-Language)
lang := r.URL.Query().Get("lang")
if lang == "" {
if cookie, err := r.Cookie("app_lang"); err == nil {
lang = cookie.Value
}
}
// 3. Assign the active locale
switch lang {
case "fr":
holder.Set(locales.Locales.FR)
case "de":
holder.Set(locales.Locales.DE)
case "es":
holder.Set(locales.Locales.ES)
default:
holder.Set(locales.Locales.EN)
}
// 4. Continue down the handler chain
next.ServeHTTP(w, r)
})
}
}2. HTML Template Integration (pkg/web)
Rays provides LocaleFuncMap in the pkg/web package, implementing the FuncMapper interface. It bridges the current LocaleContextHolder and MessageSource to Go’s standard html/template engine.
Available Template Functions
| Function | Signature | Description |
|---|---|---|
translate | translate(key string, params ...string) (string, error) | Translates a key with optional positional arguments ({0}, {1}, …). |
cardinal | cardinal(key string, num float64) (string, error) | Formats a cardinal quantity plural (e.g., “1 item”, “5 items”). |
ordinal | ordinal(key string, num float64) (string, error) | Formats an ordinal rank plural (e.g., “1st place”, “2nd place”). |
ranges | ranges(key string, num1, num2 float64) (string, error) | Formats a numeric range (e.g., “1-5 days”). |
Registering LocaleFuncMap
When parsing your HTML templates, pass the function map generated by funcMap.FuncMap():
package web
import (
"html/template"
"io"
"github.com/BeamFoundry/rays/pkg/web"
)
type TemplateRenderer struct {
funcMap *web.LocaleFuncMap
}
func (r *TemplateRenderer) Render(w io.Writer, tmplPath string, data any) error {
tmpl, err := template.New("page").
Funcs(r.funcMap.FuncMap()).
ParseFiles(tmplPath)
if err != nil {
return err
}
return tmpl.Execute(w, data)
}Writing Templates
Because the template functions look up the current locale from ThreadLocaleContextHolder dynamically at execution time, your templates automatically render in the user’s active language:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{{ translate "welcome" }}</title>
</head>
<body>
<header>
<h1>{{ translate "welcome_user" .User.FirstName .User.LastName }}</h1>
</header>
<main>
<!-- Cardinal Pluralization -->
<p class="status">{{ cardinal "days.left" .DaysRemaining }}</p>
<!-- Ordinal Pluralization -->
<p class="rank">Your rank: {{ ordinal "competition.place" .Rank }}</p>
<!-- Numeric Range -->
<p class="delivery">Estimated delivery: {{ ranges "days.range" .MinDays .MaxDays }}</p>
</main>
</body>
</html>Rendered Output Comparison
Given data where .DaysRemaining = 1.0, .Rank = 1.0, .MinDays = 1.0, and .MaxDays = 5.0:
Rendered in English (?lang=en):
<title>Welcome</title>
<h1>Welcome, Alice Smith!</h1>
<p class="status">1 day remaining</p>
<p class="rank">Your rank: 1st place</p>
<p class="delivery">Estimated delivery: 1-5 days</p>Rendered in French (?lang=fr):
<title>Bienvenue</title>
<h1>Bienvenue, Alice Smith !</h1>
<p class="status">1 jour restant</p>
<p class="rank">Your rank: 1er prix</p>
<p class="delivery">Estimated delivery: 1-5 jours</p>