Skip to content

fix(spec,objectql,rest,runtime): localize field-validation messages, name the field by its label (#3957) - #4014

Merged
baozhoutao merged 7 commits into
mainfrom
claude/issue-3957-review-8c0daf
Jul 30, 2026
Merged

fix(spec,objectql,rest,runtime): localize field-validation messages, name the field by its label (#3957)#4014
baozhoutao merged 7 commits into
mainfrom
claude/issue-3957-review-8c0daf

Conversation

@baozhoutao

Copy link
Copy Markdown
Contributor

Closes #3957

问题

写路径的内建校验消息是「硬编码英文模板 + 拼进 API 字段名」。这些字符串就是 Console toast、CSV 导入行报告、CLI 和任何自定义客户端逐字显示的内容,所以一个中文用户导入坏行看到的是:

第 1 行:penalty_amount must be ≥ 0

而该字段声明的是 label: '处罚金额',并且 zh-CN 翻译已经加载。同一条约束在表单层由浏览器原生 min 正确本地化成「值必须大于或等于 0。」—— 语言取决于哪一层先拦住它

改法

issue 的两个 Expected 都做了(不是二选一),分四层:

1. 契约 — @objectstack/spec

  • FieldValidationErrorSchema(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 集合。英文措辞与改动前逐字节相同,英文部署的消息不变。
  • 目录走常量而不是 auto-gen bundle:check:i18n 管的是「声明的 metadata label」,往那里加 key 会被判 drift。形状对齐 i18n-resolver.ts 里已有的 SYSTEM_FIELD_LABELS
  • Accept-Language 解析抽成一份共享实现(preferredLocaleFromHeader),两个传输层共用,不留第三份拷贝。

2. 消息生产 — @objectstack/objectql

  • validateOne 的 14 处模板全部改走目录;字段名解析顺序 翻译 bundle(objects.<obj>.fields.<f>.label)→ 声明 label → API 名。API 名仍留在 field 里,表单照样能聚焦到正确输入框。
  • params 带上离散约束值:{ min: 0 }{ maxLength: 512, actual: 3000 }{ allowed: 'a, b' }
  • 消息 key 比 wire code 更细(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 优先,workspace localization.locale 兜底。否则会出现同一屏里「中文字段 label + 英文报错消息」——正是这个 issue 抱怨的那种分裂。表达不出偏好的调用方(定时任务、服务间调用)继续用 workspace 默认值。

4. 导入路径(issue 的原始场景)

  • import-coerce.ts 的 10 处单元格强转消息 + 导入器的 required 预检,接同一个目录 —— 它们和引擎的约束消息落在同一份行报告里,只本地化一半会很怪。
  • 顺带修了实测发现的一个漏:行报告的列名读的是未翻译的 schema,所以出现过 Account:未找到与"…"匹配的记录import-prepare.ts 现在把翻译后的 label 折进 metaMap(翻译文档本来就已经为选项同义词取过了)。
  • 引用失败消息不再泄露目标对象的 API 名(原 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.tscall 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:i18ncheck:i18n-coveragecheck:authorable-surface 全绿。

showcase 真机实测(独立端口实例,已停,临时产物已清):

  • 导入向导行报告:第 1 行:预算必须大于或等于 0 —— 声明 label 是 "Budget",走的是 zh-CN 翻译
  • 网格行内编辑保存:保存失败: 年收入必须大于或等于 0 —— 声明 label 是 "Annual Revenue"
  • API 逐项核过 min / max / maxLength / required / email / select × zh-CN / ja-JP / 无 header

对使用方的影响

  • 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

@vercel

vercel Bot commented Jul 30, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Jul 30, 2026 8:54am

Request Review

@github-actions

github-actions Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 5 package(s): @objectstack/objectql, packages/qa, @objectstack/rest, @objectstack/runtime, @objectstack/spec.

116 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/agents.mdx (via @objectstack/spec)
  • content/docs/ai/connect-mcp.mdx (via @objectstack/rest)
  • content/docs/ai/skills-reference.mdx (via @objectstack/spec)
  • content/docs/ai/skills.mdx (via @objectstack/spec)
  • content/docs/api/client-sdk.mdx (via packages/runtime, @objectstack/spec)
  • content/docs/api/environment-routing.mdx (via @objectstack/spec)
  • content/docs/api/error-catalog.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-client.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx (via @objectstack/rest, @objectstack/spec)
  • content/docs/api/index.mdx (via @objectstack/rest, @objectstack/runtime, @objectstack/spec)
  • content/docs/api/wire-format.mdx (via @objectstack/runtime)
  • content/docs/automation/approvals.mdx (via packages/spec)
  • content/docs/automation/flows.mdx (via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx (via @objectstack/runtime, packages/spec)
  • content/docs/automation/hooks.mdx (via @objectstack/spec)
  • content/docs/automation/index.mdx (via @objectstack/spec)
  • content/docs/automation/webhooks.mdx (via @objectstack/spec)
  • content/docs/automation/workflows.mdx (via @objectstack/spec)
  • content/docs/concepts/architecture.mdx (via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx (via packages/spec)
  • content/docs/concepts/index.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx (via @objectstack/objectql, packages/spec)
  • content/docs/concepts/north-star.mdx (via packages/runtime, packages/spec)
  • content/docs/data-modeling/analytics.mdx (via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/runtime, @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx (via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx (via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx (via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx (via packages/objectql, @objectstack/spec)
  • content/docs/data-modeling/index.mdx (via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx (via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx (via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx (via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx (via @objectstack/spec)
  • content/docs/deployment/cli.mdx (via @objectstack/spec)
  • content/docs/deployment/index.mdx (via @objectstack/runtime)
  • content/docs/deployment/migration-from-objectql.mdx (via @objectstack/objectql)
  • content/docs/deployment/production-readiness.mdx (via @objectstack/runtime)
  • content/docs/deployment/single-project-mode.mdx (via @objectstack/runtime)
  • content/docs/deployment/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx (via @objectstack/spec)
  • content/docs/deployment/vercel.mdx (via @objectstack/objectql, @objectstack/runtime)
  • content/docs/getting-started/build-with-claude-code.mdx (via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx (via @objectstack/spec)
  • content/docs/getting-started/examples.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx (via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/runtime, @objectstack/spec)
  • content/docs/kernel/cluster.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx (via packages/spec)
  • content/docs/kernel/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/email-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx (via packages/spec)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/objectql, @objectstack/spec)
  • content/docs/kernel/services.mdx (via @objectstack/objectql)
  • content/docs/permissions/authentication.mdx (via @objectstack/objectql, @objectstack/runtime)
  • content/docs/permissions/authorization.mdx (via packages/qa, packages/runtime, @objectstack/spec)
  • content/docs/permissions/delegated-administration.mdx (via packages/qa)
  • content/docs/permissions/permission-sets.mdx (via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx (via @objectstack/spec)
  • content/docs/permissions/positions.mdx (via @objectstack/spec)
  • content/docs/permissions/rls.mdx (via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx (via @objectstack/spec)
  • content/docs/plugins/development.mdx (via @objectstack/spec)
  • content/docs/plugins/index.mdx (via @objectstack/objectql, @objectstack/rest, @objectstack/spec)
  • content/docs/plugins/packages.mdx (via @objectstack/objectql, @objectstack/rest, @objectstack/runtime, @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx (via @objectstack/spec)
  • content/docs/protocol/diagram.mdx (via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/http-protocol.mdx (via @objectstack/runtime)
  • content/docs/protocol/kernel/i18n-standard.mdx (via packages/rest, @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/objectql, @objectstack/runtime, @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/runtime, @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/runtime-capabilities.mdx (via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/query-syntax.mdx (via packages/objectql, @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx (via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/objectql, @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx (via @objectstack/spec)
  • content/docs/releases/implementation-status.mdx (via @objectstack/objectql, @objectstack/rest, @objectstack/runtime, @objectstack/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v12.mdx (via @objectstack/rest, @objectstack/spec)
  • content/docs/releases/v13.mdx (via @objectstack/spec)
  • content/docs/releases/v16.mdx (via @objectstack/spec)
  • content/docs/releases/v17.mdx (via @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/objectql, @objectstack/spec)
  • content/docs/ui/actions.mdx (via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx (via @objectstack/spec)
  • content/docs/ui/dashboards.mdx (via @objectstack/spec)
  • content/docs/ui/forms.mdx (via @objectstack/spec)
  • content/docs/ui/index.mdx (via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx (via @objectstack/spec)
  • content/docs/ui/setup-app.mdx (via @objectstack/spec)
  • content/docs/ui/translations.mdx (via @objectstack/spec)
  • content/docs/ui/views.mdx (via @objectstack/spec)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

baozhoutao and others added 3 commits July 29, 2026 22:38
…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>
@baozhoutao
baozhoutao merged commit 507b92a into main Jul 30, 2026
18 checks passed
@baozhoutao
baozhoutao deleted the claude/issue-3957-review-8c0daf branch July 30, 2026 10:28
baozhoutao and others added 4 commits July 30, 2026 03:55
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Field validation messages are hardcoded English + API field name — penalty_amount must be ≥ 0 reaches end users verbatim

1 participant