feat(10-02): backend strings, controller messages and declarative toolbar
- phrasebook ships the backend::lang admin strings (pl, en) with CLDR plural maps, loads them as namespace backend, applies pact.HasLangOverrides trees (lang/<locale>/<namespace>/<group>.yaml) after every namespace, and fails activation when a backend key cannot convert to plural forms - Translator.Forms, Bundle, Resolved and Has serve keys as CLDR form maps - Public GET /lang returns every backend::lang key for the request locale over the fallback locale, Cache-Control no-cache - config_list, config_form and config_relation accept a strict messages block; omitted keys take framework defaults, schemas serve every message as CLDR forms, and activation fails on a missing phrase key - toolbar.buttons is an ordered [create, delete] list; the Winter string form, duplicates, unknown actions and delete without showCheckboxes fail at boot, and create is dropped when the controller has no form - Form schema serves the raw Winter redirects; scaffold emits the list syntax; form and relation schema routes are typed in the admin OpenAPI
This commit is contained in:
@@ -91,6 +91,9 @@ func Activate[P interface{ ID() string }](app *backpack.App, plugins []P) error
|
||||
if err := cat.Load("lagoon", systemLangFS); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := cat.Load("backend", backendLang()); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, p := range plugins {
|
||||
hl, ok := any(p).(pact.HasLang)
|
||||
if !ok {
|
||||
@@ -100,6 +103,21 @@ func Activate[P interface{ ID() string }](app *backpack.App, plugins []P) error
|
||||
return err
|
||||
}
|
||||
}
|
||||
// Overrides run after every namespace is loaded (D-20): a project can
|
||||
// replace framework or plugin strings and add locales without a rebuild
|
||||
// of the admin SPA.
|
||||
for _, p := range plugins {
|
||||
ho, ok := any(p).(pact.HasLangOverrides)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if err := cat.Override(p.ID(), ho.LangOverridesFS()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := cat.checkForms("backend::"); err != nil {
|
||||
return err
|
||||
}
|
||||
opts := Options{Locale: defaultLocale, Fallback: defaultFallback}
|
||||
if app.Config != nil {
|
||||
if v := strings.TrimSpace(app.Config.String("app.locale")); v != "" {
|
||||
@@ -460,3 +478,102 @@ func ucfirst(s string) string {
|
||||
}
|
||||
return string(unicode.ToUpper(r)) + s[size:]
|
||||
}
|
||||
|
||||
// Forms returns key's text as CLDR plural forms for the SPA (D-13, D-20): a
|
||||
// plain text is {"other": text}, a YAML plural map is copied, and a pipe of
|
||||
// category forms maps by category. Exact and range pipes ({0}, [2,*]) have
|
||||
// no Intl.PluralRules equivalent and report false, as does a missing key.
|
||||
// Placeholders are left for the client to interpolate.
|
||||
func (t *Translator) Forms(locale, key string) (map[string]string, bool) {
|
||||
if t == nil {
|
||||
return nil, false
|
||||
}
|
||||
e, _, ok := t.find(locale, key)
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
return entryForms(e)
|
||||
}
|
||||
|
||||
// Bundle returns every key starting with prefix as CLDR forms, keys of the
|
||||
// requested locale over those of its fallback chain. Keys that cannot convert
|
||||
// are left out.
|
||||
func (t *Translator) Bundle(locale, prefix string) map[string]map[string]string {
|
||||
out := map[string]map[string]string{}
|
||||
if t == nil || t.cat == nil {
|
||||
return out
|
||||
}
|
||||
chain := fallbackChain(locale, t.fallbackLocale())
|
||||
for i := len(chain) - 1; i >= 0; i-- {
|
||||
for key, e := range t.cat.messages[chain[i]] {
|
||||
if !strings.HasPrefix(key, prefix) {
|
||||
continue
|
||||
}
|
||||
if forms, ok := entryForms(e); ok {
|
||||
out[key] = forms
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// Resolved returns the first locale of locale's fallback chain that the
|
||||
// catalog has any key for under prefix: the locale a Bundle is mostly in.
|
||||
func (t *Translator) Resolved(locale, prefix string) string {
|
||||
if t == nil || t.cat == nil {
|
||||
return locale
|
||||
}
|
||||
for _, step := range fallbackChain(locale, t.fallbackLocale()) {
|
||||
for key := range t.cat.messages[step] {
|
||||
if strings.HasPrefix(key, prefix) {
|
||||
return step
|
||||
}
|
||||
}
|
||||
}
|
||||
return t.fallbackLocale()
|
||||
}
|
||||
|
||||
// Has reports whether any locale of the catalog defines key.
|
||||
func (t *Translator) Has(key string) bool {
|
||||
if t == nil || t.cat == nil {
|
||||
return false
|
||||
}
|
||||
for _, byKey := range t.cat.messages {
|
||||
if _, ok := byKey[key]; ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (t *Translator) fallbackLocale() string {
|
||||
if t == nil || strings.TrimSpace(t.fallback) == "" {
|
||||
return defaultFallback
|
||||
}
|
||||
return t.fallback
|
||||
}
|
||||
|
||||
func entryForms(e entry) (map[string]string, bool) {
|
||||
switch {
|
||||
case len(e.pipes) > 0:
|
||||
out := make(map[string]string, len(e.pipes))
|
||||
for _, part := range e.pipes {
|
||||
if part.exact != nil || part.lo != nil || part.cat == "" {
|
||||
return nil, false
|
||||
}
|
||||
out[part.cat] = part.text
|
||||
}
|
||||
if _, ok := out["other"]; !ok {
|
||||
return nil, false
|
||||
}
|
||||
return out, true
|
||||
case e.plurals != nil:
|
||||
out := make(map[string]string, len(e.plurals))
|
||||
for cat, text := range e.plurals {
|
||||
out[cat] = text
|
||||
}
|
||||
return out, true
|
||||
default:
|
||||
return map[string]string{"other": e.text}, true
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user