- lagoon.ValidateRequest ports Laravel 9 request validation: wildcard expansion, implicit-rule stop, bail, sometimes/nullable/blank skipping, size messages split by type and character-counted string lengths - ParseRules, In, CustomRule, UploadedFile and ErrorKeys for rule tables - pl/en lagoon::validation catalogs ported verbatim from WinterCMS - lagoon.Validate answers a numeric range failure with the bound that failed (min, max or numeric between) instead of always max
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.*(the messages oflagoon.Validate),lagoon::validation.*(the WinterCMS/Laravel validator catalog used bylagoon.ValidateRequest, with the size messages split intonumeric,file,stringandarray; a key the Polish file lacks, such asafter_or_equal, falls back to English as in WinterCMS) 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.