CARLOS rastrillo docs

๐Ÿค– Localization

You declare your locale set and supply catalogs from an embed.FS. Each request resolves one locale, and your actions look strings up through a request-scoped function.

Declaring

opts := rastrillo.Options{
	Locales:       []string{"en", "fr"},
	DefaultLocale: "en",
	LocaleFS:      localeFS, // embed.FS carrying locales/<code>.toml
}

Catalogs are flat TOML, nothing nested:

orders.title = "Your orders"
orders.greeting = "Hello, {name}"

How a locale gets picked

In this order: a URL path prefix, stripped before your mux sees it so /fr/orders and /orders reach the same route; then the rastrillo_locale cookie, when it names a declared locale; then Accept-Language, q-ordered, so a browser sending fr-CA matches a declared fr; then your default.

The cookie is written by the framework's own POST /_locale route, which rastrillo.Serve mounts whenever you declare locales at all. The locale-menu partial renders a switcher that posts to it:

{{template "locale-menu" dict "Items" .Locales "Return" .Path}}

where .Locales is rastrillo.LocaleItems(r) โ€” empty for a one-locale app, so the partial renders nothing โ€” and .Path is the current path and query to return to. Each item lands on the same path under the new prefix, and the cookie makes the choice stick on unprefixed paths too.

Looking strings up

rastrillo.T(r, "orders.title")
rastrillo.Tf(r, "orders.greeting", "name", user.Name)

Tf interpolates {name}-style placeholders.

A missing translation stays visible on the page as orders.title โ€” never blank, never a crash. Lookup falls back through five levels, listed in the next section.

The framework base catalog

The framework ships its own strings โ€” rastrillo/ui's rastrillo.ui.* keys โ€” in twelve locales: en, ga, zh-Hans, es, hi, pt, bn, ru, ja, yue, vi, ar. Declare one of those and the built-in components speak it with no catalog of your own. Matching is by the code you declare, exactly: zh does not find zh-Hans.

Lookup falls back through five levels: the requested locale's app catalog, the default locale's app catalog, the framework's catalog for the requested locale (when it ships one), the framework's English, and finally the key itself. A missing translation stays visible on the page as orders.title โ€” never blank, never a crash.

Your own catalog entry for the same key wins, so you can reword a component's built-in string without ejecting it.

rastrillo.Dir(locale) gives the <html dir> value โ€” rtl for Arabic, Persian, Hebrew and Urdu โ€” so a layout never guesses.

Inside a partial, a caller-supplied value beats the built-in default. ui.FuncsWith rebinds T to a request-scoped lookup so those defaults resolve in the request's locale rather than in hardcoded English:

tmpl.Funcs(ui.FuncsWith(func(key string, args ...any) string {
	return rastrillo.Tf(r, key, args...)
}))

The pre-ship gate

rastrillo generate --check --default-locale en

Fails when a non-default catalog is missing keys the default has.

It also fails when a non-default catalog is for a locale the framework does not ship and leaves any rastrillo.ui.* key untranslated โ€” those components would silently render in English. The message lists the keys; copy them from the module's locales/en.toml.

This runs under --check only. Plain rastrillo generate โ€” and so rastrillo dev and rastrillo new โ€” never fails on an incomplete catalog. That split is the design: silent fallback while you iterate, loud failure before you ship.

--default-locale defaults to en and is not read from Options.DefaultLocale. If your app sets a different default, pass the matching value here, or the check compares every catalog against the wrong one and passes while telling you nothing.

Two caveats

A locale code cannot also be a first path segment. An app declaring locale en cannot serve an app route at /en/..., because the prefix is stripped before the mux sees it. That is inherent to prefix routing.

A trailing-slash redirect under a locale prefix drops the locale. When ServeMux issues its own trailing-slash redirect for a path under a prefix, it emits the unprefixed path. Known limitation, affecting that one redirect.

Read this page as markdown โ€” exact, unstyled, and cheap for an agent to fetch.