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:
257
modules/cabana/messages.go
Normal file
257
modules/cabana/messages.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user