Skip to content

fix(i18n)!: field labels emit the entry shape they declare, and stop discarding help/options (#3847) - #3859

Merged
os-zhuang merged 1 commit into
mainfrom
claude/gettranslationsrequest-namespace-keys-rv08r7
Jul 28, 2026
Merged

fix(i18n)!: field labels emit the entry shape they declare, and stop discarding help/options (#3847)#3859
os-zhuang merged 1 commit into
mainfrom
claude/gettranslationsrequest-namespace-keys-rv08r7

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Closes #3847. Third and last of the declared ≠ enforced gaps on this endpoint family, after #3676 (request filters no server read) and #3833 (a derivation scanning a retired dialect).

The gap

GetFieldLabelsResponseSchema has always declared each label as an object:

labels: z.record(z.string(), z.object({
  label: z.string(),
  help: z.string().optional(),
  options: z.record(z.string(), z.string()).optional(),
}))

Both serving surfaces emitted Record<string, string> — a bare label per field. A client typed against GetFieldLabelsResponse read labels[field].label and got undefined, because the value was the string. The SDK's type was right the whole time; the servers were wrong.

Why it is more than a type mismatch

FieldTranslationSchema carries help and options, bundles populate them, and the endpoint threw them away.

The issue asked whether this endpoint is even worth keeping, which turned on how objectui gets field labels. It does not call this route. It pulls the whole bundle from /i18n/translations/:locale and resolves client-side — packages/i18n/src/utils/spec-translations.ts transforms TranslationData into i18next resources, and useObjectLabel reads fields.<obj>.<fld>.

That transform reads label and options (as fieldOptions.<obj>.<fld>.<value>). So the data this endpoint was discarding is exactly the data its most likely consumer needs — it could not have served objectui even if objectui wanted it, because the loss happened at the emit site.

That answers the "should it exist" question in favor of fixing it: the declared shape is the useful one, and the endpoint was the only thing standing between it and a caller. Option 1 from the issue.

The fix

One change at the shared emit site, resolveObjectFieldLabels — which both surfaces already call as of #3833, so this covers the dispatcher and service-i18n together. That is the payoff from sharing it last time: this fix would otherwise have been two edits with one of them easy to miss, which is exactly how #3833 happened.

  • help and options attach only when non-empty. An options: {} would claim a field has translated options and hand back none; a help: '' would erase a caller's source help text.
  • Fields with no non-empty label are still omitted entirely — which is what lets ResolvedFieldLabel.label be a required string rather than optional.

The response schema is unchanged. This moves the implementation onto the contract, not the contract onto the implementation — check:docs reports the 250 generated files byte-identical for that reason, and check:api-surface shows 0 breaking / 1 added (the new ResolvedFieldLabel type).

On placeholder

Deliberately left out. FieldTranslationSchema has it; the response schema does not. Emitting it would be widening the contract rather than satisfying it, and that is a separate decision with no consumer asking. Adding an optional response field later is additive and non-breaking — the same low-regret reasoning that justified trimming namespace in #3676 rather than implementing it speculatively.

The regression guard

The part worth keeping:

const response = { object: 'contact', locale: 'en-US', labels: resolveObjectFieldLabels(data, 'contact') };
expect(GetFieldLabelsResponseSchema.safeParse(response).success).toBe(true);

Nothing had ever put the emitted value and the declared contract in the same assertion. That is precisely how a bare string sat under an object schema unnoticed across three issues in this area — the tests asserted emitted-vs-expected-literal, never emitted-vs-declared.

This is the small version of the CI gate worth generalizing: fixture and response bodies validated against the Zod schema that declares them would have caught this one, and would also have caught #3833's fixture (flat o. data cannot parse as TranslationDataSchema).

Verification

  • @objectstack/spec — 261 files / 6790 tests pass
  • @objectstack/runtime — 47 files / 694 tests pass
  • @objectstack/service-i18n — 5 files / 63 tests pass
  • check:api-surface — regenerated (0 breaking, 1 added), re-verified clean
  • check:docs — 250 generated files in sync, unchanged
  • @objectstack/client builds clean — its GetFieldLabelsResponse type already described this shape

Breaking

labels[field] is now { label, help?, options? } rather than a string. No consumer in this repo or objectui read it — objectui never calls the route, and in-repo use is the SDK method plus URL-shape tests — so the practical blast radius is nil, and this is the cheap moment to align it.


Generated by Claude Code

…discarding help/options (#3847)

`GetFieldLabelsResponseSchema` has always declared each label as
`{ label, help?, options? }`. Both serving surfaces emitted
`Record<string, string>` — a bare label per field. A client typed against
`GetFieldLabelsResponse` read `labels[field].label` and got undefined, because
the value was the string itself. The SDK's type was right the whole time; the
servers were wrong.

The cost is not only the type mismatch. `FieldTranslationSchema` carries `help`
and `options`, bundles populate them, and the endpoint threw them away. objectui
needs exactly those — its `spec-translations.ts` transform reads `label` AND
`options` (as `fieldOptions.<obj>.<fld>.<value>`) — and gets them by pulling the
whole bundle from `/i18n/translations/:locale` and resolving client-side. The
per-object endpoint could not have served it even if it wanted to: the data was
dropped at the emit site.

Fixed at that emit site, `resolveObjectFieldLabels`, which both surfaces already
share as of #3833, so one change covers both. `help` and `options` attach only
when non-empty: an `options: {}` would claim a field has translated options and
hand back none, and a `help: ''` would erase a caller's source help text. Fields
with no non-empty `label` are still omitted entirely, which is what lets
`ResolvedFieldLabel.label` be a required string.

The response schema is UNCHANGED — this moves the implementation onto the
contract, not the contract onto the implementation. Generated docs are
byte-identical for that reason.

`placeholder` is deliberately left out: `FieldTranslationSchema` has it and the
response schema does not, so emitting it would widen the contract rather than
satisfy it, and adding an optional response field later is additive.

The regression guard is the part worth keeping: a test that builds the response
body from the shared helper and parses it with `GetFieldLabelsResponseSchema`.
Nothing had ever put the emitted value and the declared contract in one
assertion, which is precisely why a bare string sat under an object schema
unnoticed. Third and last of the declared != enforced gaps on this endpoint
family, after #3676 and #3833.

BREAKING: `labels[field]` is now `{ label, help?, options? }` rather than a
string. No consumer in this repo or objectui read it — objectui never calls this
route, and in-repo use is the SDK method plus URL-shape tests — so the practical
blast radius is nil.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0196gwfwMK5vW8RToPyzGMJo
@vercel

vercel Bot commented Jul 28, 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 28, 2026 12:00pm

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation protocol:system tests tooling labels Jul 28, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/runtime, packages/services, @objectstack/spec.

113 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/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/spec)
  • content/docs/api/index.mdx (via @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 packages/services, @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 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 @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/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/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/audit-service.mdx (via packages/services)
  • content/docs/kernel/runtime-services/email-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/index.mdx (via packages/services, packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/settings-service.mdx (via packages/services)
  • 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/spec)
  • content/docs/permissions/authentication.mdx (via @objectstack/runtime)
  • content/docs/permissions/authorization.mdx (via packages/runtime, @objectstack/spec)
  • 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/spec)
  • content/docs/plugins/packages.mdx (via @objectstack/runtime, packages/services, @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/services, @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx (via @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 @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/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/runtime, @objectstack/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v12.mdx (via @objectstack/spec)
  • content/docs/releases/v13.mdx (via @objectstack/spec)
  • content/docs/releases/v16.mdx (via @objectstack/spec)
  • content/docs/releases/v9.mdx (via @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.

@os-zhuang
os-zhuang marked this pull request as ready for review July 28, 2026 12:56
@os-zhuang
os-zhuang merged commit 1d4756e into main Jul 28, 2026
17 checks passed
@os-zhuang
os-zhuang deleted the claude/gettranslationsrequest-namespace-keys-rv08r7 branch July 28, 2026 12:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:system size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

GetFieldLabelsResponse declares rich label entries; both surfaces emit bare strings

2 participants