refactor(10.2-01): nest framework packages under modules

- Move remaining beach packages and embedded admin assets\n- Rewrite framework, example, build, and gate paths
This commit is contained in:
Jakub Zych
2026-09-28 02:21:02 +02:00
parent ac1f6d14f4
commit 5e50b166ef
277 changed files with 303 additions and 303 deletions

257
modules/cabana/messages.go Normal file
View File

@@ -0,0 +1,257 @@
package cabana
import (
"context"
"encoding/json"
"fmt"
"reflect"
"sort"
"git.golem15.com/golem15/summercms/modules/phrasebook"
)
// MessageForms is one controller message resolved to its CLDR plural forms
// (D-13). "other" is always present; the SPA picks a form with
// Intl.PluralRules and interpolates :count, :name and :term (D-24).
type MessageForms map[string]string
// MarshalJSON keeps an unset message an object, never null.
func (m MessageForms) MarshalJSON() ([]byte, error) {
if m == nil {
return []byte("{}"), nil
}
return json.Marshal(map[string]string(m))
}
// listMessageKeys is the config_list.yaml messages block: phrase keys.
type listMessageKeys struct {
RecordCount string `yaml:"recordCount"`
Create string `yaml:"create"`
SearchPrompt string `yaml:"searchPrompt"`
Empty string `yaml:"empty"`
EmptySearch string `yaml:"emptySearch"`
EmptySearchHint string `yaml:"emptySearchHint"`
Selected string `yaml:"selected"`
DeleteSelected string `yaml:"deleteSelected"`
DeleteConfirm string `yaml:"deleteConfirm"`
Deleted string `yaml:"deleted"`
}
// ListMessages is a list schema's copy, every key resolved (same field order
// as listMessageKeys).
type ListMessages struct {
RecordCount MessageForms `json:"recordCount"`
Create MessageForms `json:"create"`
SearchPrompt MessageForms `json:"searchPrompt"`
Empty MessageForms `json:"empty"`
EmptySearch MessageForms `json:"emptySearch"`
EmptySearchHint MessageForms `json:"emptySearchHint"`
Selected MessageForms `json:"selected"`
DeleteSelected MessageForms `json:"deleteSelected"`
DeleteConfirm MessageForms `json:"deleteConfirm"`
Deleted MessageForms `json:"deleted"`
}
// formMessageKeys is the config_form.yaml messages block: phrase keys.
type formMessageKeys struct {
Create string `yaml:"create"`
Update string `yaml:"update"`
Saved string `yaml:"saved"`
DeleteConfirm string `yaml:"deleteConfirm"`
Deleted string `yaml:"deleted"`
}
// FormMessages is a form's copy, every key resolved.
type FormMessages struct {
Create MessageForms `json:"create"`
Update MessageForms `json:"update"`
Saved MessageForms `json:"saved"`
DeleteConfirm MessageForms `json:"deleteConfirm"`
Deleted MessageForms `json:"deleted"`
}
// relationMessageKeys is one config_relation.yaml relation's messages block.
type relationMessageKeys struct {
Link string `yaml:"link"`
LinkHint string `yaml:"linkHint"`
CandidateSearch string `yaml:"candidateSearch"`
Linked string `yaml:"linked"`
UnlinkSelected string `yaml:"unlinkSelected"`
UnlinkConfirm string `yaml:"unlinkConfirm"`
Unlinked string `yaml:"unlinked"`
Empty string `yaml:"empty"`
}
// RelationMessages is a relation manager's copy, every key resolved.
type RelationMessages struct {
Link MessageForms `json:"link"`
LinkHint MessageForms `json:"linkHint"`
CandidateSearch MessageForms `json:"candidateSearch"`
Linked MessageForms `json:"linked"`
UnlinkSelected MessageForms `json:"unlinkSelected"`
UnlinkConfirm MessageForms `json:"unlinkConfirm"`
Unlinked MessageForms `json:"unlinked"`
Empty MessageForms `json:"empty"`
}
// Framework defaults (D-13): every omitted key falls back to backend::lang.
var (
listMessageDefaults = listMessageKeys{
RecordCount: "backend::lang.messages.list.record_count",
Create: "backend::lang.messages.list.create",
SearchPrompt: "backend::lang.list.search_prompt",
Empty: "backend::lang.list.no_records",
EmptySearch: "backend::lang.messages.list.empty_search",
EmptySearchHint: "backend::lang.messages.list.empty_search_hint",
Selected: "backend::lang.messages.list.selected",
DeleteSelected: "backend::lang.list.delete_selected",
DeleteConfirm: "backend::lang.messages.list.delete_confirm",
Deleted: "backend::lang.messages.list.deleted",
}
formMessageDefaults = formMessageKeys{
Create: "backend::lang.messages.form.create",
Update: "backend::lang.messages.form.update",
Saved: "backend::lang.messages.form.saved",
DeleteConfirm: "backend::lang.messages.form.delete_confirm",
Deleted: "backend::lang.messages.form.deleted",
}
relationMessageDefaults = relationMessageKeys{
Link: "backend::lang.messages.relation.link",
LinkHint: "backend::lang.messages.relation.link_hint",
CandidateSearch: "backend::lang.messages.relation.candidate_search",
Linked: "backend::lang.messages.relation.linked",
UnlinkSelected: "backend::lang.messages.relation.unlink_selected",
UnlinkConfirm: "backend::lang.messages.relation.unlink_confirm",
Unlinked: "backend::lang.messages.relation.unlinked",
Empty: "backend::lang.messages.relation.empty",
}
)
// withDefaults fills every empty key of keys from defaults.
func withDefaults[K any](keys, defaults K) K {
out := keys
dst := reflect.ValueOf(&out).Elem()
src := reflect.ValueOf(defaults)
for i := 0; i < dst.NumField(); i++ {
if dst.Field(i).String() == "" {
dst.Field(i).SetString(src.Field(i).String())
}
}
return out
}
// messageKeyPairs lists (yaml name, phrase key) for every key, in field order.
func messageKeyPairs[K any](keys K) [][2]string {
v := reflect.ValueOf(keys)
t := v.Type()
out := make([][2]string, 0, t.NumField())
for i := 0; i < t.NumField(); i++ {
out = append(out, [2]string{t.Field(i).Tag.Get("yaml"), v.Field(i).String()})
}
return out
}
// localizeMessages resolves each key of K into the same-index MessageForms
// field of M. Without a translator, or for a key that cannot convert, the
// form is {"other": text} where text is the translation or the key itself.
func localizeMessages[K any, M any](ctx context.Context, tr *phrasebook.Translator, keys K) M {
var out M
dst := reflect.ValueOf(&out).Elem()
src := reflect.ValueOf(keys)
locale := schemaLocale(ctx, tr)
for i := 0; i < src.NumField(); i++ {
key := src.Field(i).String()
dst.Field(i).Set(reflect.ValueOf(messageForms(tr, locale, key)))
}
return out
}
func messageForms(tr *phrasebook.Translator, locale, key string) MessageForms {
if key == "" {
return MessageForms{"other": ""}
}
if tr == nil {
return MessageForms{"other": key}
}
if forms, ok := tr.Forms(locale, key); ok {
return MessageForms(forms)
}
return MessageForms{"other": tr.GetIn(locale, key, nil)}
}
// listMessageKeySet is the list's resolved key set: declared keys over the
// framework defaults, with searchPrompt and empty defaulting to the list's own
// toolbar.search.prompt and noRecordsMessage when those are set.
func (s *ListSchema) listMessageKeySet() listMessageKeys {
defaults := listMessageDefaults
if s.SearchPrompt != "" {
defaults.SearchPrompt = s.SearchPrompt
}
if s.NoRecordsMessage != "" {
defaults.Empty = s.NoRecordsMessage
}
return withDefaults(s.messageKeys, defaults)
}
// formMessageKeySet is the form's resolved key set; create defaults to the
// config_form.yaml name when it is set.
func (s *FormSchema) formMessageKeySet() formMessageKeys {
defaults := formMessageDefaults
if s.Name != "" {
defaults.Create = s.Name
}
return withDefaults(s.messageKeys, defaults)
}
func (s *RelationSchema) relationMessageKeySet() relationMessageKeys {
return withDefaults(s.messageKeys, relationMessageDefaults)
}
// validateMessageKeys fails activation when a controller's messages block (or
// a framework default) names a phrase key no locale defines (D-13). Defaults
// taken from the list's own search prompt, empty message or the form name are
// Winter strings that may be literal text, so they are not checked.
func validateMessageKeys(reg *Registry, tr *phrasebook.Translator) error {
if reg == nil || tr == nil {
return nil
}
ids := make([]string, 0, len(reg.byID))
for id := range reg.byID {
ids = append(ids, id)
}
sort.Strings(ids)
check := func(pluginID, id, file string, pairs [][2]string) error {
for _, pair := range pairs {
if pair[1] != "" && !tr.Has(pair[1]) {
return bootErr(pluginID, id, file, fmt.Errorf("messages.%s names missing phrase key %s", pair[0], pair[1]))
}
}
return nil
}
for _, id := range ids {
cc := reg.byID[id]
if cc.List != nil {
if err := check(cc.PluginID, id, "config_list.yaml", messageKeyPairs(withDefaults(cc.List.messageKeys, listMessageDefaults))); err != nil {
return err
}
}
if cc.Form != nil {
if err := check(cc.PluginID, id, "config_form.yaml", messageKeyPairs(withDefaults(cc.Form.messageKeys, formMessageDefaults))); err != nil {
return err
}
}
names := make([]string, 0, len(cc.Relations))
for name := range cc.Relations {
names = append(names, name)
}
sort.Strings(names)
for _, name := range names {
if schema := cc.Relations[name].Schema; schema != nil {
if err := check(cc.PluginID, id, "config_relation.yaml", messageKeyPairs(schema.relationMessageKeySet())); err != nil {
return fmt.Errorf("relation %s: %w", name, err)
}
}
}
}
return nil
}