- docs/database: models, migrations, queries and pagination, relations, casts and validation, attachments and transactions (lagoon.Transaction, lagoon.AfterCommit, nested savepoints, lagoon.OnDatabase) - docs/services: configuration, events, routing with auth groups, rate limiting, authentication, the OAuth server, mail and localization - runnable Examples for lagoon, attach, compass, surf, wire, bouncer, wristband, postcard, phrasebook and festival; lagoon TestDocs* regions run on the package's Postgres harness through DocsDB - 15 new required pages
phrasebook
Namespaced translation catalogs loaded from plugin YAML, with locale fallback, placeholder interpolation and CLDR pluralization.
import "git.golem15.com/golem15/summercms/modules/phrasebook"
Overview
phrasebook owns every translatable string in a SummerCMS application. At boot, phrasebook.Activate loads the framework's own strings plus the lang/ tree of every plugin that implements pact.HasLang, applies overrides from plugins that implement pact.HasLangOverrides, and publishes a single phrasebook.Translator on the backpack.App. Keys use the WinterCMS form namespace::group.dot.path, and message syntax follows Laravel (:name placeholders, | plural pipes), so it is the counterpart of WinterCMS's Lang::get / trans_choice and plugin lang/ directories.
Features
- YAML catalogs laid out as
lang/<locale>/<group>.yaml; nested maps flatten into keys such asacme.blog::posts.title(phrasebook.Catalog.Load). Duplicate keys, duplicate namespaces, malformed paths and non-string leaves fail at load time. - Overrides laid out as
lang/<locale>/<namespace>/<group>.yamlthat replace keys of any loaded namespace, including the framework admin strings, and may add locales (phrasebook.Catalog.Override). - Built-in framework namespaces:
lagoon::validate.*(validation messages) andbackend::lang.*(admin UI strings), shipped forenandpl. - Lookups by request locale (
phrasebook.Translator.Get, read from the context throughtowel.Locale) or by explicit locale (phrasebook.Translator.GetIn), walking the locale, its parent tags (pt-BRtopt) and then the fallback locale. A missing key returns the key itself. - Laravel-style placeholders:
:name,:Name(first letter upper-cased) and:NAME(upper-cased). - Pluralization with
phrasebook.Translator.Choiceandphrasebook.Translator.ChoiceIn: CLDR plural maps (one,few,many,other, ...) validated against the locale's categories, or Laravel pipes with exact ({0}) and range ([2,*]) conditions.:countis filled in automatically. - Export for the admin SPA:
phrasebook.Translator.Formsreturns a key as CLDR plural forms,phrasebook.Translator.Bundlereturns every key under a prefix, andphrasebook.Translator.Resolvedreports which locale such a bundle mostly comes from. Activation fails if abackend::string cannot be expressed as CLDR forms. - Missing keys are logged once per key through
log/slog, except in the production environment.
Usage
Plugins normally only ship a lang/ tree and implement pact.HasLang; the runtime calls phrasebook.Activate and handlers look the translator up from the app. A catalog can also be built directly:
# lang/en/posts.yaml
title: Posts
greeting: "Hello, :name"
count:
one: ":count post"
other: ":count posts"
package blog
import (
"context"
"embed"
"fmt"
"git.golem15.com/golem15/summercms/modules/backpack"
"git.golem15.com/golem15/summercms/modules/phrasebook"
"git.golem15.com/golem15/summercms/modules/towel"
)
//go:embed lang
var langFS embed.FS
func Example() error {
cat := phrasebook.NewCatalog()
if err := cat.Load("acme.blog", langFS); err != nil {
return err
}
tr := phrasebook.NewTranslator(cat, phrasebook.Options{Locale: "en", Fallback: "en"})
ctx := towel.WithLocale(context.Background(), "en")
fmt.Println(tr.Get(ctx, "acme.blog::posts.greeting", map[string]string{"name": "Ada"})) // Hello, Ada
fmt.Println(tr.ChoiceIn("en", "acme.blog::posts.count", 3, nil)) // 3 posts
return nil
}
// Inside a running application, use the translator published at boot.
func Title(ctx context.Context, app *backpack.App) string {
tr, ok := app.Lookup[*phrasebook.Translator]()
if !ok {
return "acme.blog::posts.title"
}
return tr.Get(ctx, "acme.blog::posts.title", nil)
}
API reference
| Identifier | Description |
|---|---|
phrasebook.Activate |
Loads framework and plugin catalogs in plugin order, applies overrides and publishes one phrasebook.Translator on the app. |
phrasebook.Catalog |
Set of namespaced translation entries, immutable once loading is done. |
phrasebook.NewCatalog |
Returns an empty catalog. |
phrasebook.Catalog.Load |
Loads a plugin's lang/<locale>/<group>.yaml files under the plugin ID namespace. |
phrasebook.Catalog.Override |
Applies an override tree over already loaded namespaces. |
phrasebook.Options |
Translator settings: app locale, fallback locale and production mode. |
phrasebook.Translator |
Resolves namespaced keys against a catalog. |
phrasebook.NewTranslator |
Builds a translator; empty locale and fallback default to en. |
phrasebook.Translator.Get |
Translates a key in the request locale, or the app locale when the context has none. |
phrasebook.Translator.GetIn |
Translates a key in an explicit locale. |
phrasebook.Translator.Choice |
Selects a plural form in the request locale. |
phrasebook.Translator.ChoiceIn |
Selects a plural form in an explicit locale. |
phrasebook.Translator.Has |
Reports whether any locale defines a key. |
phrasebook.Translator.Locale |
Returns the configured app locale. |
phrasebook.Translator.Forms |
Returns a key as CLDR plural forms for the admin SPA. |
phrasebook.Translator.Bundle |
Returns all keys under a prefix as CLDR forms, merged over the fallback chain. |
phrasebook.Translator.Resolved |
Returns the first locale in the fallback chain that has keys under a prefix. |
Configuration
phrasebook.Activate reads these keys from the app's compass config:
| Key | Default | Controls |
|---|---|---|
app.locale |
en |
The app locale, used when a request carries no locale. |
app.fallback_locale |
en |
The last locale tried before a key is reported missing. |
When the compass environment is production, missing-key warnings are not logged.
Dependencies
- SummerCMS modules: backpack, pact, towel.
- Third-party:
github.com/goccy/go-yaml,github.com/nicksnyder/go-i18n/v2(CLDR plural rules),golang.org/x/text/language. - Standard library:
context,embed,fmt,io/fs,log/slog,path,regexp,sort,strconv,strings,sync,unicode,unicode/utf8.
Testing
go test ./modules/phrasebook/...
The tests use in-memory filesystems and need no external services.