Locales & i18n
Locale subtrees are derived from the base tree, not written twice.
A multilingual site is the same site with different text. A locale's subtree is generated from the base tree; you list only the pages that differ.
Documentation
Locale
type Locale struct {
Code string
Prefix string
Override map[string]func(ssg.Node) ssg.Node
}
| Field | Meaning |
|---|---|
Code |
language code and mount directory (de → /de/...) |
Prefix |
URL prefix passed to pages; defaults to "/" + Code |
Override |
patches for the nodes that differ, keyed by path relative to the locale root ("" is its home page) |
BuildLocales
root = ssg.BuildLocales(root, []ssg.Locale{…})
BuildLocales copies the base tree and mounts each copy under the locale's
code. Everything not named in Override — every other page, including generated
collection children — appears under /de/ on its own, rendered by the same
components with Ctx.Prefix set to "/de".
One line mounts a whole mirror; the override swaps in the one page whose text genuinely differs:
root = ssg.BuildLocales(root, []ssg.Locale{{
Code: "de",
Override: map[string]func(ssg.Node) ssg.Node{
"": func(n ssg.Node) ssg.Node { // the locale's own home page
n.Title, n.Page = "Meine Seite", ssg.Templ(views_de.Home)
return n
},
},
}})
What exactly is derived
BuildLocales copies the base tree and mounts the copy under the locale's
code. The copy carries Page, Files, Generate and Children — and drops
CopyFrom and CompileFrom (see below). Overrides are applied
shallowest-first, and branches that end up with nothing to render are pruned.
The deep mechanics are on How it works.
Overrides
An override is a function over the derived node, not a replacement for it:
it receives the node BuildLocales would otherwise have derived, so changing
one field is one line and nothing you don't touch is lost.
"about": func(n ssg.Node) ssg.Node {
n.Title = "Über mich" // Page and Children survive untouched
return n
},
A key with no counterpart in the base tree gets the zero Node, which is how a
page that only exists in one language is written:
"impressum": func(n ssg.Node) ssg.Node {
return ssg.Node{Title: "Impressum", Page: ssg.Templ(views_de.Impressum)}
},
Assets are not inherited
CopyFrom and CompileFrom are dropped from derived subtrees — a locale
mirrors pages, not assets. A locale that genuinely needs its own asset sets
the field back in an override.
Why assets aren't inherited
Stylesheets and scripts are shared across languages and served from the site
root. Inheriting them would produce build/de/style/, duplicating every
asset per language — and every new asset would silently grow every locale's
output.
Ctx.Link
templ Nav(c ssg.Ctx) {
<a href={ c.Link("projects/") }>Projects</a>
<a href={ c.Link("about/") }>About</a>
}
Link applies the prefix for you: the same markup yields /projects/ in the
base tree and /de/projects/ in the German subtree.
InLocale
templ LocaleSwitch(c ssg.Ctx) {
<a href={ c.InLocale("") }>English</a>
<a href={ c.InLocale("/de") }>Deutsch</a>
}
InLocale returns this page's URL in another language, which works because a
locale subtree mirrors the base tree — the same page exists at both paths.
Don't build it from Ctx.Path: Path already contains the current locale
segment, so re-prefixing gives /de/de/about/. Ctx.Rel is the
locale-independent form.
Why derived rather than declared
The obvious API is a Tree func() ssg.Node per locale. It was tried and thrown
out: it forces duplication, because the only thing you can hand it is a whole
subtree restating the structure you already wrote — every page added to the
base tree then has to be added again per language, and the two drift. Deriving
inverts that: the default is "the same site", and an override is an exception
you opt into.
Example
root = ssg.BuildLocales(root, []ssg.Locale{{
Code: "de",
Override: map[string]func(ssg.Node) ssg.Node{
"": func(n ssg.Node) ssg.Node {
n.Title, n.Page = "Meine Seite", ssg.Templ(views_de.Home)
return n
},
},
}})
One line mounts a German mirror of the whole site; the override swaps in the one page whose text genuinely differs.