Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .changeset/32840-advisory-user-text.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
'@objectstack/lint': patch
---

Clause-②: no
Metadata-protocol: none

The form layout, visibility and page-sort advisories now carry a user-facing sentence in all 11 platform locales, for the save door's `userMessage` (the pattern the security-posture and dashboard advisories already follow). Four advisory rule ids gain one: the form-layout rule's `form-field-unknown` and `form-section-group-unknown` on a view write, `visibility-root-mislayered` on a view or page write, and `page-field-unprovisioned` where a page's sort names an injected column an external object does not store. Their findings carry the names the sentence interpolates as `userTextParams`, and `findingUserMessage` renders the sentence in the request's locale, else the workspace default, else English.

Rule ids, `message` and `hint` are unchanged, so `os validate`, `os lint` and every save response read exactly as before. The page, navigation and translation advisories of the same rules (`react-page-source-unparseable`, `react-prop-deprecated`, `react-prop-typo`, `react-chart-aggregate-invalid`, `react-chart-field-unprovisioned`, `nav-object-ungranted`, `nav-target-unresolved`, `page-field-unknown` and `page-section-group-unknown` at their advisory grade, `page-field-unprovisioned` on a page component's field binding, `translation-option-key-unknown`, `translation-section-name-missing`) have no sentence: the publish door never returns them, because those rules run there only on a flow write, whose snapshot carries no pages, apps or translations. They stay on the English `message`.
19 changes: 3 additions & 16 deletions packages/lint/src/finding-user-text.data-model.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ import {
OBJECT_REFERENCE_UNREGISTERED_PLATFORM,
validateObjectReferences,
} from './validate-object-references.js';
import { PAGE_FIELD_UNPROVISIONED } from './validate-page-field-bindings.js';
import { TITLE_UNRESOLVABLE } from './validate-record-title.js';
import {
RELATIONSHIP_MASTER_DETAIL_REQUIRED,
Expand Down Expand Up @@ -243,21 +242,9 @@ describe('data-model advisories user text · rendered from the rules\' real find
const unknown = lintAutonumberFormats(autonumberStack('{gone_field}-{0000}')).filter((f) => f.rule === AUTONUMBER_UNKNOWN_FIELD);
expect(unknown.map((f) => f.severity)).toEqual(['error']);
expect(unknown[0]!.userTextParams).toBeUndefined();
// A page sort's unprovisioned warning is `validate-page-field-bindings.ts`'s
// id: this file's page-sort site carries no params of its own.
const pageSort = validateSortableFields({
objects: [EXT],
pages: [{
name: 'acme_customer_page', type: 'app',
regions: [{ name: 'main', components: [{
type: 'object-grid',
properties: { objectName: 'acme_ext_customer', sort: [{ field: 'created_at', order: 'desc' }] },
}] }],
}],
}).filter((f) => f.rule === PAGE_FIELD_UNPROVISIONED);
expect(pageSort.map((f) => f.severity)).toEqual(['warning']);
expect(pageSort[0]!.userTextParams).toBeUndefined();
expect(FINDING_USER_TEXT_CATALOG[PAGE_FIELD_UNPROVISIONED]).toBeUndefined();
// The page-sort `page-field-unprovisioned` warning this rule pushes does
// carry params, for its owner's catalog (`validate-page-field-bindings.user-text.ts`);
// `finding-user-text.pages-forms.test.ts` pins that site.
// The object-reference sites a per-write snapshot never carries as the
// written item (here an object field's lookup target) carry none.
const fieldSite = validateObjectReferences({
Expand Down
329 changes: 329 additions & 0 deletions packages/lint/src/finding-user-text.pages-forms.test.ts

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions packages/lint/src/finding-user-text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ import { AUTONUMBER_FORMATS_USER_TEXT } from './lint-autonumber-formats.user-tex
import { SORTABLE_FIELDS_USER_TEXT } from './validate-sortable-fields.user-text.js';
import { SEARCHABLE_FIELDS_USER_TEXT } from './validate-searchable-fields.user-text.js';
import { OBJECT_REFERENCES_USER_TEXT } from './validate-object-references.user-text.js';
import { FORM_LAYOUT_USER_TEXT } from './validate-form-layout.user-text.js';
import { VISIBILITY_PREDICATES_USER_TEXT } from './validate-visibility-predicates.user-text.js';
import { PAGE_FIELD_BINDINGS_USER_TEXT } from './validate-page-field-bindings.user-text.js';

/**
* The names a finding's user sentence interpolates — object, field and
Expand Down Expand Up @@ -181,6 +184,9 @@ export const FINDING_USER_TEXT_CATALOG: UserTextCatalog = mergeUserTextCatalogs(
SORTABLE_FIELDS_USER_TEXT,
SEARCHABLE_FIELDS_USER_TEXT,
OBJECT_REFERENCES_USER_TEXT,
FORM_LAYOUT_USER_TEXT,
VISIBILITY_PREDICATES_USER_TEXT,
PAGE_FIELD_BINDINGS_USER_TEXT,
);

/**
Expand Down
28 changes: 21 additions & 7 deletions packages/lint/src/validate-form-layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
*/

import { collectionEntries } from './collection-entries.js';
import type { UserTextParams } from './finding-user-text.js';
// [#13855] The field-group reference question, shared with
// `validate-page-field-bindings` so both layout escape hatches resolve a
// `section.group` against one index and report the same way.
Expand Down Expand Up @@ -92,6 +93,12 @@ export interface FormLayoutFinding {
message: string;
/** How to fix it. */
hint: string;
/**
* The names this finding's user-facing sentence interpolates, set at both
* advisory push sites; the sentences are `validate-form-layout.user-text.ts`
* (`AuthoringFinding.userTextParams`).
*/
userTextParams?: UserTextParams;
}

type AnyRec = Record<string, unknown>;
Expand Down Expand Up @@ -220,14 +227,20 @@ export function validateFormLayout(stack: AnyRec): FormLayoutFinding[] {
// ── (c) [#13855] a section's `group` names a declared field group ──
// Both buckets, for the reason (a) reads both: a rule that judges only
// the canonical spelling is silent on the legacy one.
//
// The user sentence's names are added HERE, not in the shared helper:
// its other host (`validate-page-field-bindings`) emits its group id
// on no write the publish door judges, so it authors no sentence. The
// helper returns a finding only when `objName` resolves, and each
// finding's `path` is its reference's, which names the group.
const groupRefs = sectionGroupRefs(sections, `${site.path}.${bucket}`);
const groupAt = new Map(groupRefs.map((ref) => [ref.path, ref.key]));
findings.push(
...checkSectionGroupRefs(
sectionGroupRefs(sections, `${site.path}.${bucket}`),
objName,
objectFieldGroups,
where,
FORM_SECTION_GROUP_UNKNOWN,
),
...checkSectionGroupRefs(groupRefs, objName, objectFieldGroups, where, FORM_SECTION_GROUP_UNKNOWN)
.map((finding) => ({
...finding,
userTextParams: { view: viewName, object: objName ?? '', group: groupAt.get(finding.path) ?? '' },
})),
);

for (let s = 0; s < sections.length; s++) {
Expand All @@ -251,6 +264,7 @@ export function validateFormLayout(stack: AnyRec): FormLayoutFinding[] {
hint:
`Fix the field name, or add "${fname}" to ${objName}. Section field ` +
`references must match the object's field names exactly.`,
userTextParams: { view: viewName, object: objName ?? '', field: fname },
});
}
}
Expand Down
130 changes: 130 additions & 0 deletions packages/lint/src/validate-form-layout.user-text.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.

/**
* The user-facing sentences of `validate-form-layout.ts`'s advisory findings,
* in every platform locale (`PLATFORM_LOCALE_TAGS`, `@objectstack/spec/system`).
* The finding mechanism and the locale ladder are `finding-user-text.ts`'s;
* this file is the rule's own half of it, kept beside the rule so its wording
* and its translations change together.
*
* Who reads these: the person who saved a form view in Studio, in their own
* language, once the save door composes `RuntimeAuthoringIssueSchema.userMessage`
* from them. So each sentence says what the form fails to show and the one
* change that fixes it, in the form designer's own words (view, form, section,
* field, field group, object); the developer-facing detail stays in the
* finding's `message`, `hint` and `os explain` entry.
*
* Both of the rule's ids are advisory and both reach the publish door, on a
* `view` write. Each entry reads the `userTextParams` its finding carries:
*
* | rule id | params | set at |
* |---|---|---|
* | `form-field-unknown` | `view`, `object`, `field` | the section-field push site |
* | `form-section-group-unknown` | `view`, `object`, `group` | the `checkSectionGroupRefs` call site |
*
* The group finding is built by the shared `object-field-groups.ts` helper,
* so its params are added where this rule calls it, not in the helper: the
* helper's other host (`validate-page-field-bindings`) emits its group id on
* no write the door judges.
*
* Names in a sentence are the API names the rule saw (`contract_form`, a
* field's name), not labels, for the reason `validate-security-posture.user-text.ts`
* gives: the rule is a pure function of the stack with no label lookup in the
* request's language. A view with no name shows the rule's own stand-in, as
* its `message` does.
*
* `finding-user-text.pages-forms.test.ts` renders every entry from the rule's
* real findings in every platform locale.
*/

import type { UserTextCatalog } from './finding-user-text.js';
import { FORM_FIELD_UNKNOWN, FORM_SECTION_GROUP_UNKNOWN } from './validate-form-layout.js';

export const FORM_LAYOUT_USER_TEXT: UserTextCatalog = Object.freeze({
[FORM_FIELD_UNKNOWN]: {
en: (p) =>
`The form in view "${p.view}" lists the field "${p.field}", which object "${p.object}" does not have, so ` +
`it is skipped and never appears on the form. Correct the field name, or add the field to "${p.object}".`,
'zh-Hans': (p) =>
`视图“${p.view}”的表单列出了字段“${p.field}”,但对象“${p.object}”没有这个字段,因此它会被跳过,` +
`不会出现在表单上。请更正字段名称,或在“${p.object}”中添加该字段。`,
'zh-Hant': (p) =>
`檢視「${p.view}」的表單列出了欄位「${p.field}」,但物件「${p.object}」沒有這個欄位,因此它會被略過,` +
`不會出現在表單上。請更正欄位名稱,或在「${p.object}」中新增該欄位。`,
es: (p) =>
`El formulario de la vista "${p.view}" incluye el campo "${p.field}", que el objeto "${p.object}" no ` +
`tiene, así que se omite y nunca aparece en el formulario. Corrija el nombre del campo o agregue el ` +
`campo a "${p.object}".`,
ar: (p) =>
`يتضمن النموذج في طريقة العرض "${p.view}" الحقل "${p.field}"، وهو غير موجود في الكائن "${p.object}"، ` +
`لذلك يتم تخطيه ولا يظهر في النموذج أبدًا. صحّح اسم الحقل، أو أضف الحقل إلى "${p.object}".`,
fr: (p) =>
`Le formulaire de la vue « ${p.view} » liste le champ « ${p.field} », que l'objet « ${p.object} » ne ` +
`possède pas : il est donc ignoré et n'apparaît jamais dans le formulaire. Corrigez le nom du champ, ou ` +
`ajoutez ce champ à « ${p.object} ».`,
'pt-BR': (p) =>
`O formulário da visualização "${p.view}" lista o campo "${p.field}", que o objeto "${p.object}" não ` +
`tem, então ele é ignorado e nunca aparece no formulário. Corrija o nome do campo ou adicione o campo a ` +
`"${p.object}".`,
ru: (p) =>
`Форма в представлении «${p.view}» содержит поле «${p.field}», которого нет у объекта «${p.object}», ` +
`поэтому оно пропускается и никогда не отображается в форме. Исправьте имя поля или добавьте это поле ` +
`в «${p.object}».`,
de: (p) =>
`Das Formular in der Ansicht „${p.view}“ führt das Feld „${p.field}“ auf, das das Objekt „${p.object}“ ` +
`nicht hat. Es wird daher übersprungen und erscheint nie im Formular. Korrigieren Sie den Feldnamen, oder ` +
`fügen Sie das Feld zu „${p.object}“ hinzu.`,
ja: (p) =>
`ビュー「${p.view}」のフォームには項目「${p.field}」が含まれていますが、オブジェクト「${p.object}」には` +
`この項目がないため、スキップされてフォームに表示されません。項目名を修正するか、「${p.object}」に` +
`この項目を追加してください。`,
ko: (p) =>
`보기 "${p.view}"의 양식에 필드 "${p.field}"이(가) 있지만 개체 "${p.object}"에는 이 필드가 없으므로, ` +
`건너뛰어지고 양식에 표시되지 않습니다. 필드 이름을 수정하거나 "${p.object}"에 이 필드를 추가하세요.`,
},
[FORM_SECTION_GROUP_UNKNOWN]: {
en: (p) =>
`A section of the form in view "${p.view}" takes its fields from the field group "${p.group}", which ` +
`object "${p.object}" does not declare, so the section has no fields and does not appear. Correct the ` +
`group name, or add the group to "${p.object}" and assign fields to it.`,
'zh-Hans': (p) =>
`视图“${p.view}”的表单中有一个分区从字段分组“${p.group}”获取字段,但对象“${p.object}”没有声明这个分组,` +
`因此该分区没有字段,也不会显示。请更正分组名称,或在“${p.object}”中添加该分组并为其分配字段。`,
'zh-Hant': (p) =>
`檢視「${p.view}」的表單中有一個區段從欄位分組「${p.group}」取得欄位,但物件「${p.object}」沒有宣告這個分組,` +
`因此該區段沒有欄位,也不會顯示。請更正分組名稱,或在「${p.object}」中新增該分組並為其指派欄位。`,
es: (p) =>
`Una sección del formulario de la vista "${p.view}" toma sus campos del grupo de campos "${p.group}", que ` +
`el objeto "${p.object}" no declara, así que la sección no tiene campos y no aparece. Corrija el nombre ` +
`del grupo, o agregue el grupo a "${p.object}" y asígnele campos.`,
ar: (p) =>
`يأخذ قسم في النموذج ضمن طريقة العرض "${p.view}" حقوله من مجموعة الحقول "${p.group}"، التي لا يعرّفها ` +
`الكائن "${p.object}"، لذلك لا يحتوي القسم على أي حقول ولا يظهر. صحّح اسم المجموعة، أو أضف المجموعة ` +
`إلى "${p.object}" وعيّن لها حقولًا.`,
fr: (p) =>
`Une section du formulaire de la vue « ${p.view} » tire ses champs du groupe de champs « ${p.group} », ` +
`que l'objet « ${p.object} » ne déclare pas : la section n'a donc aucun champ et ne s'affiche pas. ` +
`Corrigez le nom du groupe, ou ajoutez ce groupe à « ${p.object} » et attribuez-lui des champs.`,
'pt-BR': (p) =>
`Uma seção do formulário da visualização "${p.view}" obtém seus campos do grupo de campos "${p.group}", ` +
`que o objeto "${p.object}" não declara, então a seção não tem campos e não aparece. Corrija o nome do ` +
`grupo ou adicione o grupo a "${p.object}" e atribua campos a ele.`,
ru: (p) =>
`Раздел формы в представлении «${p.view}» берёт поля из группы полей «${p.group}», которую объект ` +
`«${p.object}» не объявляет, поэтому в разделе нет полей и он не отображается. Исправьте имя группы или ` +
`добавьте эту группу в «${p.object}» и назначьте ей поля.`,
de: (p) =>
`Ein Abschnitt des Formulars in der Ansicht „${p.view}“ bezieht seine Felder aus der Feldgruppe ` +
`„${p.group}“, die das Objekt „${p.object}“ nicht deklariert. Der Abschnitt hat daher keine Felder und ` +
`wird nicht angezeigt. Korrigieren Sie den Gruppennamen, oder fügen Sie die Gruppe zu „${p.object}“ ` +
`hinzu und ordnen Sie ihr Felder zu.`,
ja: (p) =>
`ビュー「${p.view}」のフォームのセクションが項目グループ「${p.group}」から項目を取得していますが、` +
`オブジェクト「${p.object}」はこのグループを宣言していないため、セクションに項目がなく表示されません。` +
`グループ名を修正するか、「${p.object}」にこのグループを追加して項目を割り当ててください。`,
ko: (p) =>
`보기 "${p.view}"의 양식에 있는 섹션이 필드 그룹 "${p.group}"에서 필드를 가져오지만, 개체 ` +
`"${p.object}"은(는) 이 그룹을 선언하지 않으므로 섹션에 필드가 없고 표시되지 않습니다. 그룹 이름을 ` +
`수정하거나 "${p.object}"에 이 그룹을 추가하고 필드를 할당하세요.`,
},
});
Loading
Loading