sbuilder-mcp 0.15.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/CHANGELOG.vi.md +10 -0
- package/dist/catalog/element-search.js +19 -0
- package/dist/catalog/fieldskin.generated.js +162 -0
- package/dist/catalog/search.js +9 -0
- package/dist/catalog/shapes.generated.js +1 -1
- package/dist/catalog/translations.generated.js +656 -0
- package/dist/domains/site/fieldskin.js +59 -0
- package/dist/domains/site/translate.js +72 -0
- package/dist/tools/page.js +26 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,16 @@ All notable changes to this project are documented in this file.
|
|
|
6
6
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
7
7
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
8
8
|
|
|
9
|
+
## [0.16.0] - 2026-09-10
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- sb_traits_for now returns a `translatable` block naming which of an element's specials a translation may safely rewrite, and which of its own specials must never be translated, since translating a non-content special (a lucide icon `name`, a `src` URL, a `filterSource` registry id) does not degrade the page, it breaks the render.
|
|
13
|
+
- sb_api_find and sb_api_call now attach a `translation_fields` call sheet to every `/translations` operation, listing the translatable columns for each entity type and pointing to sb_traits_for for the per-element `node` vocabulary, since every translations route was already reachable with no way to know which fields were safe to send.
|
|
14
|
+
- sb_set now warns once per node type when a field-skin config key (`payCardBg`, `choice*`, `slot*`, `file*`, and the rest of the 55-key vocabulary) is written on a form node whose renderer does not read it, naming the field node that actually renders it, since such a write is stored, saved and published but rendered nowhere.
|
|
15
|
+
|
|
16
|
+
### Internal
|
|
17
|
+
- The generated catalog gained a translations table (`translations.generated.ts`, 141 translatable specials across 58 elements, 156 keys classified as never-translate, 12 entity types with their columns) and a field-skin table (`fieldskin.generated.ts`), both cross-checked at codegen time against the platform's own registries rather than hand-kept.
|
|
18
|
+
|
|
9
19
|
## [0.15.0] - 2026-09-10
|
|
10
20
|
|
|
11
21
|
### Added
|
package/CHANGELOG.vi.md
CHANGED
|
@@ -6,6 +6,16 @@ Mọi thay đổi đáng chú ý của dự án được ghi lại trong file n
|
|
|
6
6
|
Định dạng dựa trên [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
7
7
|
và dự án tuân theo [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
8
8
|
|
|
9
|
+
## [0.16.0] - 2026-09-10
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- sb_traits_for giờ trả về một khối `translatable` nêu tên những special nào của element có thể an toàn để một bản dịch ghi đè, và những special riêng của nó không bao giờ được dịch, vì dịch một special không phải nội dung (một `name` icon lucide, một URL `src`, một id registry `filterSource`) không làm trang xấu đi, mà làm hỏng hẳn phần render.
|
|
13
|
+
- sb_api_find và sb_api_call giờ đính kèm một call sheet `translation_fields` vào mọi operation `/translations`, liệt kê các cột có thể dịch cho từng loại entity và trỏ tới sb_traits_for để lấy vocabulary riêng theo từng element cho entity `node`, vì trước đây mọi route translations đều đã gọi được mà không có cách nào biết field nào an toàn để gửi.
|
|
14
|
+
- sb_set giờ cảnh báo một lần cho mỗi loại node khi một config key thuộc field-skin (`payCardBg`, `choice*`, `slot*`, `file*`, và phần còn lại trong vocabulary 55 key) được ghi trên một form node mà renderer của nó không đọc key đó, nêu tên field node thực sự render nó, vì việc ghi này vẫn được lưu, save và publish nhưng không được render ở đâu cả.
|
|
15
|
+
|
|
16
|
+
### Internal
|
|
17
|
+
- Catalog được sinh ra giờ có thêm bảng translations (`translations.generated.ts`, 141 special có thể dịch trên 58 element, 156 key được phân loại không bao giờ dịch, 12 loại entity cùng các cột của chúng) và bảng field-skin (`fieldskin.generated.ts`), cả hai đều được đối chiếu chéo lúc codegen với chính các registry của nền tảng thay vì giữ thủ công.
|
|
18
|
+
|
|
9
19
|
## [0.15.0] - 2026-09-10
|
|
10
20
|
|
|
11
21
|
### Added
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ELEMENTS, TRAIT_WRITES } from './elements.generated.js';
|
|
2
2
|
import { vocabulariesForWrites } from '../domains/site/vocabulary.js';
|
|
3
|
+
import { neverTranslatedOn, translatableSpecials } from '../domains/site/translate.js';
|
|
3
4
|
/** Eight to choose from; the hints for the chosen one come with sb_traits_for. */
|
|
4
5
|
export const DEFAULT_CATALOG_LIMIT = 8;
|
|
5
6
|
/**
|
|
@@ -95,6 +96,24 @@ export function traitsFor(type, control) {
|
|
|
95
96
|
// which, unlike `style`, are NOT open — this is the machine-readable
|
|
96
97
|
// answer to "what does this element store", and often the only one.
|
|
97
98
|
defaults: el.defaults,
|
|
99
|
+
// WHICH OF THIS ELEMENT'S STRINGS A TRANSLATION MAY REWRITE. Translating one
|
|
100
|
+
// of the others does not degrade the page, it BREAKS the render — `name` is
|
|
101
|
+
// a lucide icon id, `src` a URL, `filterSource` a registry id the renderer
|
|
102
|
+
// switches on. An empty list is the complete answer for an element whose
|
|
103
|
+
// only string is one of those, so it is reported rather than omitted.
|
|
104
|
+
...(() => {
|
|
105
|
+
const may = translatableSpecials(el.type);
|
|
106
|
+
const own = Object.keys(el.defaults?.specials ?? {});
|
|
107
|
+
const never = neverTranslatedOn(el.type, own);
|
|
108
|
+
if (!may.length && !never.length)
|
|
109
|
+
return {};
|
|
110
|
+
return {
|
|
111
|
+
translatable: {
|
|
112
|
+
specials: may,
|
|
113
|
+
...(never.length ? { never_translate: never } : {}),
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
})(),
|
|
98
117
|
// WHAT THOSE KEYS ARE ALLOWED TO HOLD, for the few where guessing wrong is
|
|
99
118
|
// silent. Attached to the ELEMENT rather than to a control, because the keys
|
|
100
119
|
// that most need it are exactly the UNDECLARED ones: `collectionType` is in
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
// GENERATED by scripts/gen-catalog.ts — do not edit by hand.
|
|
2
|
+
// Source: <WB_REPO>/schema/src/elements/fieldSkin.ts (the vocabulary) and each
|
|
3
|
+
// server/render/nodes/form*/css.go (which node emits which group).
|
|
4
|
+
export const FIELD_SKIN_SOURCE = {
|
|
5
|
+
"nodes": 11,
|
|
6
|
+
"keys": 55
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* The field-skin config keys each form node's renderer actually reads.
|
|
10
|
+
*
|
|
11
|
+
* A knob written on a node that is not in its list is stored, saved, published
|
|
12
|
+
* and rendered NOWHERE — the documented case being payCard* on the FORM, which
|
|
13
|
+
* emits only the input vocabulary. Composed from the TS groups and asserted
|
|
14
|
+
* against the Go's own key literals at codegen.
|
|
15
|
+
*/
|
|
16
|
+
export const FIELD_SKIN_BY_NODE = {
|
|
17
|
+
"form": [
|
|
18
|
+
"fieldStackGap",
|
|
19
|
+
"fieldReqColor",
|
|
20
|
+
"fieldHintOpacity",
|
|
21
|
+
"fieldBg",
|
|
22
|
+
"fieldBorderColor",
|
|
23
|
+
"fieldBorderWidth",
|
|
24
|
+
"fieldRadius",
|
|
25
|
+
"fieldPadY",
|
|
26
|
+
"fieldPadX"
|
|
27
|
+
],
|
|
28
|
+
"form-calendar": [
|
|
29
|
+
"fieldStackGap",
|
|
30
|
+
"fieldReqColor",
|
|
31
|
+
"fieldHintOpacity",
|
|
32
|
+
"fieldBg",
|
|
33
|
+
"fieldBorderColor",
|
|
34
|
+
"fieldBorderWidth",
|
|
35
|
+
"fieldRadius",
|
|
36
|
+
"fieldPadY",
|
|
37
|
+
"fieldPadX"
|
|
38
|
+
],
|
|
39
|
+
"form-checkbox": [
|
|
40
|
+
"fieldStackGap",
|
|
41
|
+
"fieldReqColor",
|
|
42
|
+
"fieldHintOpacity",
|
|
43
|
+
"choiceRowGap",
|
|
44
|
+
"choiceColGap",
|
|
45
|
+
"choiceGap",
|
|
46
|
+
"choiceSize",
|
|
47
|
+
"choiceBorderWidth",
|
|
48
|
+
"choiceBorderColor",
|
|
49
|
+
"choiceBg",
|
|
50
|
+
"choiceAccent",
|
|
51
|
+
"choiceRadius",
|
|
52
|
+
"choiceMarkWidth"
|
|
53
|
+
],
|
|
54
|
+
"form-date": [
|
|
55
|
+
"fieldStackGap",
|
|
56
|
+
"fieldReqColor",
|
|
57
|
+
"fieldHintOpacity",
|
|
58
|
+
"fieldBg",
|
|
59
|
+
"fieldBorderColor",
|
|
60
|
+
"fieldBorderWidth",
|
|
61
|
+
"fieldRadius",
|
|
62
|
+
"fieldPadY",
|
|
63
|
+
"fieldPadX"
|
|
64
|
+
],
|
|
65
|
+
"form-file": [
|
|
66
|
+
"fieldStackGap",
|
|
67
|
+
"fieldReqColor",
|
|
68
|
+
"fieldHintOpacity",
|
|
69
|
+
"fileGap",
|
|
70
|
+
"filePlaceholderWeight"
|
|
71
|
+
],
|
|
72
|
+
"form-number": [
|
|
73
|
+
"fieldStackGap",
|
|
74
|
+
"fieldReqColor",
|
|
75
|
+
"fieldHintOpacity",
|
|
76
|
+
"fieldBg",
|
|
77
|
+
"fieldBorderColor",
|
|
78
|
+
"fieldBorderWidth",
|
|
79
|
+
"fieldRadius",
|
|
80
|
+
"fieldPadY",
|
|
81
|
+
"fieldPadX"
|
|
82
|
+
],
|
|
83
|
+
"form-payment": [
|
|
84
|
+
"fieldStackGap",
|
|
85
|
+
"fieldReqColor",
|
|
86
|
+
"fieldHintOpacity",
|
|
87
|
+
"payCardBg",
|
|
88
|
+
"payCardBorderColor",
|
|
89
|
+
"payCardBorderWidth",
|
|
90
|
+
"payCardRadius",
|
|
91
|
+
"payCardPadY",
|
|
92
|
+
"payCardPadX",
|
|
93
|
+
"payCardGap",
|
|
94
|
+
"payCardRowGap",
|
|
95
|
+
"payCardAccent",
|
|
96
|
+
"payCardSelectedBg",
|
|
97
|
+
"payCardHoverColor",
|
|
98
|
+
"payCardHoverBg",
|
|
99
|
+
"payCardText",
|
|
100
|
+
"payCardNameWeight",
|
|
101
|
+
"payCardDescOpacity",
|
|
102
|
+
"payCardBodyGap",
|
|
103
|
+
"payMarkerSize",
|
|
104
|
+
"payMarkerDot"
|
|
105
|
+
],
|
|
106
|
+
"form-radio": [
|
|
107
|
+
"fieldStackGap",
|
|
108
|
+
"fieldReqColor",
|
|
109
|
+
"fieldHintOpacity",
|
|
110
|
+
"choiceRowGap",
|
|
111
|
+
"choiceColGap",
|
|
112
|
+
"choiceGap",
|
|
113
|
+
"choiceSize",
|
|
114
|
+
"choiceBorderWidth",
|
|
115
|
+
"choiceBorderColor",
|
|
116
|
+
"choiceBg",
|
|
117
|
+
"choiceAccent",
|
|
118
|
+
"choiceDot"
|
|
119
|
+
],
|
|
120
|
+
"form-select": [
|
|
121
|
+
"fieldStackGap",
|
|
122
|
+
"fieldReqColor",
|
|
123
|
+
"fieldHintOpacity",
|
|
124
|
+
"fieldBg",
|
|
125
|
+
"fieldBorderColor",
|
|
126
|
+
"fieldBorderWidth",
|
|
127
|
+
"fieldRadius",
|
|
128
|
+
"fieldPadY",
|
|
129
|
+
"fieldPadX"
|
|
130
|
+
],
|
|
131
|
+
"form-text": [
|
|
132
|
+
"fieldStackGap",
|
|
133
|
+
"fieldReqColor",
|
|
134
|
+
"fieldHintOpacity",
|
|
135
|
+
"fieldBg",
|
|
136
|
+
"fieldBorderColor",
|
|
137
|
+
"fieldBorderWidth",
|
|
138
|
+
"fieldRadius",
|
|
139
|
+
"fieldPadY",
|
|
140
|
+
"fieldPadX"
|
|
141
|
+
],
|
|
142
|
+
"form-timeslot": [
|
|
143
|
+
"fieldStackGap",
|
|
144
|
+
"fieldReqColor",
|
|
145
|
+
"fieldHintOpacity",
|
|
146
|
+
"slotBg",
|
|
147
|
+
"slotBorderColor",
|
|
148
|
+
"slotBorderWidth",
|
|
149
|
+
"slotRadius",
|
|
150
|
+
"slotPadY",
|
|
151
|
+
"slotPadX",
|
|
152
|
+
"slotGap",
|
|
153
|
+
"slotMinWidth",
|
|
154
|
+
"slotMinHeight",
|
|
155
|
+
"slotAccent",
|
|
156
|
+
"slotHoverColor",
|
|
157
|
+
"slotSelectedWeight",
|
|
158
|
+
"slotMarkerSize",
|
|
159
|
+
"slotMarkerDot",
|
|
160
|
+
"slotMarkerGap"
|
|
161
|
+
]
|
|
162
|
+
};
|
package/dist/catalog/search.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { API_OPERATIONS, API_DEFINITIONS } from './api.generated.js';
|
|
2
2
|
import { REQUEST_SHAPES } from './shapes.generated.js';
|
|
3
|
+
import { translationCallSheet } from '../domains/site/translate.js';
|
|
3
4
|
/**
|
|
4
5
|
* Term-hit scoring, weighted by field, and deliberately NOT fuzzy.
|
|
5
6
|
*
|
|
@@ -82,6 +83,14 @@ export function describeOperation(op) {
|
|
|
82
83
|
// The two warnings below are dropped when a shape is present: they exist
|
|
83
84
|
// because the shape was unknown, and keeping them once it is known is prose
|
|
84
85
|
// the model has to read past on its way to the answer.
|
|
86
|
+
// A TRANSLATIONS OPERATION IS REACHABLE AND WAS UNSAFE. Every route here is in
|
|
87
|
+
// the catalog, so an agent could call them all and had no way to know which
|
|
88
|
+
// fields are content — and translating the wrong special BREAKS the render
|
|
89
|
+
// rather than degrading it. Attached to the call sheet because that is where
|
|
90
|
+
// the agent already is when it decides what to send.
|
|
91
|
+
if (/\/translations(\/|$)/.test(op.path)) {
|
|
92
|
+
out.translation_fields = translationCallSheet();
|
|
93
|
+
}
|
|
85
94
|
const shape = REQUEST_SHAPES[op.id];
|
|
86
95
|
if (shape) {
|
|
87
96
|
out.body_shape = shape;
|
|
@@ -0,0 +1,656 @@
|
|
|
1
|
+
// GENERATED by scripts/gen-catalog.ts — do not edit by hand.
|
|
2
|
+
// Source: <WB_REPO>/schema/src/elements/translatableFields.ts
|
|
3
|
+
export const TRANSLATION_SOURCE = {
|
|
4
|
+
"elements": 58,
|
|
5
|
+
"pairs": 141,
|
|
6
|
+
"neverKeys": 156,
|
|
7
|
+
"entityTypes": 12
|
|
8
|
+
};
|
|
9
|
+
/** Every entity type a translation record can name, including "node". */
|
|
10
|
+
export const TRANSLATION_ENTITY_TYPES = [
|
|
11
|
+
"product",
|
|
12
|
+
"category",
|
|
13
|
+
"article",
|
|
14
|
+
"blogCategory",
|
|
15
|
+
"course",
|
|
16
|
+
"review",
|
|
17
|
+
"courseSection",
|
|
18
|
+
"courseLesson",
|
|
19
|
+
"node",
|
|
20
|
+
"page",
|
|
21
|
+
"uiString",
|
|
22
|
+
"mailString",
|
|
23
|
+
"siteLabel"
|
|
24
|
+
];
|
|
25
|
+
/**
|
|
26
|
+
* The specials a translation MAY rewrite, per element type.
|
|
27
|
+
*
|
|
28
|
+
* An element absent here has none. That is not an oversight: an icon's only
|
|
29
|
+
* string is a lucide id.
|
|
30
|
+
*/
|
|
31
|
+
export const TRANSLATABLE_SPECIALS = {
|
|
32
|
+
"popup": [
|
|
33
|
+
"closeLabel"
|
|
34
|
+
],
|
|
35
|
+
"select": [
|
|
36
|
+
"placeholder",
|
|
37
|
+
"filterValues"
|
|
38
|
+
],
|
|
39
|
+
"filter-checkbox": [
|
|
40
|
+
"filterValues"
|
|
41
|
+
],
|
|
42
|
+
"filter-radio": [
|
|
43
|
+
"filterValues"
|
|
44
|
+
],
|
|
45
|
+
"filter-color": [
|
|
46
|
+
"filterValues"
|
|
47
|
+
],
|
|
48
|
+
"filter-tag": [
|
|
49
|
+
"filterValues"
|
|
50
|
+
],
|
|
51
|
+
"account-info": [
|
|
52
|
+
"promptText",
|
|
53
|
+
"loginLabel"
|
|
54
|
+
],
|
|
55
|
+
"cart-count": [
|
|
56
|
+
"placeholder"
|
|
57
|
+
],
|
|
58
|
+
"cart-total": [
|
|
59
|
+
"label",
|
|
60
|
+
"placeholder"
|
|
61
|
+
],
|
|
62
|
+
"order-history": [
|
|
63
|
+
"title",
|
|
64
|
+
"emptyText",
|
|
65
|
+
"signedOutText",
|
|
66
|
+
"lookupNumberPlaceholder",
|
|
67
|
+
"lookupContactPlaceholder",
|
|
68
|
+
"lookupButtonText"
|
|
69
|
+
],
|
|
70
|
+
"order-receipt": [
|
|
71
|
+
"title",
|
|
72
|
+
"numberLabel",
|
|
73
|
+
"placedLabel",
|
|
74
|
+
"statusLabel",
|
|
75
|
+
"itemsLabel",
|
|
76
|
+
"totalLabel",
|
|
77
|
+
"dueLabel",
|
|
78
|
+
"unavailableText"
|
|
79
|
+
],
|
|
80
|
+
"course-outline": [
|
|
81
|
+
"emptyText"
|
|
82
|
+
],
|
|
83
|
+
"my-courses": [
|
|
84
|
+
"emptyText",
|
|
85
|
+
"signedOutText",
|
|
86
|
+
"continueText"
|
|
87
|
+
],
|
|
88
|
+
"course-player": [
|
|
89
|
+
"completeLabel",
|
|
90
|
+
"completedLabel",
|
|
91
|
+
"certificateText",
|
|
92
|
+
"lockedText",
|
|
93
|
+
"lockedLabel",
|
|
94
|
+
"emptyText"
|
|
95
|
+
],
|
|
96
|
+
"wishlist-list": [
|
|
97
|
+
"title",
|
|
98
|
+
"emptyText"
|
|
99
|
+
],
|
|
100
|
+
"address-book": [
|
|
101
|
+
"title",
|
|
102
|
+
"emptyText",
|
|
103
|
+
"signedOutText",
|
|
104
|
+
"addLabel"
|
|
105
|
+
],
|
|
106
|
+
"points-card": [
|
|
107
|
+
"title",
|
|
108
|
+
"signedOutText"
|
|
109
|
+
],
|
|
110
|
+
"member-field": [
|
|
111
|
+
"prefix",
|
|
112
|
+
"suffix",
|
|
113
|
+
"guestText"
|
|
114
|
+
],
|
|
115
|
+
"accordion-content": [
|
|
116
|
+
"label"
|
|
117
|
+
],
|
|
118
|
+
"button": [
|
|
119
|
+
"text",
|
|
120
|
+
"textHtml"
|
|
121
|
+
],
|
|
122
|
+
"cart-order": [
|
|
123
|
+
"emptyText"
|
|
124
|
+
],
|
|
125
|
+
"payment-status": [
|
|
126
|
+
"paidTitle",
|
|
127
|
+
"paidText",
|
|
128
|
+
"pendingTitle",
|
|
129
|
+
"pendingText",
|
|
130
|
+
"failedTitle",
|
|
131
|
+
"failedText",
|
|
132
|
+
"retryLabel",
|
|
133
|
+
"referenceLabel"
|
|
134
|
+
],
|
|
135
|
+
"collection-media": [
|
|
136
|
+
"alt"
|
|
137
|
+
],
|
|
138
|
+
"divider": [
|
|
139
|
+
"label"
|
|
140
|
+
],
|
|
141
|
+
"heading": [
|
|
142
|
+
"text",
|
|
143
|
+
"textHtml"
|
|
144
|
+
],
|
|
145
|
+
"form-text": [
|
|
146
|
+
"label",
|
|
147
|
+
"placeholder",
|
|
148
|
+
"description"
|
|
149
|
+
],
|
|
150
|
+
"form-number": [
|
|
151
|
+
"label",
|
|
152
|
+
"placeholder",
|
|
153
|
+
"description"
|
|
154
|
+
],
|
|
155
|
+
"form-select": [
|
|
156
|
+
"label",
|
|
157
|
+
"options",
|
|
158
|
+
"placeholder",
|
|
159
|
+
"description"
|
|
160
|
+
],
|
|
161
|
+
"form-address": [
|
|
162
|
+
"label",
|
|
163
|
+
"description",
|
|
164
|
+
"provinceLabel",
|
|
165
|
+
"wardLabel",
|
|
166
|
+
"detailLabel",
|
|
167
|
+
"provincePlaceholder",
|
|
168
|
+
"wardPlaceholder",
|
|
169
|
+
"detailPlaceholder",
|
|
170
|
+
"placeholder"
|
|
171
|
+
],
|
|
172
|
+
"form-radio": [
|
|
173
|
+
"label",
|
|
174
|
+
"options",
|
|
175
|
+
"description"
|
|
176
|
+
],
|
|
177
|
+
"form-payment": [
|
|
178
|
+
"label",
|
|
179
|
+
"description",
|
|
180
|
+
"methods"
|
|
181
|
+
],
|
|
182
|
+
"form-timeslot": [
|
|
183
|
+
"label",
|
|
184
|
+
"description"
|
|
185
|
+
],
|
|
186
|
+
"form-checkbox": [
|
|
187
|
+
"label",
|
|
188
|
+
"options",
|
|
189
|
+
"description"
|
|
190
|
+
],
|
|
191
|
+
"form-date": [
|
|
192
|
+
"label",
|
|
193
|
+
"description",
|
|
194
|
+
"dayPlaceholder",
|
|
195
|
+
"monthPlaceholder",
|
|
196
|
+
"yearPlaceholder",
|
|
197
|
+
"dayLabel",
|
|
198
|
+
"monthLabel",
|
|
199
|
+
"yearLabel",
|
|
200
|
+
"timeLabel"
|
|
201
|
+
],
|
|
202
|
+
"form-calendar": [
|
|
203
|
+
"label",
|
|
204
|
+
"placeholder",
|
|
205
|
+
"description"
|
|
206
|
+
],
|
|
207
|
+
"form-file": [
|
|
208
|
+
"label",
|
|
209
|
+
"placeholder",
|
|
210
|
+
"hintText",
|
|
211
|
+
"description",
|
|
212
|
+
"buttonLabel"
|
|
213
|
+
],
|
|
214
|
+
"form-paragraph": [
|
|
215
|
+
"text"
|
|
216
|
+
],
|
|
217
|
+
"form-step-button": [
|
|
218
|
+
"text"
|
|
219
|
+
],
|
|
220
|
+
"form-submit": [
|
|
221
|
+
"text",
|
|
222
|
+
"submittingText"
|
|
223
|
+
],
|
|
224
|
+
"form-title": [
|
|
225
|
+
"text"
|
|
226
|
+
],
|
|
227
|
+
"image": [
|
|
228
|
+
"alt"
|
|
229
|
+
],
|
|
230
|
+
"image-comparison": [
|
|
231
|
+
"beforeAlt",
|
|
232
|
+
"afterAlt"
|
|
233
|
+
],
|
|
234
|
+
"list-dataset": [
|
|
235
|
+
"loadMoreLabel"
|
|
236
|
+
],
|
|
237
|
+
"list-item": [
|
|
238
|
+
"text"
|
|
239
|
+
],
|
|
240
|
+
"product-image-feature": [
|
|
241
|
+
"alt"
|
|
242
|
+
],
|
|
243
|
+
"search-input": [
|
|
244
|
+
"searchPlaceholder",
|
|
245
|
+
"searchClearLabel",
|
|
246
|
+
"searchToggleLabel"
|
|
247
|
+
],
|
|
248
|
+
"tab-content": [
|
|
249
|
+
"label"
|
|
250
|
+
],
|
|
251
|
+
"text": [
|
|
252
|
+
"text",
|
|
253
|
+
"textHtml"
|
|
254
|
+
],
|
|
255
|
+
"text-marquee": [
|
|
256
|
+
"text"
|
|
257
|
+
],
|
|
258
|
+
"text-marquee-item": [
|
|
259
|
+
"text"
|
|
260
|
+
],
|
|
261
|
+
"breadcrumb": [
|
|
262
|
+
"crumbs",
|
|
263
|
+
"homeLabel"
|
|
264
|
+
],
|
|
265
|
+
"menu": [
|
|
266
|
+
"menuItems"
|
|
267
|
+
],
|
|
268
|
+
"chat-widget": [
|
|
269
|
+
"title"
|
|
270
|
+
],
|
|
271
|
+
"qr-code": [
|
|
272
|
+
"alt",
|
|
273
|
+
"value"
|
|
274
|
+
],
|
|
275
|
+
"bundle-items": [
|
|
276
|
+
"heading",
|
|
277
|
+
"quantitySeparator"
|
|
278
|
+
],
|
|
279
|
+
"search-keywords": [
|
|
280
|
+
"keywordItems"
|
|
281
|
+
],
|
|
282
|
+
"locale-switcher": [
|
|
283
|
+
"panelTitle"
|
|
284
|
+
],
|
|
285
|
+
"theme-switcher": [
|
|
286
|
+
"lightLabel",
|
|
287
|
+
"darkLabel"
|
|
288
|
+
]
|
|
289
|
+
};
|
|
290
|
+
/**
|
|
291
|
+
* Specials keys that must NEVER be translated, across every element.
|
|
292
|
+
*
|
|
293
|
+
* Translating one of these does not degrade the page — it BREAKS the render.
|
|
294
|
+
* Kept as the platform's whole classification rather than an allow-list,
|
|
295
|
+
* because a positive-only list goes stale silently as elements ship new strings.
|
|
296
|
+
*/
|
|
297
|
+
export const NEVER_TRANSLATED = [
|
|
298
|
+
"acceptedDates",
|
|
299
|
+
"addressGroup",
|
|
300
|
+
"addressLink",
|
|
301
|
+
"afterSrc",
|
|
302
|
+
"allowMultiple",
|
|
303
|
+
"allowedCodes",
|
|
304
|
+
"arrow",
|
|
305
|
+
"audience",
|
|
306
|
+
"authKind",
|
|
307
|
+
"autoComplete",
|
|
308
|
+
"availableDays",
|
|
309
|
+
"beforeSrc",
|
|
310
|
+
"bookMaxDays",
|
|
311
|
+
"bookMinDays",
|
|
312
|
+
"bookSlots",
|
|
313
|
+
"boundBundleItems",
|
|
314
|
+
"boundCompareCents",
|
|
315
|
+
"boundHref",
|
|
316
|
+
"boundHrefLabel",
|
|
317
|
+
"boundMoneyOverride",
|
|
318
|
+
"boundPriceCents",
|
|
319
|
+
"boundProductURL",
|
|
320
|
+
"boundValue",
|
|
321
|
+
"breakEnd",
|
|
322
|
+
"breakStart",
|
|
323
|
+
"button",
|
|
324
|
+
"capturesProduct",
|
|
325
|
+
"className",
|
|
326
|
+
"closeIcon",
|
|
327
|
+
"code",
|
|
328
|
+
"collectionIds",
|
|
329
|
+
"contentType",
|
|
330
|
+
"copyFrom",
|
|
331
|
+
"country",
|
|
332
|
+
"courseId",
|
|
333
|
+
"customCss",
|
|
334
|
+
"customName",
|
|
335
|
+
"ddFilterSource",
|
|
336
|
+
"ddFilterStash",
|
|
337
|
+
"ddSortStash",
|
|
338
|
+
"defaultMode",
|
|
339
|
+
"defaultVariant",
|
|
340
|
+
"dir",
|
|
341
|
+
"displayType",
|
|
342
|
+
"end",
|
|
343
|
+
"field",
|
|
344
|
+
"fileFormat",
|
|
345
|
+
"fileLimit",
|
|
346
|
+
"filterArity",
|
|
347
|
+
"filterAxis",
|
|
348
|
+
"filterBehavior",
|
|
349
|
+
"filterColors",
|
|
350
|
+
"filterExcluded",
|
|
351
|
+
"filterMatch",
|
|
352
|
+
"filterRanges",
|
|
353
|
+
"filterSource",
|
|
354
|
+
"filterTarget",
|
|
355
|
+
"filterTargets",
|
|
356
|
+
"filterValueMode",
|
|
357
|
+
"filterable",
|
|
358
|
+
"formId",
|
|
359
|
+
"formRules",
|
|
360
|
+
"format",
|
|
361
|
+
"hideWhenEmpty",
|
|
362
|
+
"hoverHostDepth",
|
|
363
|
+
"href",
|
|
364
|
+
"htmlTag",
|
|
365
|
+
"icon",
|
|
366
|
+
"iconEnabled",
|
|
367
|
+
"iconName",
|
|
368
|
+
"inputType",
|
|
369
|
+
"keywordAction",
|
|
370
|
+
"kind",
|
|
371
|
+
"labelMode",
|
|
372
|
+
"learnHref",
|
|
373
|
+
"limitChars",
|
|
374
|
+
"lockedHref",
|
|
375
|
+
"loginHref",
|
|
376
|
+
"logoShape",
|
|
377
|
+
"lookupEnabled",
|
|
378
|
+
"mapTo",
|
|
379
|
+
"mapType",
|
|
380
|
+
"maxCents",
|
|
381
|
+
"maxChars",
|
|
382
|
+
"maxLabelText",
|
|
383
|
+
"maxValue",
|
|
384
|
+
"menuId",
|
|
385
|
+
"minCents",
|
|
386
|
+
"minChars",
|
|
387
|
+
"minLabelText",
|
|
388
|
+
"minValue",
|
|
389
|
+
"moneyPayload",
|
|
390
|
+
"moreButtonEnabled",
|
|
391
|
+
"multiline",
|
|
392
|
+
"name",
|
|
393
|
+
"openOnHover",
|
|
394
|
+
"orderSource",
|
|
395
|
+
"orientation",
|
|
396
|
+
"overlayId",
|
|
397
|
+
"part",
|
|
398
|
+
"parts",
|
|
399
|
+
"pattern",
|
|
400
|
+
"patternCountries",
|
|
401
|
+
"patternPreset",
|
|
402
|
+
"payload",
|
|
403
|
+
"picker",
|
|
404
|
+
"poster",
|
|
405
|
+
"prefillValue",
|
|
406
|
+
"presetCode",
|
|
407
|
+
"provinceField",
|
|
408
|
+
"ratingValue",
|
|
409
|
+
"required",
|
|
410
|
+
"reviewForm",
|
|
411
|
+
"sameDefault",
|
|
412
|
+
"searchBehavior",
|
|
413
|
+
"searchClearSwap",
|
|
414
|
+
"searchDebounceMs",
|
|
415
|
+
"searchDisplay",
|
|
416
|
+
"searchIconPosition",
|
|
417
|
+
"searchMinChars",
|
|
418
|
+
"searchOnEnter",
|
|
419
|
+
"searchPages",
|
|
420
|
+
"searchScope",
|
|
421
|
+
"searchShowClear",
|
|
422
|
+
"searchTargets",
|
|
423
|
+
"searchable",
|
|
424
|
+
"segmentId",
|
|
425
|
+
"selectMode",
|
|
426
|
+
"sentRedirect",
|
|
427
|
+
"sentText",
|
|
428
|
+
"setRange",
|
|
429
|
+
"showCount",
|
|
430
|
+
"showCurrent",
|
|
431
|
+
"showDetail",
|
|
432
|
+
"showFlag",
|
|
433
|
+
"showHeading",
|
|
434
|
+
"showIcon",
|
|
435
|
+
"showLabel",
|
|
436
|
+
"showLogos",
|
|
437
|
+
"showMarker",
|
|
438
|
+
"showOutline",
|
|
439
|
+
"showPhone",
|
|
440
|
+
"showPlaceholder",
|
|
441
|
+
"source",
|
|
442
|
+
"specificDate",
|
|
443
|
+
"src",
|
|
444
|
+
"start",
|
|
445
|
+
"step",
|
|
446
|
+
"stepMode",
|
|
447
|
+
"stylePreset",
|
|
448
|
+
"symbolChar",
|
|
449
|
+
"target",
|
|
450
|
+
"trackUrl",
|
|
451
|
+
"variant",
|
|
452
|
+
"videoId",
|
|
453
|
+
"videoSrc"
|
|
454
|
+
];
|
|
455
|
+
/** The columns a translation may rewrite on each entity, SEO fields included. */
|
|
456
|
+
export const TRANSLATABLE_ENTITY_FIELDS = {
|
|
457
|
+
"product": [
|
|
458
|
+
{
|
|
459
|
+
"key": "title"
|
|
460
|
+
},
|
|
461
|
+
{
|
|
462
|
+
"key": "description",
|
|
463
|
+
"html": true
|
|
464
|
+
},
|
|
465
|
+
{
|
|
466
|
+
"key": "vendor"
|
|
467
|
+
},
|
|
468
|
+
{
|
|
469
|
+
"key": "attributes",
|
|
470
|
+
"list": "attributes"
|
|
471
|
+
},
|
|
472
|
+
{
|
|
473
|
+
"key": "seoTitle"
|
|
474
|
+
},
|
|
475
|
+
{
|
|
476
|
+
"key": "seoDescription"
|
|
477
|
+
},
|
|
478
|
+
{
|
|
479
|
+
"key": "seoKeywords"
|
|
480
|
+
},
|
|
481
|
+
{
|
|
482
|
+
"key": "ogTitle"
|
|
483
|
+
},
|
|
484
|
+
{
|
|
485
|
+
"key": "ogDesc"
|
|
486
|
+
}
|
|
487
|
+
],
|
|
488
|
+
"category": [
|
|
489
|
+
{
|
|
490
|
+
"key": "title"
|
|
491
|
+
},
|
|
492
|
+
{
|
|
493
|
+
"key": "description",
|
|
494
|
+
"html": true
|
|
495
|
+
},
|
|
496
|
+
{
|
|
497
|
+
"key": "seoTitle"
|
|
498
|
+
},
|
|
499
|
+
{
|
|
500
|
+
"key": "seoDescription"
|
|
501
|
+
},
|
|
502
|
+
{
|
|
503
|
+
"key": "seoKeywords"
|
|
504
|
+
},
|
|
505
|
+
{
|
|
506
|
+
"key": "ogTitle"
|
|
507
|
+
},
|
|
508
|
+
{
|
|
509
|
+
"key": "ogDesc"
|
|
510
|
+
}
|
|
511
|
+
],
|
|
512
|
+
"article": [
|
|
513
|
+
{
|
|
514
|
+
"key": "title"
|
|
515
|
+
},
|
|
516
|
+
{
|
|
517
|
+
"key": "summary"
|
|
518
|
+
},
|
|
519
|
+
{
|
|
520
|
+
"key": "content",
|
|
521
|
+
"html": true,
|
|
522
|
+
"multiline": true
|
|
523
|
+
},
|
|
524
|
+
{
|
|
525
|
+
"key": "tags"
|
|
526
|
+
},
|
|
527
|
+
{
|
|
528
|
+
"key": "seoTitle"
|
|
529
|
+
},
|
|
530
|
+
{
|
|
531
|
+
"key": "seoDescription"
|
|
532
|
+
},
|
|
533
|
+
{
|
|
534
|
+
"key": "seoKeywords"
|
|
535
|
+
},
|
|
536
|
+
{
|
|
537
|
+
"key": "ogTitle"
|
|
538
|
+
},
|
|
539
|
+
{
|
|
540
|
+
"key": "ogDesc"
|
|
541
|
+
}
|
|
542
|
+
],
|
|
543
|
+
"blogCategory": [
|
|
544
|
+
{
|
|
545
|
+
"key": "title"
|
|
546
|
+
},
|
|
547
|
+
{
|
|
548
|
+
"key": "description"
|
|
549
|
+
},
|
|
550
|
+
{
|
|
551
|
+
"key": "seoTitle"
|
|
552
|
+
},
|
|
553
|
+
{
|
|
554
|
+
"key": "seoDescription"
|
|
555
|
+
},
|
|
556
|
+
{
|
|
557
|
+
"key": "seoKeywords"
|
|
558
|
+
},
|
|
559
|
+
{
|
|
560
|
+
"key": "ogTitle"
|
|
561
|
+
},
|
|
562
|
+
{
|
|
563
|
+
"key": "ogDesc"
|
|
564
|
+
}
|
|
565
|
+
],
|
|
566
|
+
"course": [
|
|
567
|
+
{
|
|
568
|
+
"key": "title"
|
|
569
|
+
},
|
|
570
|
+
{
|
|
571
|
+
"key": "summary"
|
|
572
|
+
},
|
|
573
|
+
{
|
|
574
|
+
"key": "description",
|
|
575
|
+
"html": true,
|
|
576
|
+
"multiline": true
|
|
577
|
+
},
|
|
578
|
+
{
|
|
579
|
+
"key": "level"
|
|
580
|
+
},
|
|
581
|
+
{
|
|
582
|
+
"key": "instructorTitle"
|
|
583
|
+
},
|
|
584
|
+
{
|
|
585
|
+
"key": "instructorBio",
|
|
586
|
+
"html": true,
|
|
587
|
+
"multiline": true
|
|
588
|
+
},
|
|
589
|
+
{
|
|
590
|
+
"key": "tags"
|
|
591
|
+
},
|
|
592
|
+
{
|
|
593
|
+
"key": "seoTitle"
|
|
594
|
+
},
|
|
595
|
+
{
|
|
596
|
+
"key": "seoDescription"
|
|
597
|
+
},
|
|
598
|
+
{
|
|
599
|
+
"key": "seoKeywords"
|
|
600
|
+
},
|
|
601
|
+
{
|
|
602
|
+
"key": "ogTitle"
|
|
603
|
+
},
|
|
604
|
+
{
|
|
605
|
+
"key": "ogDesc"
|
|
606
|
+
}
|
|
607
|
+
],
|
|
608
|
+
"review": [
|
|
609
|
+
{
|
|
610
|
+
"key": "reply",
|
|
611
|
+
"html": true
|
|
612
|
+
}
|
|
613
|
+
],
|
|
614
|
+
"courseSection": [
|
|
615
|
+
{
|
|
616
|
+
"key": "title"
|
|
617
|
+
}
|
|
618
|
+
],
|
|
619
|
+
"courseLesson": [
|
|
620
|
+
{
|
|
621
|
+
"key": "title"
|
|
622
|
+
}
|
|
623
|
+
],
|
|
624
|
+
"page": [
|
|
625
|
+
{
|
|
626
|
+
"key": "title"
|
|
627
|
+
},
|
|
628
|
+
{
|
|
629
|
+
"key": "description"
|
|
630
|
+
},
|
|
631
|
+
{
|
|
632
|
+
"key": "keywords"
|
|
633
|
+
},
|
|
634
|
+
{
|
|
635
|
+
"key": "ogTitle"
|
|
636
|
+
},
|
|
637
|
+
{
|
|
638
|
+
"key": "ogDesc"
|
|
639
|
+
}
|
|
640
|
+
],
|
|
641
|
+
"uiString": [
|
|
642
|
+
{
|
|
643
|
+
"key": "text"
|
|
644
|
+
}
|
|
645
|
+
],
|
|
646
|
+
"mailString": [
|
|
647
|
+
{
|
|
648
|
+
"key": "text"
|
|
649
|
+
}
|
|
650
|
+
],
|
|
651
|
+
"siteLabel": [
|
|
652
|
+
{
|
|
653
|
+
"key": "text"
|
|
654
|
+
}
|
|
655
|
+
]
|
|
656
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { FIELD_SKIN_BY_NODE } from '../../catalog/fieldskin.generated.js';
|
|
2
|
+
/**
|
|
3
|
+
* A FORM'S FIELDS ARE STYLED BY CONFIG KEYS, AND THE WRONG LEVEL IS SILENT.
|
|
4
|
+
*
|
|
5
|
+
* `fieldSkin.ts` / `fieldskin.go` hold the vocabulary, and each form node's
|
|
6
|
+
* `css.go` emits only the group it names. A knob written on a node that does not
|
|
7
|
+
* read it is stored, saved, published, and rendered NOWHERE — the same shape as
|
|
8
|
+
* a binding outside the `specials` namespace.
|
|
9
|
+
*
|
|
10
|
+
* CLAUDE.md carried the rule as PROSE ("form/css.go emits only FieldKnobs, so
|
|
11
|
+
* payCard* written on the form is stored and rendered nowhere"), which is a
|
|
12
|
+
* hand-kept summary of a 55-key table across 11 nodes. It is generated now: the
|
|
13
|
+
* groups from `fieldSkin.ts` (pure data), the node mapping from each `css.go`'s
|
|
14
|
+
* `fieldskin.<Group>` identifier, and the vocabulary cross-checked against the
|
|
15
|
+
* Go's own key literals at codegen.
|
|
16
|
+
*
|
|
17
|
+
* THE ANSWER IS PER NODE, not per key: `payCardBg` is real and rendered on
|
|
18
|
+
* `form-payment`, and dead on `form`. So a caller is told which node DOES read
|
|
19
|
+
* it, which is the only thing that turns the warning into a fix.
|
|
20
|
+
*/
|
|
21
|
+
/** Every field-skin key, across every form node. */
|
|
22
|
+
const ALL_KEYS = new Set(Object.values(FIELD_SKIN_BY_NODE).flat());
|
|
23
|
+
/** Does this node's renderer read this skin key? */
|
|
24
|
+
export function readsSkinKey(nodeType, key) {
|
|
25
|
+
return (FIELD_SKIN_BY_NODE[nodeType] ?? []).includes(key);
|
|
26
|
+
}
|
|
27
|
+
/** The node types whose renderer DOES read a key — the fix half of the warning. */
|
|
28
|
+
export function nodesReading(key) {
|
|
29
|
+
return Object.entries(FIELD_SKIN_BY_NODE)
|
|
30
|
+
.filter(([, keys]) => keys.includes(key))
|
|
31
|
+
.map(([type]) => type)
|
|
32
|
+
.sort();
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The warning for field-skin keys written on a node that renders none of them.
|
|
36
|
+
*
|
|
37
|
+
* Only fires for a key that IS a skin knob somewhere — an unknown config key is
|
|
38
|
+
* not this module's business, and claiming it would put a false positive on
|
|
39
|
+
* every ordinary write. A node that reads no skin keys at all (an ordinary
|
|
40
|
+
* heading) is equally not the target: the mistake this catches is a skin key on
|
|
41
|
+
* the WRONG form node, which is the one the platform documents.
|
|
42
|
+
*/
|
|
43
|
+
export function skinLevelNote(nodeType, keys) {
|
|
44
|
+
const misplaced = keys.filter((k) => ALL_KEYS.has(k) && !readsSkinKey(nodeType, k));
|
|
45
|
+
if (!misplaced.length)
|
|
46
|
+
return null;
|
|
47
|
+
const detail = misplaced
|
|
48
|
+
.map((k) => {
|
|
49
|
+
const readers = nodesReading(k);
|
|
50
|
+
return `${k} (read by ${readers.join(', ') || 'no node'})`;
|
|
51
|
+
})
|
|
52
|
+
.join('; ');
|
|
53
|
+
return (`"${nodeType}" renders none of these field-skin keys, so they would be stored, saved, ` +
|
|
54
|
+
`published and read by nothing: ${detail}. A skin knob is emitted only by the node whose ` +
|
|
55
|
+
'css.go names its group — the FORM dresses every field it holds with the input vocabulary, ' +
|
|
56
|
+
'and a payment card, choice group, timeslot or file field carries its own. Write them on ' +
|
|
57
|
+
'the field node, which lives in the FORM DOCUMENT ' +
|
|
58
|
+
'(PUT /api/sites/{siteId}/forms/{id}/document), not on the page.');
|
|
59
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { NEVER_TRANSLATED, TRANSLATABLE_ENTITY_FIELDS, TRANSLATABLE_SPECIALS, TRANSLATION_ENTITY_TYPES, } from '../../catalog/translations.generated.js';
|
|
2
|
+
/**
|
|
3
|
+
* WHICH CONTENT A TRANSLATION MAY REWRITE — AND WHICH BREAKS THE PAGE.
|
|
4
|
+
*
|
|
5
|
+
* A multi-language store was REACHABLE and UNSAFE. Every translations route is
|
|
6
|
+
* in the catalog — read, write, auto-fill, the review queue — so an agent could
|
|
7
|
+
* call them all and had no way to know which fields are content.
|
|
8
|
+
*
|
|
9
|
+
* The platform's registry says why that matters: the element registry declares
|
|
10
|
+
* 117 `(element, special)` pairs across 66 keys, INTERLEAVED in one object —
|
|
11
|
+
*
|
|
12
|
+
* text · label · alt · emptyText · searchPlaceholder ← content
|
|
13
|
+
* htmlTag · videoId · filterSource · contentType · src · name ← NOT
|
|
14
|
+
*
|
|
15
|
+
* — and translating one of the second group "does not degrade the page, it
|
|
16
|
+
* breaks the render": `name` is a lucide icon id, `src` is a URL,
|
|
17
|
+
* `filterSource` is a registry id the Go predicate switches on. An agent
|
|
18
|
+
* walking a page document and translating every string it finds hits all three,
|
|
19
|
+
* and the page it hands back renders wrong with nothing reporting why.
|
|
20
|
+
*
|
|
21
|
+
* A NEGATIVE ANSWER HERE IS INFORMATION, not an absence. `icon` has no
|
|
22
|
+
* translatable specials at all, and that is the correct, complete answer.
|
|
23
|
+
*
|
|
24
|
+
* NO NEW TOOL: this rides inside `sb_traits_for`'s result and the call sheet
|
|
25
|
+
* `sb_api_find` already prints for a translations operation.
|
|
26
|
+
*/
|
|
27
|
+
/** The specials a translation may rewrite on this element. Empty is an answer. */
|
|
28
|
+
export function translatableSpecials(elementType) {
|
|
29
|
+
return TRANSLATABLE_SPECIALS[elementType] ?? [];
|
|
30
|
+
}
|
|
31
|
+
/** Would translating this key break a render, whatever element it is on? */
|
|
32
|
+
export function isNeverTranslated(key) {
|
|
33
|
+
return NEVER_TRANSLATED.includes(key);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The specials this element HAS that must never be translated.
|
|
37
|
+
*
|
|
38
|
+
* Named per element rather than handed over as the whole 156-key list, because
|
|
39
|
+
* the answer a caller needs is about the node in front of it — and a list that
|
|
40
|
+
* long, on every element, is the kind of weight that makes a result unreadable.
|
|
41
|
+
*/
|
|
42
|
+
export function neverTranslatedOn(elementType, ownKeys) {
|
|
43
|
+
const ok = new Set(translatableSpecials(elementType));
|
|
44
|
+
return ownKeys.filter((k) => !ok.has(k) && isNeverTranslated(k));
|
|
45
|
+
}
|
|
46
|
+
/** The columns a translation may rewrite on an entity, or null for an unknown one. */
|
|
47
|
+
export function entityFields(entityType) {
|
|
48
|
+
return TRANSLATABLE_ENTITY_FIELDS[entityType] ?? null;
|
|
49
|
+
}
|
|
50
|
+
export function entityTypes() {
|
|
51
|
+
return TRANSLATION_ENTITY_TYPES;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* The vocabulary a translations operation needs, for its call sheet.
|
|
55
|
+
*
|
|
56
|
+
* The ENTITY FIELDS are the body of it; `node` is called out separately because
|
|
57
|
+
* it is the one entity type with no column list — a node translation is keyed
|
|
58
|
+
* by (node id, special), so its vocabulary is per element and lives in
|
|
59
|
+
* `sb_traits_for`. Without that sentence the empty answer for `node` reads as
|
|
60
|
+
* "nothing on a node is translatable", which is the opposite of true.
|
|
61
|
+
*/
|
|
62
|
+
export function translationCallSheet() {
|
|
63
|
+
return {
|
|
64
|
+
entity_types: TRANSLATION_ENTITY_TYPES,
|
|
65
|
+
entity_fields: TRANSLATABLE_ENTITY_FIELDS,
|
|
66
|
+
node_note: 'entityType "node" has no column list because a node translation is keyed by (node id, ' +
|
|
67
|
+
'specials key). Ask sb_traits_for <element> for `translatable` — it names the specials ' +
|
|
68
|
+
'that element allows. Translating any OTHER special BREAKS the render rather than ' +
|
|
69
|
+
'degrading it: `name` is a lucide icon id, `src` a URL, `filterSource` a registry id the ' +
|
|
70
|
+
'renderer switches on. Never walk a document translating every string you find.',
|
|
71
|
+
};
|
|
72
|
+
}
|
package/dist/tools/page.js
CHANGED
|
@@ -9,6 +9,7 @@ import { detachNote, presetIdOf, presetLayer } from '../domains/site/theme.js';
|
|
|
9
9
|
import { inertHintsFor } from '../domains/site/inert.js';
|
|
10
10
|
import { hasSeed, seedDocument, seedSummary, seededTypes } from '../domains/site/storepage.js';
|
|
11
11
|
import { unknownValueNote } from '../domains/site/vocabulary.js';
|
|
12
|
+
import { skinLevelNote } from '../domains/site/fieldskin.js';
|
|
12
13
|
import { siteTheme } from '../domains/site/theme-fetch.js';
|
|
13
14
|
import { request, redact } from '../transport/http.js';
|
|
14
15
|
import { siteToken } from './credentialpick.js';
|
|
@@ -441,6 +442,28 @@ export function registerPageTools(server, ctx) {
|
|
|
441
442
|
}
|
|
442
443
|
}
|
|
443
444
|
const valueNote = valueNotes.join(' ');
|
|
445
|
+
// A FIELD-SKIN KNOB ON THE WRONG NODE renders nowhere. The FORM dresses
|
|
446
|
+
// every field it holds with the input vocabulary; a payment card, choice
|
|
447
|
+
// group, timeslot or file field carries its own, and a knob written on a
|
|
448
|
+
// node whose css.go does not name that group is stored and read by
|
|
449
|
+
// nothing. Once per node TYPE, because the answer is about the type.
|
|
450
|
+
const skinNotes = [];
|
|
451
|
+
const skinSeen = new Set();
|
|
452
|
+
for (const e of batch) {
|
|
453
|
+
if (e.namespace !== 'config')
|
|
454
|
+
continue;
|
|
455
|
+
const type = d.doc.nodes[e.id]?.data.type ?? '';
|
|
456
|
+
if (!type || skinSeen.has(type))
|
|
457
|
+
continue;
|
|
458
|
+
const n = skinLevelNote(type, Object.keys(e.keys));
|
|
459
|
+
if (!n)
|
|
460
|
+
continue;
|
|
461
|
+
skinSeen.add(type);
|
|
462
|
+
const once = ctx.notices.once(`field-skin:${type}`, n);
|
|
463
|
+
if (once)
|
|
464
|
+
skinNotes.push(once);
|
|
465
|
+
}
|
|
466
|
+
const skinNote = skinNotes.join(' ');
|
|
444
467
|
// THE STICKY WARNING IS COMPUTED AGAINST THE DOCUMENT AS IT WILL BE, so
|
|
445
468
|
// the dry run and the real run say the same thing. A caller who is told
|
|
446
469
|
// only after committing has already shipped a header that does not move.
|
|
@@ -475,6 +498,7 @@ export function registerPageTools(server, ctx) {
|
|
|
475
498
|
...(baseNote ? { base_only: baseNote } : {}),
|
|
476
499
|
...(presetNote ? { preset: presetNote } : {}),
|
|
477
500
|
...(valueNote ? { value: valueNote } : {}),
|
|
501
|
+
...(skinNote ? { field_skin: skinNote } : {}),
|
|
478
502
|
...(Object.keys(hostNotes).length ? { hover_host: hostNotes } : {}),
|
|
479
503
|
...(note ? { note } : {}),
|
|
480
504
|
});
|
|
@@ -497,6 +521,7 @@ export function registerPageTools(server, ctx) {
|
|
|
497
521
|
...(baseNote ? { base_only: baseNote } : {}),
|
|
498
522
|
...(presetNote ? { preset: presetNote } : {}),
|
|
499
523
|
...(valueNote ? { value: valueNote } : {}),
|
|
524
|
+
...(skinNote ? { field_skin: skinNote } : {}),
|
|
500
525
|
...(hostNotes[batch[0].id] ? { hover_host: hostNotes[batch[0].id] } : {}),
|
|
501
526
|
});
|
|
502
527
|
}
|
|
@@ -508,6 +533,7 @@ export function registerPageTools(server, ctx) {
|
|
|
508
533
|
...(baseNote ? { base_only: baseNote } : {}),
|
|
509
534
|
...(presetNote ? { preset: presetNote } : {}),
|
|
510
535
|
...(valueNote ? { value: valueNote } : {}),
|
|
536
|
+
...(skinNote ? { field_skin: skinNote } : {}),
|
|
511
537
|
...(Object.keys(hostNotes).length ? { hover_host: hostNotes } : {}),
|
|
512
538
|
});
|
|
513
539
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sbuilder-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"description": "MCP server that designs and operates a Store Builder site — pages, data, theme and publish — through the platform's own API and live-edit protocol.",
|
|
5
5
|
"mcpName": "io.github.vuluu2k/sbuilder-mcp",
|
|
6
6
|
"type": "module",
|