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:

  1. A middleware clears any leftover thread state by calling holder.Clear().
  2. The middleware determines the desired locale from the request (URL query parameter, cookie, or Accept-Language header).
  3. The middleware sets the locale via holder.Set(locale).
  4. 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

FunctionSignatureDescription
translatetranslate(key string, params ...string) (string, error)Translates a key with optional positional arguments ({0}, {1}, …).
cardinalcardinal(key string, num float64) (string, error)Formats a cardinal quantity plural (e.g., “1 item”, “5 items”).
ordinalordinal(key string, num float64) (string, error)Formats an ordinal rank plural (e.g., “1st place”, “2nd place”).
rangesranges(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>