serve language variants under a prefix
The default locale stays at the root; every other language is the same key under
/{lang}/ (ADR-0009). /en/… is never live and redirects to the root form so the URL
space cannot fork. Lookup now takes a language and reports which one it served,
following requested → default → any rather than 404ing when a translation is
missing.
That is the second routing case, so the resolver is extracted to resolve.go and
the mux keeps one entry: URL shape is the resolver's business. A leading segment
counts as a language only when some bundle is written in it, so an unknown prefix
is a 404 rather than a stripped path — and a section may not be named after a
language in use, now recorded in content-model.md.
Because the served variant can differ from the URL requested, Page gained
.Canonical (the variant actually served) and .Alternates for hreflang. A theme
must never build a path, so both come from the engine.
Evidence: /bn/pages/about/ serves the Bengali body with lang="bn" and canonical
/bn/pages/about/; /bn/posts/hello-world/ falls back to English with canonical
/posts/hello-world/; /en/pages/about/ 301s to /pages/about/; /fr/… is 404.
This commit is contained in:
@@ -11,6 +11,7 @@ import (
|
||||
"log/slog"
|
||||
"os"
|
||||
"path"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/text/unicode/norm"
|
||||
@@ -231,14 +232,58 @@ func NewSite(bundles []Bundle) *Site {
|
||||
return s
|
||||
}
|
||||
|
||||
// Lookup returns the default-locale variant of a key.
|
||||
// Lookup returns the best variant of a key for a requested language, and the language actually served.
|
||||
//
|
||||
// Other languages are reachable only once language routing exists; until then a key means its default
|
||||
// variant (ADR-0021).
|
||||
func (s *Site) Lookup(key string) (Bundle, bool) {
|
||||
b, ok := s.byKeyLang[key+"\x00"+DefaultLang]
|
||||
return b, ok
|
||||
// The fallback chain is requested → default → any (ADR-0009); "any" is resolved in sorted order so the
|
||||
// same request always answers the same way. A key with no variants at all reports false.
|
||||
func (s *Site) Lookup(key, lang string) (b Bundle, served string, ok bool) {
|
||||
for _, try := range []string{lang, DefaultLang} {
|
||||
if try == "" {
|
||||
continue
|
||||
}
|
||||
if b, ok = s.byKeyLang[key+"\x00"+try]; ok {
|
||||
return b, try, true
|
||||
}
|
||||
}
|
||||
for _, l := range s.Variants(key) {
|
||||
b = s.byKeyLang[key+"\x00"+l]
|
||||
return b, l, true
|
||||
}
|
||||
return Bundle{}, "", false
|
||||
}
|
||||
|
||||
// Variants lists the languages a key exists in, sorted.
|
||||
func (s *Site) Variants(key string) []string {
|
||||
var langs []string
|
||||
for kl := range s.byKeyLang {
|
||||
k, l, found := strings.Cut(kl, "\x00")
|
||||
if found && k == key {
|
||||
langs = append(langs, l)
|
||||
}
|
||||
}
|
||||
sort.Strings(langs)
|
||||
return langs
|
||||
}
|
||||
|
||||
// HasLang reports whether any bundle is written in lang. The resolver needs this to tell a language
|
||||
// prefix from a section that happens to share its name.
|
||||
func (s *Site) HasLang(lang string) bool {
|
||||
for kl := range s.byKeyLang {
|
||||
if _, l, found := strings.Cut(kl, "\x00"); found && l == lang {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Len reports how many bundles the site holds.
|
||||
func (s *Site) Len() int { return len(s.byKeyLang) }
|
||||
|
||||
// URL is the permalink of a variant: /{section}/{slug}/, with a language prefix for anything but the
|
||||
// default locale (ADR-0008, ADR-0009). Templates never build a path by hand.
|
||||
func URL(key, lang string) string {
|
||||
if lang == "" || lang == DefaultLang {
|
||||
return "/" + key + "/"
|
||||
}
|
||||
return "/" + lang + "/" + key + "/"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user