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:
Jakub Zych
2026-09-27 16:16:32 +02:00
parent fe04dbc89e
commit c87148a34f
24 changed files with 2148 additions and 45 deletions

View File

@@ -154,6 +154,36 @@
],
"type": "object"
},
"cabana.Envelope-cabana_FormView": {
"properties": {
"data": {
"$ref": "#/components/schemas/cabana.FormView"
},
"meta": {
"$ref": "#/components/schemas/cabana.SuccessMeta"
}
},
"required": [
"data",
"meta"
],
"type": "object"
},
"cabana.Envelope-cabana_LangBundle": {
"properties": {
"data": {
"$ref": "#/components/schemas/cabana.LangBundle"
},
"meta": {
"$ref": "#/components/schemas/cabana.SuccessMeta"
}
},
"required": [
"data",
"meta"
],
"type": "object"
},
"cabana.Envelope-cabana_ListSchema": {
"properties": {
"data": {
@@ -169,6 +199,21 @@
],
"type": "object"
},
"cabana.Envelope-cabana_RelationSchema": {
"properties": {
"data": {
"$ref": "#/components/schemas/cabana.RelationSchema"
},
"meta": {
"$ref": "#/components/schemas/cabana.SuccessMeta"
}
},
"required": [
"data",
"meta"
],
"type": "object"
},
"cabana.ErrorBody": {
"properties": {
"code": {
@@ -200,6 +245,99 @@
],
"type": "object"
},
"cabana.FormField": {
"properties": {
"attributes": {
"additionalProperties": {
"$ref": "#/components/schemas/cabana.jsonScalar"
},
"type": "object"
},
"comment": {
"type": "string"
},
"context": {
"$ref": "#/components/schemas/cabana.fieldContext"
},
"default": {
"$ref": "#/components/schemas/cabana.jsonScalar"
},
"emptyOption": {
"type": "string"
},
"label": {
"type": "string"
},
"multiple": {
"type": "boolean"
},
"name": {
"type": "string"
},
"nameFrom": {
"type": "string"
},
"options": {
"items": {
"$ref": "#/components/schemas/cabana.FormOption"
},
"type": "array"
},
"readOnly": {
"type": "boolean"
},
"relation": {
"type": "string"
},
"required": {
"type": "boolean"
},
"size": {
"type": "string"
},
"span": {
"type": "string"
},
"tab": {
"type": "string"
},
"type": {
"type": "string"
}
},
"required": [
"name",
"type"
],
"type": "object"
},
"cabana.FormMessages": {
"properties": {
"create": {
"$ref": "#/components/schemas/cabana.MessageForms"
},
"deleteConfirm": {
"$ref": "#/components/schemas/cabana.MessageForms"
},
"deleted": {
"$ref": "#/components/schemas/cabana.MessageForms"
},
"saved": {
"$ref": "#/components/schemas/cabana.MessageForms"
},
"update": {
"$ref": "#/components/schemas/cabana.MessageForms"
}
},
"required": [
"create",
"deleteConfirm",
"deleted",
"saved",
"update"
],
"type": "object"
},
"cabana.FormMeta": {
"properties": {
"locale": {
@@ -226,6 +364,88 @@
],
"type": "object"
},
"cabana.FormRedirect": {
"properties": {
"redirect": {
"type": "string"
},
"redirectClose": {
"type": "string"
}
},
"required": [
"redirect",
"redirectClose"
],
"type": "object"
},
"cabana.FormRedirects": {
"properties": {
"create": {
"$ref": "#/components/schemas/cabana.FormRedirect"
},
"default": {
"type": "string"
},
"update": {
"$ref": "#/components/schemas/cabana.FormRedirect"
}
},
"required": [
"create",
"default",
"update"
],
"type": "object"
},
"cabana.FormView": {
"properties": {
"fields": {
"items": {
"$ref": "#/components/schemas/cabana.FormField"
},
"type": "array"
},
"messages": {
"allOf": [
{
"$ref": "#/components/schemas/cabana.FormMessages"
}
],
"description": "Messages is the form's copy resolved in the request locale (D-13)."
},
"meta": {
"$ref": "#/components/schemas/cabana.FormMeta"
},
"modelClass": {
"type": "string"
},
"name": {
"type": "string"
},
"redirects": {
"allOf": [
{
"$ref": "#/components/schemas/cabana.FormRedirects"
}
],
"description": "Redirects are the raw Winter config_form.yaml targets; the SPA maps\nthem onto its routes."
}
},
"required": [
"fields",
"messages",
"meta",
"redirects"
],
"type": "object"
},
"cabana.LangBundle": {
"additionalProperties": {
"$ref": "#/components/schemas/cabana.MessageForms"
},
"type": "object"
},
"cabana.ListColumn": {
"properties": {
"key": {
@@ -337,6 +557,53 @@
],
"type": "object"
},
"cabana.ListMessages": {
"properties": {
"create": {
"$ref": "#/components/schemas/cabana.MessageForms"
},
"deleteConfirm": {
"$ref": "#/components/schemas/cabana.MessageForms"
},
"deleteSelected": {
"$ref": "#/components/schemas/cabana.MessageForms"
},
"deleted": {
"$ref": "#/components/schemas/cabana.MessageForms"
},
"empty": {
"$ref": "#/components/schemas/cabana.MessageForms"
},
"emptySearch": {
"$ref": "#/components/schemas/cabana.MessageForms"
},
"emptySearchHint": {
"$ref": "#/components/schemas/cabana.MessageForms"
},
"recordCount": {
"$ref": "#/components/schemas/cabana.MessageForms"
},
"searchPrompt": {
"$ref": "#/components/schemas/cabana.MessageForms"
},
"selected": {
"$ref": "#/components/schemas/cabana.MessageForms"
}
},
"required": [
"create",
"deleteConfirm",
"deleteSelected",
"deleted",
"empty",
"emptySearch",
"emptySearchHint",
"recordCount",
"searchPrompt",
"selected"
],
"type": "object"
},
"cabana.ListMeta": {
"properties": {
"last_page": {
@@ -383,6 +650,14 @@
},
"type": "array"
},
"messages": {
"allOf": [
{
"$ref": "#/components/schemas/cabana.ListMessages"
}
],
"description": "Messages is the list's copy (D-13). The cached schema carries each\nphrase key as its own form; a response resolves them in its locale."
},
"meta": {
"$ref": "#/components/schemas/cabana.FormMeta"
},
@@ -442,6 +717,7 @@
"bulkActions",
"columns",
"filters",
"messages",
"perPageOptions",
"recordsPerPage",
"rowActions",
@@ -469,6 +745,12 @@
],
"type": "object"
},
"cabana.MessageForms": {
"additionalProperties": {
"type": "string"
},
"type": "object"
},
"cabana.NavigationEntry": {
"properties": {
"code": {
@@ -535,6 +817,82 @@
],
"type": "object"
},
"cabana.RelationColumn": {
"properties": {
"key": {
"type": "string"
},
"label": {
"type": "string"
},
"searchable": {
"type": "boolean"
},
"sortable": {
"type": "boolean"
}
},
"required": [
"key",
"label",
"searchable",
"sortable"
],
"type": "object"
},
"cabana.RelationList": {
"properties": {
"columns": {
"items": {
"$ref": "#/components/schemas/cabana.RelationColumn"
},
"type": "array"
}
},
"required": [
"columns"
],
"type": "object"
},
"cabana.RelationMessages": {
"properties": {
"candidateSearch": {
"$ref": "#/components/schemas/cabana.MessageForms"
},
"empty": {
"$ref": "#/components/schemas/cabana.MessageForms"
},
"link": {
"$ref": "#/components/schemas/cabana.MessageForms"
},
"linkHint": {
"$ref": "#/components/schemas/cabana.MessageForms"
},
"linked": {
"$ref": "#/components/schemas/cabana.MessageForms"
},
"unlinkConfirm": {
"$ref": "#/components/schemas/cabana.MessageForms"
},
"unlinkSelected": {
"$ref": "#/components/schemas/cabana.MessageForms"
},
"unlinked": {
"$ref": "#/components/schemas/cabana.MessageForms"
}
},
"required": [
"candidateSearch",
"empty",
"link",
"linkHint",
"linked",
"unlinkConfirm",
"unlinkSelected",
"unlinked"
],
"type": "object"
},
"cabana.RelationOption": {
"properties": {
"label": {
@@ -550,6 +908,60 @@
],
"type": "object"
},
"cabana.RelationPanel": {
"properties": {
"list": {
"$ref": "#/components/schemas/cabana.RelationList"
},
"showSearch": {
"type": "boolean"
},
"toolbarButtons": {
"items": {
"type": "string"
},
"type": "array"
}
},
"required": [
"list",
"showSearch",
"toolbarButtons"
],
"type": "object"
},
"cabana.RelationSchema": {
"properties": {
"label": {
"type": "string"
},
"manage": {
"$ref": "#/components/schemas/cabana.RelationPanel"
},
"messages": {
"allOf": [
{
"$ref": "#/components/schemas/cabana.RelationMessages"
}
],
"description": "Messages is the relation manager's copy (D-13); the cached schema\ncarries each phrase key as its own form."
},
"name": {
"type": "string"
},
"view": {
"$ref": "#/components/schemas/cabana.RelationPanel"
}
},
"required": [
"label",
"manage",
"messages",
"name",
"view"
],
"type": "object"
},
"cabana.RowAction": {
"properties": {
"label": {
@@ -600,6 +1012,19 @@
},
"type": "object"
},
"cabana.fieldContext": {
"oneOf": [
{
"type": "string"
},
{
"items": {
"type": "string"
},
"type": "array"
}
]
},
"cabana.jsonScalar": {
"nullable": true,
"oneOf": [
@@ -803,6 +1228,27 @@
]
}
},
"/lang": {
"get": {
"description": "Every backend::lang key as CLDR plural forms for the Accept-Language locale, over the fallback locale's keys. Public: the login screen loads it before signing in. meta.locale is the locale the bundle resolved to.",
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/cabana.Envelope-cabana_LangBundle"
}
}
},
"description": "OK"
}
},
"summary": "Admin UI strings",
"tags": [
"admin"
]
}
},
"/navigation": {
"get": {
"responses": {
@@ -1544,7 +1990,7 @@
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/cabana.SuccessEnvelope"
"$ref": "#/components/schemas/cabana.Envelope-cabana_FormView"
}
}
},
@@ -1721,7 +2167,7 @@
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/cabana.SuccessEnvelope"
"$ref": "#/components/schemas/cabana.Envelope-cabana_RelationSchema"
}
}
},