|
| 1 | +--- |
| 2 | +"@objectstack/spec": minor |
| 3 | +"@objectstack/objectql": minor |
| 4 | +"@objectstack/rest": minor |
| 5 | +"@objectstack/runtime": patch |
| 6 | +--- |
| 7 | + |
| 8 | +fix(spec,objectql,rest,runtime): field-validation messages answer in the caller's language, named by the field's label (#3957) |
| 9 | + |
| 10 | +The write path built every built-in validation message by concatenating the **API |
| 11 | +field name** into a **hardcoded English** template. Those strings are what the |
| 12 | +Console toast, the CSV-import row report, the CLI and any custom client display |
| 13 | +verbatim, so a Chinese-locale user importing a bad row read: |
| 14 | + |
| 15 | +``` |
| 16 | +第 1 行:penalty_amount must be ≥ 0 |
| 17 | +``` |
| 18 | + |
| 19 | +…for a field declared `label: '处罚金额'` with a full `zh-CN` bundle loaded. The |
| 20 | +form layer localized the *same* constraint correctly (the browser's native |
| 21 | +`min`), so the language flipped depending on which layer caught the value. |
| 22 | + |
| 23 | +**Three things changed.** |
| 24 | + |
| 25 | +1. **The message is rendered in the caller's locale** from a built-in catalog |
| 26 | + (`BUILTIN_VALIDATION_MESSAGES`, `@objectstack/spec/system`) shipping `en`, |
| 27 | + `zh-CN`, `ja-JP`, `es-ES` — the same four locales as the platform bundles. |
| 28 | + The locale comes from `ExecutionContext.locale`, whose contract already read |
| 29 | + "Drives message catalogs"; this is the consumer that makes that true. Both |
| 30 | + HTTP entries (REST server, runtime dispatcher) now resolve it from the |
| 31 | + request's `Accept-Language` / `?locale` first, falling back to the workspace |
| 32 | + `localization.locale` — so a rejection message and the field labels around it |
| 33 | + can no longer disagree. |
| 34 | + |
| 35 | +2. **The field is named by its label, never the API name**: translation bundle |
| 36 | + (`objects.<obj>.fields.<f>.label`) → declared `label` → API name as the last |
| 37 | + resort. `FieldValidationError.field` still carries the API name so a form can |
| 38 | + focus the right input. |
| 39 | + |
| 40 | +3. **The constraint is exposed as data**, so a client can format its own text |
| 41 | + instead of parsing the sentence: |
| 42 | + `{ field, code, message, label, constraint: { min: 0 } }`. This rides |
| 43 | + ADR-0114's existing `constraint` / `value` positions on `FieldErrorSchema` |
| 44 | + (`constraint` tightens from `unknown` to `Record<string, unknown>`) rather |
| 45 | + than adding a parallel payload — `label` is the only new field. The bag |
| 46 | + carries `min`/`max`/`minLength`/`maxLength`/`actual`/`allowed`/`type`, and the |
| 47 | + message templates interpolate from exactly those keys. |
| 48 | + |
| 49 | +Covered end-to-end, not only in the validator: single and batch insert, |
| 50 | +single-id and multi-row update, ADR-0113's clear-out rejection, the object-level |
| 51 | +rule evaluator's own built-in messages (`requiredWhen`, per-option gating, |
| 52 | +state-machine fallbacks), and the importer's cell-coercion, required pre-check |
| 53 | +and #3956 bound pre-check messages — all of which land in the same row report. |
| 54 | + |
| 55 | +**What this changes for consumers.** |
| 56 | + |
| 57 | +- `code` is unchanged (ADR-0114's `FieldErrorCode`) and remains the thing to |
| 58 | + match on. Message keys are finer-grained than codes — `invalid_datetime`, |
| 59 | + `invalid_option_value`, `required_cleared` are rendering detail and never reach |
| 60 | + the wire — so localization never splits the client-facing vocabulary. |
| 61 | +- `message` **text changes**: it is localized, and it names the field by label |
| 62 | + even in English (`Budget must be ≥ 0`, not `budget must be ≥ 0`). Anything |
| 63 | + asserting on the old English string should match `code` (and now |
| 64 | + `constraint`) instead. |
| 65 | +- An author-written validation-rule `message` is never touched — it is already |
| 66 | + in the language its author chose. |
| 67 | +- A deployment can override any built-in message with a `translation` item |
| 68 | + defining `validation.field.<messageKey>` (e.g. |
| 69 | + `validation.field.min_value: '{{label}}不得小于 {{min}} 元'`). |
| 70 | +- The importer's reference-failure message no longer names the target object's |
| 71 | + API name (`no sys_user matches "…"`): naming internal identifiers is the |
| 72 | + defect being fixed, and the column plus the offending value are what an |
| 73 | + importer can act on. |
0 commit comments