fix(runtime,i18n): field labels read the bundle shape producers write — one shared derivation (#3833) - #3846
Merged
os-zhuang merged 1 commit intoJul 28, 2026
Conversation
… — 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
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 3 package(s): 113 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
os-zhuang
marked this pull request as ready for review
July 28, 2026 11:37
os-zhuang
deleted the
claude/gettranslationsrequest-namespace-keys-rv08r7
branch
July 28, 2026 11:37
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 #3833. Found while verifying #3676 (PR #3832) and filed rather than folded into it.
The bug
GET /i18n/labels/:object/:localeserved through the dispatcher returned{ labels: {} }for every provider.packages/runtime/src/domains/i18n.ts:94scanned for flato.<object>.fields.<field>keys — the dialect #3778 retired. No producer has ever written it, and a real bundle's top-level keys are theTranslationDatagroups (objects,apps,messages, …), so the prefix matched nothing.4cca74cfixed the identical derivation inservice-i18nand 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 onII18nServiceand implemented by nothing — swept the repo: neithermemory-i18nnorfile-i18n-adapterhas 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
resolveObjectFieldLabelsinpackages/spec/src/system/i18n-resolver.ts, beside the other resolvers that readTranslationData. 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
labelstay omitted rather than emitted blank. Partial translation is the normal state (perObjectTranslationDataSchema'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_namekeys 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 —
— 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
getFieldLabelsno shipped provider has, and returned flat-dialect data fromgetTranslations. 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 passcheck:api-surface— regenerated (1 added export, 0 breaking) and re-verified cleancheck:docs— 250 generated files in syncDeliberately out of scope
GetFieldLabelsResponseSchemadeclareslabelsasRecord<string, { label, help?, options? }>, but both surfaces emitRecord<string, string>— a third declared ≠ enforced gap on this same endpoint, which also means thehelpandoptionsthe 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.