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

@@ -204,6 +204,45 @@ export interface paths {
patch?: never;
trace?: never;
};
"/lang": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/**
* Admin UI strings
* @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.
*/
get: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description OK */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["cabana.Envelope-cabana_LangBundle"];
};
};
};
};
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/navigation": {
parameters: {
query?: never;
@@ -823,7 +862,7 @@ export interface paths {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["cabana.SuccessEnvelope"];
"application/json": components["schemas"]["cabana.Envelope-cabana_FormView"];
};
};
/** @description Unauthorized */
@@ -965,7 +1004,7 @@ export interface paths {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["cabana.SuccessEnvelope"];
"application/json": components["schemas"]["cabana.Envelope-cabana_RelationSchema"];
};
};
/** @description Unauthorized */
@@ -1523,10 +1562,22 @@ export interface components {
data: components["schemas"]["cabana.AdminProfile"];
meta: components["schemas"]["cabana.SuccessMeta"];
};
"cabana.Envelope-cabana_FormView": {
data: components["schemas"]["cabana.FormView"];
meta: components["schemas"]["cabana.SuccessMeta"];
};
"cabana.Envelope-cabana_LangBundle": {
data: components["schemas"]["cabana.LangBundle"];
meta: components["schemas"]["cabana.SuccessMeta"];
};
"cabana.Envelope-cabana_ListSchema": {
data: components["schemas"]["cabana.ListSchema"];
meta: components["schemas"]["cabana.SuccessMeta"];
};
"cabana.Envelope-cabana_RelationSchema": {
data: components["schemas"]["cabana.RelationSchema"];
meta: components["schemas"]["cabana.SuccessMeta"];
};
"cabana.ErrorBody": {
code: string;
details: {
@@ -1537,6 +1588,34 @@ export interface components {
"cabana.ErrorEnvelope": {
error: components["schemas"]["cabana.ErrorBody"];
};
"cabana.FormField": {
attributes?: {
[key: string]: components["schemas"]["cabana.jsonScalar"];
};
comment?: string;
context?: components["schemas"]["cabana.fieldContext"];
default?: components["schemas"]["cabana.jsonScalar"];
emptyOption?: string;
label?: string;
multiple?: boolean;
name: string;
nameFrom?: string;
options?: components["schemas"]["cabana.FormOption"][];
readOnly?: boolean;
relation?: string;
required?: boolean;
size?: string;
span?: string;
tab?: string;
type: string;
};
"cabana.FormMessages": {
create: components["schemas"]["cabana.MessageForms"];
deleteConfirm: components["schemas"]["cabana.MessageForms"];
deleted: components["schemas"]["cabana.MessageForms"];
saved: components["schemas"]["cabana.MessageForms"];
update: components["schemas"]["cabana.MessageForms"];
};
"cabana.FormMeta": {
locale: string;
};
@@ -1544,6 +1623,31 @@ export interface components {
label: string;
value: components["schemas"]["cabana.jsonScalar"];
};
"cabana.FormRedirect": {
redirect: string;
redirectClose: string;
};
"cabana.FormRedirects": {
create: components["schemas"]["cabana.FormRedirect"];
default: string;
update: components["schemas"]["cabana.FormRedirect"];
};
"cabana.FormView": {
fields: components["schemas"]["cabana.FormField"][];
/** @description Messages is the form's copy resolved in the request locale (D-13). */
messages: components["schemas"]["cabana.FormMessages"];
meta: components["schemas"]["cabana.FormMeta"];
modelClass?: string;
name?: string;
/**
* @description Redirects are the raw Winter config_form.yaml targets; the SPA maps
* them onto its routes.
*/
redirects: components["schemas"]["cabana.FormRedirects"];
};
"cabana.LangBundle": {
[key: string]: components["schemas"]["cabana.MessageForms"];
};
"cabana.ListColumn": {
key: string;
label: string;
@@ -1575,6 +1679,18 @@ export interface components {
trueValue?: components["schemas"]["cabana.jsonScalar"];
type: string;
};
"cabana.ListMessages": {
create: components["schemas"]["cabana.MessageForms"];
deleteConfirm: components["schemas"]["cabana.MessageForms"];
deleteSelected: components["schemas"]["cabana.MessageForms"];
deleted: components["schemas"]["cabana.MessageForms"];
empty: components["schemas"]["cabana.MessageForms"];
emptySearch: components["schemas"]["cabana.MessageForms"];
emptySearchHint: components["schemas"]["cabana.MessageForms"];
recordCount: components["schemas"]["cabana.MessageForms"];
searchPrompt: components["schemas"]["cabana.MessageForms"];
selected: components["schemas"]["cabana.MessageForms"];
};
"cabana.ListMeta": {
last_page: number;
page: number;
@@ -1586,6 +1702,11 @@ export interface components {
columns: components["schemas"]["cabana.ListColumn"][];
defaultSort?: components["schemas"]["cabana.ListSort"];
filters: components["schemas"]["cabana.ListFilter"][];
/**
* @description Messages is the list's copy (D-13). The cached schema carries each
* phrase key as its own form; a response resolves them in its locale.
*/
messages: components["schemas"]["cabana.ListMessages"];
meta?: components["schemas"]["cabana.FormMeta"];
modelClass?: string;
noRecordsMessage?: string;
@@ -1606,6 +1727,9 @@ export interface components {
column: string;
direction: string;
};
"cabana.MessageForms": {
[key: string]: string;
};
"cabana.NavigationEntry": {
code: string;
controller: string;
@@ -1623,10 +1747,45 @@ export interface components {
[key: string]: components["schemas"]["cabana.RelationOption"][];
};
};
"cabana.RelationColumn": {
key: string;
label: string;
searchable: boolean;
sortable: boolean;
};
"cabana.RelationList": {
columns: components["schemas"]["cabana.RelationColumn"][];
};
"cabana.RelationMessages": {
candidateSearch: components["schemas"]["cabana.MessageForms"];
empty: components["schemas"]["cabana.MessageForms"];
link: components["schemas"]["cabana.MessageForms"];
linkHint: components["schemas"]["cabana.MessageForms"];
linked: components["schemas"]["cabana.MessageForms"];
unlinkConfirm: components["schemas"]["cabana.MessageForms"];
unlinkSelected: components["schemas"]["cabana.MessageForms"];
unlinked: components["schemas"]["cabana.MessageForms"];
};
"cabana.RelationOption": {
label: string;
value: number;
};
"cabana.RelationPanel": {
list: components["schemas"]["cabana.RelationList"];
showSearch: boolean;
toolbarButtons: string[];
};
"cabana.RelationSchema": {
label: string;
manage: components["schemas"]["cabana.RelationPanel"];
/**
* @description Messages is the relation manager's copy (D-13); the cached schema
* carries each phrase key as its own form.
*/
messages: components["schemas"]["cabana.RelationMessages"];
name: string;
view: components["schemas"]["cabana.RelationPanel"];
};
"cabana.RowAction": {
label?: string;
name: string;
@@ -1643,6 +1802,7 @@ export interface components {
per_page?: number;
total?: number;
};
"cabana.fieldContext": string | string[];
"cabana.jsonScalar": (string | number | boolean) | null;
};
responses: never;