API reference

phrasebook

Namespaced translation catalogs loaded from plugin YAML, with locale fallback, placeholder interpolation and CLDR pluralization.

On this page

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.