Skip to content

fix(runtime,i18n)!: /i18n/locales answers in one shape, plus the success-envelope conformance gate that found it - #3870

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

fix(runtime,i18n)!: /i18n/locales answers in one shape, plus the success-envelope conformance gate that found it#3870
os-zhuang merged 1 commit into
mainfrom
claude/gettranslationsrequest-namespace-keys-rv08r7

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Follow-up to #3676 / #3833 / #3847 — the generalizable part of that trilogy, plus the fourth gap it immediately caught.

Why a gate, not another one-off fix

Those three defects were each a body that did not match the schema declaring it. All three survived a green suite, for one shared reason:

Every test asserted the emitted body against a hand-written literal. Comparing output to a literal proves the code does what the test author believed. It cannot prove the code does what the contract declares. Nothing had ever put the emitted value and the declared schema in the same assertion.

That is not three bugs, it is one missing check.

The gate

packages/runtime/src/i18n-success-envelope.conformance.test.ts — the missing success-path twin of service-i18n's error-envelope.conformance.test.ts, and the same pairing storage got in #3689 (success-envelope.conformance.test.ts, whose header notes i18n went through the error half in #3675 and never the success half).

Every /i18n success body is parsed against BaseResponseSchema and against the schema plugin-rest-api names for that route — responseSchema: 'GetLocalesResponseSchema', etc. — imported rather than restated. A body that parses is a body the SDK's published return type does not lie about.

It found a fourth gap on its first run

GET /i18n/locales passed getLocales()'s raw string[] straight through the dispatcher, while GetLocalesResponseSchema declares { code, label, isDefault }[] — and service-i18n, the other provider of this identical route, already emitted descriptors.

One endpoint, two shapes, decided by which plugin mounted it, with the dispatcher's form contradicting the SDK's own GetLocalesResponse type.

That is the same split #3833 found in the field-labels derivation, one route over, and for the same reason: two surfaces, one mapping, kept twice. So the mapping is now shared as toLocaleDescriptors in packages/spec/src/system/i18n-resolver.ts, next to resolveObjectFieldLabels, and both surfaces call it.

label is the locale code. No display-name source exists in the tree and the schema requires the field; inventing an ICU display-name table here would be a product decision, not an implementation detail.

Verified by reverting, not by passing

Same discipline as #3846. The fix was reverted and the suite confirmed to fail on it:

locales body does not match its declared schema:
  [{"expected":"object","code":"invalid_type","path":["locales",0],
    "message":"Invalid input: expected object, received string"}, …]
 Tests  3 failed | 3 passed (6)

A regression test never observed failing is just another assertion.

Five existing tests pinned the bare string[]. They now assert on .map(l => l.code) — the codes stay pinned, the shape is owned by the schema. That is the intended division: literals for values, schemas for shapes.

Verification

  • @objectstack/spec — 262 files / 6822 tests pass
  • @objectstack/runtime — 48 files / 702 tests pass
  • @objectstack/service-i18n — 5 files / 63 tests pass
  • Conformance suite confirmed failing against pre-fix code, passing after
  • check:api-surface — regenerated (0 breaking, toLocaleDescriptors + LocaleDescriptor added), re-verified clean
  • check:docs — 250 generated files in sync
  • @objectstack/client builds clean

Breaking

GET /i18n/locales served by the dispatcher now returns [{ code, label, isDefault }] instead of ['en', …]. Callers on the service-i18n mount already received this shape, and the SDK's published GetLocalesResponse type has always described it — this ends a divergence rather than starting one.

Worth generalizing

This suite covers one route family. The same check is mechanically available repo-wide today: plugin-rest-api.zod.ts already carries a responseSchema name on essentially every route (29 declarations across 28 handlers), so the route → declaring-schema mapping needed to drive it exists and is currently unused by any check.

Note the existing check:liveness gate does not cover this: it is registry-rooted over authorable metadata types, not API request/response schemas. That is exactly the blind spot all four gaps lived in.

Also swept while here, per #3833's follow-up: every remaining reader of a translation bundle (rest-server.ts:1681, the two i18n surfaces, the two adapters) is accounted for — no third consumer is still on the retired o. dialect.


Generated by Claude Code

…ccess-envelope conformance gate that found it

Follow-up to #3676 / #3833 / #3847. Those three were each a body that did not
match the schema declaring it, and each survived a green suite because every
test asserted the emitted body against a hand-written literal. Comparing output
to a literal proves the code does what the test author believed; it cannot prove
the code does what the contract declares. Nothing had ever put the emitted value
and the declared schema in the same assertion.

This adds that assertion as a suite — `i18n-success-envelope.conformance.test.ts`
in runtime, the missing success-path twin of service-i18n's
`error-envelope.conformance.test.ts` and the same pairing storage got in #3689.
Every `/i18n` success body is parsed against `BaseResponseSchema` and against the
schema `plugin-rest-api` names for that route, imported rather than restated.

It found a fourth gap on its first run. `GET /i18n/locales` passed
`getLocales()`'s raw `string[]` straight through the dispatcher, while
`GetLocalesResponseSchema` declares `{ code, label, isDefault }[]` — and
service-i18n, the OTHER provider of this identical route, already emitted
descriptors. One endpoint, two shapes, decided by which plugin mounted it, with
the dispatcher's form contradicting the SDK's own `GetLocalesResponse` type.

That is the same split #3833 found in the field-labels derivation, one route
over, and for the same reason: two surfaces, one mapping, kept twice. The mapping
is now shared as `toLocaleDescriptors` next to `resolveObjectFieldLabels`, and
both surfaces call it. `label` is the locale code — no display-name source exists
in the tree and the schema requires the field; inventing an ICU display-name
table here would be a product decision, not an implementation detail.

The gate was verified the way #3833's was: the fix was reverted and the suite
confirmed to FAIL on it ("expected object, received string" at locales[0]),
rather than merely passing once written. Five existing tests pinned the bare
`string[]`; they now assert on `.map(l => l.code)`, so the codes stay pinned
while the shape is owned by the schema.

BREAKING: `GET /i18n/locales` served by the dispatcher now returns
`[{ code, label, isDefault }]` instead of `['en', ...]`. Callers on the
service-i18n mount already received this shape, and the SDK's published
`GetLocalesResponse` type has always described it, so this ends a divergence
rather than starting one.

Worth generalizing beyond `/i18n`: `plugin-rest-api.zod.ts` already carries a
`responseSchema` name on essentially every route (29 declarations across 28
handlers), so the route -> declaring-schema mapping needed to run this check
repo-wide exists today and is unused.

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 1:14pm

Request Review

@github-actions github-actions Bot added size/m 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 13:31
@os-zhuang
os-zhuang merged commit 41642b0 into main Jul 28, 2026
17 checks passed
@os-zhuang
os-zhuang deleted the claude/gettranslationsrequest-namespace-keys-rv08r7 branch July 28, 2026 13:32
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.

2 participants