# 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//.yaml`; nested maps flatten into keys such as `acme.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///.yaml` that 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 of `lagoon.Validate`), `lagoon::validation.*` (the WinterCMS/Laravel validator catalog used by `lagoon.ValidateRequest`, with the size messages split into `numeric`, `file`, `string` and `array`; a key the Polish file lacks, such as `after_or_equal`, falls back to English as in WinterCMS) and `backend::lang.*` (admin UI strings), shipped for `en` and `pl`. - Lookups by request locale (`phrasebook.Translator.Get`, read from the context through `towel.Locale`) or by explicit locale (`phrasebook.Translator.GetIn`), walking the locale, its parent tags (`pt-BR` to `pt`) 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.Choice` and `phrasebook.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. `:count` is filled in automatically. - Export for the admin SPA: `phrasebook.Translator.Forms` returns a key as CLDR plural forms, `phrasebook.Translator.Bundle` returns every key under a prefix, and `phrasebook.Translator.Resolved` reports which locale such a bundle mostly comes from. Activation fails if a `backend::` 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: ```yaml # lang/en/posts.yaml title: Posts greeting: "Hello, :name" count: one: ":count post" other: ":count posts" ``` ```go 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//.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](../compass/README.md) 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](../backpack/README.md), [pact](../pact/README.md), [towel](../towel/README.md). - 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 ```sh go test ./modules/phrasebook/... ``` The tests use in-memory filesystems and need no external services.