Skip to content

fix(runtime,i18n): field labels read the bundle shape producers write — one shared derivation (#3833) - #3846

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

fix(runtime,i18n): field labels read the bundle shape producers write — one shared derivation (#3833)#3846
os-zhuang merged 1 commit into
mainfrom
claude/gettranslationsrequest-namespace-keys-rv08r7

Conversation

@os-zhuang

@os-zhuang os-zhuang commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Closes #3833. Found while verifying #3676 (PR #3832) and filed rather than folded into it.

The bug

GET /i18n/labels/:object/:locale served through the dispatcher returned { labels: {} } for every provider. packages/runtime/src/domains/i18n.ts:94 scanned for flat o.<object>.fields.<field> keys — the dialect #3778 retired. No producer has ever written it, and a real bundle's top-level keys are the TranslationData groups (objects, apps, messages, …), so the prefix matched nothing. 4cca74c fixed the identical derivation in service-i18n and did not reach the dispatcher's copy.

It is not a rare fallback — it is the only path

The branch guards on typeof i18nService.getFieldLabels === 'function'. That method is optional on II18nService and implemented by nothing — swept the repo: neither memory-i18n nor file-i18n-adapter has it, and there is no other provider. So the dedicated-method branch is dead in production, and the broken derivation is what every request reaches.

That answers the reachability question the issue left open: this is live, not latent. Any stack served by the dispatcher — the AppPlugin in-memory provider auto-registered for stacks declaring translation bundles — got an empty map, indistinguishable from "this object has no translated labels". Nothing errored, nothing warned.

Worse than the class it was found beside: #3676 ignored a declared filter and returned the full bundle — a correct superset. This returned nothing and said it was fine.

The fix

The derivation now lives once, as resolveObjectFieldLabels in packages/spec/src/system/i18n-resolver.ts, beside the other resolvers that read TranslationData. Both surfaces call it.

Sharing it is the point, not a tidy-up: two copies of the same logic is exactly how one got fixed by #3778 and the other did not. The next bundle-shape change now has one place to land.

It is distinct from the existing lookupObjectFieldAttr, which answers "what is this one field called" against a whole bundle and a locale chain. This answers "which fields does this locale translate at all" — an enumeration no per-field lookup can produce.

Fields carrying no non-empty label stay omitted rather than emitted blank. Partial translation is the normal state (per ObjectTranslationDataSchema's own reasoning), and callers merge this map over their source labels, where a '' would erase them.

The tests were fiction on both sides

This is the part worth reviewing.

The dispatcher's fallback test fed flat o.contact.fields.first_name keys and asserted labels came back. It passed on data that cannot occur, while production returned {} — the same failure mode as the client test retired in #3676, which asserted a query string was built that no server read. Two consecutive issues in this area were both protected by a green test built on a fiction.

So this test was verified the other way round: reverted the fix, ran the new test, confirmed it fails

AssertionError: expected {} to deeply equal { first_name: 'First Name', …(1) }
 Tests  1 failed | 197 skipped (198)

— then restored the fix and confirmed green. A regression test that was never observed failing is just another assertion.

The same suite's mock also declared a getFieldLabels no shipped provider has, and returned flat-dialect data from getTranslations. Both now reflect what a real provider does, with a note where the divergence stays deliberate (the tests covering a hypothetical provider that does supply the method are kept — that path is contractually allowed, just unused).

The shared helper carries its own unit tests, including one pinning that the retired flat dialect resolves to {}.

Verification

  • @objectstack/spec — 259 files / 6756 tests pass
  • @objectstack/runtime — 47 files / 694 tests pass
  • @objectstack/service-i18n — 5 files / 63 tests pass
  • New dispatcher test confirmed failing against pre-fix code, passing after
  • check:api-surface — regenerated (1 added export, 0 breaking) and re-verified clean
  • check:docs — 250 generated files in sync

Deliberately out of scope

GetFieldLabelsResponseSchema declares labels as Record<string, { label, help?, options? }>, but both surfaces emit Record<string, string> — a third declared ≠ enforced gap on this same endpoint, which also means the help and options the bundle already carries are dropped. Fixing it changes the wire shape, which is breaking and orthogonal to a correctness fix, so it is filed as #3847 rather than folded in here.

… — one shared derivation (#3833)

`GET /i18n/labels/:object/:locale` served through the dispatcher returned
`{ labels: {} }` for every provider. Its derivation scanned for flat
`o.<object>.fields.<field>` keys — the dialect #3778 retired. No producer has
ever written it, and a real bundle's top-level keys are the `TranslationData`
groups (`objects`, `apps`, `messages`, ...), so the prefix matched nothing.
4cca74c fixed the identical derivation in service-i18n and did not reach the
dispatcher's copy.

Not a rare fallback: `getFieldLabels` is optional on `II18nService` and nothing
implements it — not `memory-i18n`, not `file-i18n-adapter` — so the dedicated-
method branch both surfaces check first is dead in production and this
derivation is the only path there is. Any stack served by the dispatcher got an
empty map, indistinguishable from "this object has no translated labels".
Nothing errored, nothing warned.

Worse than the class it was found beside: #3676 ignored a declared filter and
returned the full bundle, a correct superset. This returned nothing and said it
was fine.

The derivation now lives once, as `resolveObjectFieldLabels` in
`packages/spec/src/system/i18n-resolver.ts`, beside the other resolvers that
read `TranslationData`. Both surfaces call it — keeping a copy each is precisely
how one got fixed and the other did not. Fields with no non-empty `label` stay
omitted rather than emitted blank: partial translation is the normal state, and
callers merge this map over their source labels, where a '' would erase them.

The tests were fiction on both sides. The dispatcher's fallback test fed flat
`o.contact.fields.first_name` keys and asserted labels came back, so it passed
on data that cannot occur while production returned `{}` — the same failure mode
as the client test retired in #3676. It now feeds the nested shape, and was
confirmed to FAIL against the pre-fix code rather than merely passing after it.
The same suite's mock declared a `getFieldLabels` no shipped provider has and
returned flat-dialect data; both now reflect what a real provider does. The
shared helper carries unit tests, including one pinning the retired flat dialect
to `{}`.

Filed separately, not addressed here: `GetFieldLabelsResponseSchema` declares
`labels` as `Record<string, { label, help?, options? }>` while both surfaces emit
`Record<string, string>` — a third declared != enforced gap on this endpoint, and
a wire-shape change too breaking to fold into a correctness fix.

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 9:40am

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 11:37
@os-zhuang
os-zhuang merged commit 720c5ad into main Jul 28, 2026
17 checks passed
@os-zhuang
os-zhuang deleted the claude/gettranslationsrequest-namespace-keys-rv08r7 branch July 28, 2026 11:37
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.

Dispatcher's getFieldLabels fallback still scans the retired flat o. prefix — always returns {}

2 participants