Files
summercms/modules/phrasebook
Jakub Zych 107d820109 feat(10.1-02): mount plugin widget elements and run their actions from the form
- pluginAssets loads controller scripts and stylesheets from {base}/assets/ only, once per URL
- WidgetField mounts the custom element with attributes only and posts summer-action through the typed client
- Only declared fill keys returned by the server are patched; the form turns dirty and nothing saves
- widget is a registered valueless type rendered on create and update, labelled as a group
- backend::lang.extension strings in en and pl; embedded dist rebuilt
2026-09-29 02:04:34 +02:00
..

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:

# 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.