fix(spec,objectql,rest,runtime): localize field-validation messages, name the field by its label (#3957) - #4014
Merged
Merged
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 5 package(s): 116 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
…name the field by its label (#3957) The write path built every built-in validation message by concatenating the API field name into a hardcoded English template, and those strings are what the Console toast, the CSV-import row report and every custom client display verbatim. A zh-CN user importing a bad row read `第 1 行:penalty_amount must be ≥ 0` for a field declared `label: '处罚金额'` with a full zh-CN bundle loaded — while the form layer localized the SAME constraint correctly via the browser's native `min`, so the language flipped with whichever layer caught it. - `@objectstack/spec/system` ships the message catalog (en / zh-CN / ja-JP / es-ES) plus `renderValidationMessage`; `@objectstack/spec/data` owns the per-field envelope (`FieldValidationErrorSchema`) that objectql used to hand-declare. Message keys are finer-grained than wire codes, so one `code` can carry several sentences without splitting the client-facing vocabulary. - The locale is `ExecutionContext.locale` — whose contract already read "Drives message catalogs" with no consumer. Both HTTP entries now resolve it from the request's `Accept-Language` / `?locale` first, falling back to the workspace `localization.locale`, so a message and the labels around it cannot come from different locales. - The field is named by its translated label → declared label → API name; `field` still carries the API name for input focus. - `params` exposes the constraint as data (`{ min: 0 }`, `{ maxLength: 512, actual: 3000 }`) so a client can format its own text. - Applied at every call site, not just the validator: single/batch insert, single-id/multi-row update, the rule evaluator's own built-in messages, and the importer's cell-coercion + required pre-check. An author-written `rule.message` is never overridden. Verified end-to-end in the running showcase: the import wizard's row report and the grid's inline-edit save now read 「预算必须大于或等于 0」/「保存失败: 年收入 必须大于或等于 0」 with translated labels, and `Accept-Language` switches the sentence. Pinned by a dogfood test through the real HTTP stack. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…r schema (#3957) AUTO-GEN under content/docs/references — `check:docs` gates it against the spec, and adding `data/validation-error.zod.ts` adds a page plus its two index entries. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…s in the API surface snapshot (#3957) `check:api-surface` ratchets @objectstack/spec's public exports. 17 added, 0 breaking — the per-field error contract plus the message catalog and the shared Accept-Language / field-label key helpers. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…DR-0114's catalog `main` landed ADR-0114 (#4035), which gave the field level its own closed code catalog — the same convergence this branch had reached independently. ADR-0114's version is the one that stays; #3957 is rebuilt on top of it rather than beside it (Prime Directive #12 — one contract, not two dialects): - Deleted `spec/src/data/validation-error.zod.ts` wholesale. Its `FieldValidationCode` duplicated `FieldErrorCode`, and its `FieldValidationErrorSchema` duplicated `FieldErrorSchema`. The generated snapshots (json-schema manifest, authorable + API surface) were reset to main's and regenerated, so nothing of the parallel contract survives. - `FieldErrorSchema` instead gains the one field it lacked: `label`. The constraint payload rides its EXISTING `constraint` position (typed from `unknown` to `Record<string, unknown>`) and the offending value rides `value` — so `params` is gone as a concept. The message templates interpolate from those same keys. - `objectql`'s `FieldValidationError` keeps main's `code: FieldErrorCode` and adds `label` / `constraint` / `value`. Two messages that arrived on main in the meantime were localized too, or the fix would have shipped with fresh holes in it: - ADR-0113's clear-out rejection (`X is required and cannot be cleared`) — new catalog key `required_cleared`, one wire code (`required`), four locales. - #3956's import dry-run bound pre-check (`firstConstraintViolation`) — it had reintroduced `penalty_amount must be ≥ 0` verbatim. It now renders from the shared catalog, which is also what keeps its "same verdict, same message as the real write" promise true after localization. Test expectations updated where the label now replaces the API name; the ADR-0113 `requiredWhen` pre-state check and the #3956 dry-run tests from main are kept as they were, only their messages re-pinned. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The changeset predated the ADR-0114 merge and still described a `params` bag and a `FieldValidationErrorSchema` that no longer exist. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…etired `params` Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…he api reference Only conflict was `content/docs/references/api/errors.mdx`, an AUTO-GEN file that both sides touched: main through #4054's authoring-key sweep, this branch through `FieldErrorSchema.label`. Took main's copy and re-ran `gen:schema && gen:docs` rather than hand-merging a generated artifact. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #3957
问题
写路径的内建校验消息是「硬编码英文模板 + 拼进 API 字段名」。这些字符串就是 Console toast、CSV 导入行报告、CLI 和任何自定义客户端逐字显示的内容,所以一个中文用户导入坏行看到的是:
而该字段声明的是
label: '处罚金额',并且zh-CN翻译已经加载。同一条约束在表单层由浏览器原生min正确本地化成「值必须大于或等于 0。」—— 语言取决于哪一层先拦住它。改法
issue 的两个 Expected 都做了(不是二选一),分四层:
1. 契约 —
@objectstack/specFieldValidationErrorSchema(data/validation-error.zod.ts):{ field, code, message, label, params }。objectql 原来手写的同名 interface 删掉改为复用 —— 一个 Zod 源(Prime Directive Add metamodel interfaces for ObjectQL/ObjectUI contract #1/Add comprehensive test suite for Zod schema validation #12)。system/validation-message.ts:en / zh-CN / ja-JP / es-ES,与平台 bundle 同一 locale 集合。英文措辞与改动前逐字节相同,英文部署的消息不变。check:i18n管的是「声明的 metadata label」,往那里加 key 会被判 drift。形状对齐i18n-resolver.ts里已有的SYSTEM_FIELD_LABELS。Accept-Language解析抽成一份共享实现(preferredLocaleFromHeader),两个传输层共用,不留第三份拷贝。2. 消息生产 —
@objectstack/objectqlvalidateOne的 14 处模板全部改走目录;字段名解析顺序 翻译 bundle(objects.<obj>.fields.<f>.label)→ 声明label→ API 名。API 名仍留在field里,表单照样能聚焦到正确输入框。params带上离散约束值:{ min: 0 }、{ maxLength: 512, actual: 3000 }、{ allowed: 'a, b' }。invalid_option_value/invalid_datetime/invalid_type_array),这样一个 code 能承载多个句子,而客户端匹配的code词汇表不用拆。rule-validator.ts的内建消息同样处理(requiredWhen、逐选项 gating、状态机兜底)。作者写的rule.message一律不覆盖 —— 那已经是作者选定的语言。3. locale 从哪来
ExecutionContext.locale的 TSDoc 一直写着 "Drives message catalogs and number/date formatting",却没有任何消费者 —— declared ≠ enforced(AGENTS.md PD #10)。现在它就是那个消费者。并且两个 HTTP 入口(
rest-server.ts的 execCtx、runtime 的resolve-execution-context.ts)都改成请求自身的Accept-Language/?locale优先,workspacelocalization.locale兜底。否则会出现同一屏里「中文字段 label + 英文报错消息」——正是这个 issue 抱怨的那种分裂。表达不出偏好的调用方(定时任务、服务间调用)继续用 workspace 默认值。4. 导入路径(issue 的原始场景)
import-coerce.ts的 10 处单元格强转消息 + 导入器的 required 预检,接同一个目录 —— 它们和引擎的约束消息落在同一份行报告里,只本地化一半会很怪。Account:未找到与"…"匹配的记录。import-prepare.ts现在把翻译后的 label 折进 metaMap(翻译文档本来就已经为选项同义词取过了)。no sys_user matches \"…\")—— 暴露内部标识符正是本 issue 要修的病,列名 + 出错值才是导入者能据以行动的信息。验证
测试
packages/spec/src/system/validation-message.test.ts— 目录完整性(每个 locale 定义每个 key、边界值必须出现在模板里)、locale 回退阶梯、部署覆盖、i18n 服务抛异常时不把 400 变 500。packages/objectql/src/engine-validation-locale.test.ts— call site 测试:单条 insert、批量 insert、单 id update、多行 update(#3106那条批量路径)全覆盖,外加setI18nService桥接。case标签不等于生效,得看调用点。record-validator.test.ts/rule-validator.test.ts/import-coerce.test.ts各自补了本地化断言。packages/qa/dogfood/test/validation-message-locale.dogfood.test.ts— 真实 HTTP 栈端到端:Accept-Language切语言、label/code/params三件套、PATCH、导入行报告。闸门:
pnpm build(71 包)、pnpm test(132 任务)、check:i18n、check:i18n-coverage、check:authorable-surface全绿。showcase 真机实测(独立端口实例,已停,临时产物已清):
第 1 行:预算必须大于或等于 0—— 声明 label 是 "Budget",走的是 zh-CN 翻译保存失败: 年收入必须大于或等于 0—— 声明 label 是 "Annual Revenue"对使用方的影响
code不变,仍然是该匹配的东西;params是新增可选字段。message文本会变:被本地化了,而且即使英文也改用 label(Budget must be ≥ 0,不再是budget must be ≥ 0)。断言旧英文字符串的地方应改成匹配code(现在还可以匹配params)。本 PR 内更新了一处这样的测试断言(import-integration.test.ts)。translation覆盖任意内建消息:定义validation.field.<messageKey>,例如validation.field.min_value: '{{label}}不得小于 {{min}} 元'。🤖 Generated with Claude Code