Files
summercms/modules/phrasebook/README.md

120 lines
6.4 KiB
Markdown

# 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 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/<locale>/<namespace>/<group>.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.*` (validation messages) 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/<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](../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.