Skip to content
Open
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
1 change: 1 addition & 0 deletions docs/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ The quick lookup surface: one line and a minimal snippet per construct. For rule
| [`dependsOn`](/spec/relations) | link a dropdown to another, copy a value from the referenced record, or default a line from the open document header |
| [`function`](/spec/entities#function-the-presentation-role) | an explicit presentation role (Document, Setting, ...) |
| [`label`](/spec/entities#label-a-stored-display-name) | a stored, read-only display name for lookups |
| [`label` / `countryLabels` (field)](/spec/entities#label-countrylabels-what-a-field-is-called) | what a field is called, and the variants the tenant's country resolves |
| [`number`](/spec/entities#document-numbering) | a platform-numbered, gap-free document field |
| [`checks`](/spec/entities#checks-declarative-validations) | cross-field / cross-line validations |
| [`checks: kind: guard`](/spec/entities#kind-guard-a-precondition-over-an-aggregate) | a precondition over an aggregate: block, mark for a task, or reject |
Expand Down
48 changes: 48 additions & 0 deletions docs/spec/entities.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ fields:
| `pattern` | an input-format regular expression the value must match (string / text fields only) |
| `defaultValue` | the field's default: the column default, the reason a `required` field is not demanded from the caller, and the value a **new** row is seeded with in the UI (see [Field defaults](#defaultvalue-field-defaults)) |
| `unique` | a UNIQUE constraint (e.g. a code or business key) |
| `label` | what the field is called in the UI, replacing the humanized name (see [what a field is called](#label-countrylabels-what-a-field-is-called)) |
| `countryLabels` | label variants resolved from the tenant's country rather than the reader's language (see [what a field is called](#label-countrylabels-what-a-field-is-called)) |
| `visibleTo` | an allow-list of roles that may read the field, enforced where the data leaves the server (see [Role-scoped field visibility](#role-scoped-field-visibility-visibleto)) |
| `precision` / `scale` | override the decimal default (16, 2) |
| `readOnly` | rendered read-only in the UI (e.g. a calculated total) |
Expand Down Expand Up @@ -87,6 +89,52 @@ By default the generated UI controls follow declaration order - all fields first

Names match field / relation names (case-insensitive). A partial order is fine - any property not listed keeps its default position and is appended after the listed ones.

## label / countryLabels — what a field is called

A field's caption is the humanized form of its name, which is right most of the time and cannot
produce an acronym, a unit or a term of art: `nationalId` reads as "National Id". State it instead:

```yaml
fields:
- { name: nationalId, type: string, label: National ID }
- { name: iban, type: string, label: IBAN }
```

The label is what every surface renders - the form caption, the list column header, the read-only
details block - and it seeds the field's entry in the default-language catalog, so it is translated
like any other label.

Some terms are fixed by the **company's country**, not by the language its users read the interface
in. A national identification number is called ЕГН in Bulgaria and Steuer-ID in Germany; an
English-reading accountant at a Bulgarian company needs the Bulgarian term, and a Bulgarian-reading
user at a German company does not. Declare such a label per country and let the tenant's country
resolve it:

```yaml
- name: nationalId
type: string
label: National ID
countryLabels:
BG: ЕГН
DE: Steuer-ID
```

Keys are ISO 3166-1 alpha-2 country codes. Which country a deployment serves is a property of the
tenant, not of the model - the intent declares which term applies where, and nothing else.

::: info Normative
A field's `label:` MUST replace the humanized field name wherever the field is rendered, and MUST
seed its entry in the default-language catalog so it is translated like any other label.
`countryLabels:` declares label variants keyed by ISO 3166-1 alpha-2 country code, resolved from the
tenant's country and applied in **every** language: a label resolved by country is not a
translation, and MUST NOT be treated as one.
A country that declares no variant MUST fall back to `label:`, and a field that declares no label to
the humanized name, so a model declaring neither behaves as before.
A `countryLabels` key that is not a country MUST be rejected - it can never match a tenant, and
would leave the base label rendering with nothing reported. A blank `label:`, and a variant with no
label, MUST be rejected for the same reason.
:::

## unique — a business key over more than one field

`unique: true` on a field constrains one column. When what makes a row unique spans several,
Expand Down