fix(i18n): render I18nLabel objects at the 13 remaining sites - #4208
Merged
Conversation
…pec bump (objectstack#7100) `global_nav` was an `ACTION_LOCATIONS` member no running-app surface ever rendered — the console's ⌘K palette reads no action metadata at all — and the Studio designer previewed a mock command-palette frame for it anyway. @objectstack/spec 17.0.0-rc.6 retired the member (objectstack#6888, maintainer ruling 2026-08-09 direction 2); this removes the designer surfaces that outlived it, plus the bump that makes them stop compiling. - ActionPreview.tsx: the mock ⌘K placement frame is gone. The metadata strip still echoes the declared token, deliberately — reporting what a draft says is honest; the frame CLAIMED the platform renders it. - ActionDefaultInspector.tsx: the `LOCATION_LABELS` entry is gone. The map is `Record<ActionLocation, string>`, so the retirement arrived as a compile error (objectui#3017's mechanism firing as designed). - block-config.ts + both locale tables: the palette option and its now-orphaned i18n key. - components' action:bar doc comment aligned (its enum is `[...ACTION_LOCATIONS]` and followed on its own). - ActionEngine.getActionsForLocation is UNCHANGED: narrowing it would be a second rejection point beside the schema's. The type and `ActionLocationSchema` are the enforcement, both pinned. The bump (all 37 `@objectstack/*` declarations, rc.5 -> rc.6) rides along because the two cannot compile apart, and its fallout is repaired here: the `…Input` alias retirement (types/theme, types/spec-report, core/ActionRunner), the five dead i18n re-exports, the `I18nLabel` widening at 8 render sites, `.extend` -> `.safeExtend` on a now-refined `GlobalFilterSchema`, and two parity lists re-pointed. NOT green: the bump's remaining surface is out of this card's scope and filed — objectui#4165 (contract conflict, blocks), #4167 (adaptation remainder), #4163 (I18nLabel consumer story), #4164 (name reclaim available). Part of objectstack-ai/objectstack#7100 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Qqyix2QcnpUC9XeYVDzx3
… one conflict
Second pass over the @objectstack/spec 17.0.0-rc.6 bump. The first commit fixed
every COMPILE error; a full `vitest run` (14615 tests) then surfaced the runtime
half. All mechanical, each pinned; the one non-mechanical case is left as a
tripwire rather than decided.
- types/zod/complex.zod.ts: `GlobalFilterSchema` composes by spreading the spec's
`.shape`. rc.6 closed BOTH extension doors on a now-refined schema — `.extend()`
throws at module load, `.safeExtend()` types the three overridden keys as
`never` (TS2322, which only `apps/site`'s build caught). The spread preserves
rc.5 behaviour exactly and decides nothing: the refinement-vs-stored-`{preset}`
conflict is objectui#4165, pinned by a new tripwire that reds from either side.
The pin isolates the refinement by widening `defaultValue` first — the bare spec
schema rejects `{preset}` on the OLD field type and the refinement never runs,
so the obvious shorter assertion would have pinned the wrong thing.
- core/actionKeys.ts: `description` joins SPEC_ACTION_KEYS (rc.6 added it to
`ActionSchema`). Inventory only — `ActionDef` is untouched.
- metadata-admin flow tests x2: `FlowNodeSchema` became pipe-wrapped, so `.shape`
read `undefined`. Access-path repair (walk the wrapper); the blocks themselves
are unchanged upstream, and a non-vacuity guard now says so.
- clientValidation.optOuts: INVERTED — rc.6 makes `SharingRuleSchema` declare the
seven ADR-0010 envelope keys, so the create door now ACCEPTS a stamped
`_packageId`. Upstream convergence, pinned as accept, with a bogus-key case
proving the door is still a door.
- clientValidation.viewDiagnostics: fixture supplies a `label`; rc.6's widened
`I18nLabel` made every label a LEAF union, which #3606's root-union expansion
does not reach. The resulting bare `Invalid input` is recorded as its own pin
(objectui#4163), not papered over.
- components/record-picker parity: fixture drops `displayField` — an ADR-0087 D2
tombstone refused by name, which was failing all three `filter` assertions.
- console registry parity: `SPEC_SHAPE_EMPTY_ON_THE_PIN` deleted exactly as its
own self-clearing test instructed (rc.6 lands `PageContainerProps`), with a
tombstone assertion so the deletion cannot be silently undone.
- i18n residue ratchet: matches `const { t, … } = useObjectTranslation(` by
pattern. Both sidebars legitimately also take `language` now, to resolve the
spec's inline per-locale labels.
Still red, filed, NOT fixed here: 2 console-parity exemption-list entries and
`check:spec-symbols`' 13 name collisions (objectui#4167) — each is per-symbol
triage, not a mechanical edit — and objectui#4165, which blocks ready.
Part of objectstack-ai/objectstack#7100
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Qqyix2QcnpUC9XeYVDzx3
…console parity exemptions (#4167) objectui#4167, objectstack#4115. `check:spec-symbols` reported 13 collisions on the @objectstack/spec 17.0.0-rc.6 bump and a 14th (`GlobalFilterSchema`) appeared during the bump itself; each is triaged on its own rather than blanket-renamed. Renamed (3) — the spec's same-named export means something else: MetadataState -> MetadataCacheState (react, app-shell) resolveI18nLabel -> resolveKeyedI18nLabel (react, app-shell, + apps/console) DateRangePreset -> FilterBuilderDateRangePreset (types) Bound or derived (10): DATE_RANGE_PRESETS (+ a `satisfies` completeness pin on the local bounds table), NavigationMode (+ a both-directions type pin), AddressValue, BreakpointColumnMap, BreakpointOrderMap, KanbanConfig, CalendarConfig, GanttConfig. Four copies were losing information: GanttConfig declared 6 of the spec's 17 keys while plugin-gantt reads all of them, AddressValue 5 of 7 parts, and DATE_RANGE_PRESETS was the third copy objectstack#4614 extracted the vocabulary to collapse. ALLOW (1): GlobalFilterSchema, the same `.shape`-spread dialect as SelectOptionSchema beside it — it collided only because rc.6's refinement forced `.extend()` to be respelled, moving a derivation the guard could see into an object literal it deliberately does not descend into. Nothing about #4165 is encoded. Console registry-inputs-spec-parity: all 12 off-spec exemptions deleted (rc.6 landed objectstack#6776 and objectstack#5775, verified per key against the resolved shape), and `element:record_picker` publishes `sort` / `limit` / `emptyText` — read by the renderer all along, and the red demanding them was predicted in writing by the exemption it replaces. 49 tests/2 failing -> 51/0. Also repairs 3 pre-existing type-check errors of this card's own rc.6 family, found only by running `type-check` (which the bump PR did not): the retired `ThemeInput` alias re-pointed BY SIDE, and `DroppedFieldsEvent.reason`'s new `primary_key` arm. The consumer gap that arm exposes is objectui#3935. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Qqyix2QcnpUC9XeYVDzx3
`@objectstack/spec` 17.0.0-rc.6 widened `I18nLabel` from `string` to
`string | Record<string, string>`, so an author may inline a per-locale map
(`label: { en: 'Owner', 'zh-CN': '负责人' }`) anywhere the spec accepts a
display label. PR #4169 repaired eight such sites; thirteen more were invisible
to it because the five packages involved build through vite/rolldown, so
`turbo run build` never type-checks their sources — only `turbo run type-check`
does.
Read sites now resolve the map to the viewer's locale instead of reaching a
text node as `[object Object]`:
- `@object-ui/layout` — `NavigationArea.label` (area switcher button + tooltip)
- `@object-ui/plugin-list` — `ViewTab.label` (pill row, dropdown trigger, menu)
- `@object-ui/plugin-dashboard` — `DashboardWidget.title` (heading + attribute)
- `@object-ui/plugin-designer` — `DashboardWidget.title` (card + preview tile)
- `@object-ui/app-shell` — `ActionParam.label` and each option's `label`,
fixed on the `RawActionParam` DECLARATION rather than on the test that
surfaced it
Two edges are deliberate rather than mechanical:
`DashboardEditor`'s title `<input>` is an authoring WRITE, not a display.
Resolving a map into it and writing `e.target.value` back would collapse every
other locale on the first keystroke, so the write is GUARDED — a map-valued
title renders resolved and read-only, and survives an unrelated edit-and-save
round trip untouched. The type error dies from the guard, not from a cast.
What Studio should actually offer for authoring a per-locale label is #4163
part 2, which stays unclaimed and pending design.
`@object-ui/layout` carries no i18n dependency by design (its whole i18n story
is injection), so it uses the PRODUCER's resolver — `resolveI18nLabel` from
`@objectstack/spec/ui` — at the spec's documented `en` default rather than
hand-rolling a resolution that could drift. The three plugin packages sit
inside objectui's i18n tree and already hold the live language, so they use
`pickLocalized`; `plugin-list/src/__tests__/i18nLabel-resolver-parity.test.ts`
pins the two resolvers limb for limb so the runtime dashboard and the
designer's preview of it cannot start disagreeing about which entry wins.
Also re-points four `plugin-list` type-test assertions at `ViewTabParsed`:
rc.6's `…Input`-alias retirement moved the bare `ViewTab` onto the input side,
so following the NAME left them comparing the authoring side against itself.
Invisible until now because `type-check` is `tsc --noEmit && tsc -p
tsconfig.typetests.json` and the `&&` never reached that project.
Part of #4163
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
This was referenced Aug 10, 2026
…nlabel-render-sites # Conflicts: # packages/app-shell/src/layout/AppSidebar.tsx # packages/app-shell/src/layout/UnifiedSidebar.tsx # packages/fields/src/widgets/AddressField.tsx # packages/layout/package.json # pnpm-lock.yaml
Three residual reds on main after the rc.5→rc.6 spec bump (#4169), none of which the train cars themselves covered. QUICK_REFERENCE.md's "Current Release" block still quoted `^17.0.0-rc.5` for both `@objectstack/spec` and `@objectstack/client` while every manifest it names as its anchor declares `rc.6`. The pin test for that block (quick-reference-current-release-4143) landed on main AFTER the train was built, so no car ever ran against it. recordDetailsInputs.spec-parity carried `layout: 'custom'` in both fixtures of its object-sections case. `layout` was removed in @objectstack/spec 17.0.0 (objectstack#6946, ADR-0087 D2) and rc.6 gave it a named `never` rejection, so the object-form fixture failed on `layout` while its `sections` were valid — and the id-list fixture above it kept PASSING on `layout`'s own `invalid_type` without ever reaching `sections`. Dropping the key puts both verdicts back on `sections`; the id-list case now also pins the issue PATH, since a bare code could not tell the two rejections apart. AddressField re-exports `AddressValue` through its declaring module rather than bare. main's #4222 moved the declaration into the pure `address-format` module while this train re-sourced the type from `@objectstack/spec/data`; the merge keeps both by importing the spec type in `address-format` (where the definition now lives) and letting the widget re-export from there, which is the form check:spec-symbols resolves to a declaration site instead of reading as a second local declaration of a name the spec owns.
yinlianghui
changed the base branch from
claude/issue-4167-rc6-adaptation-remainder
to
main
August 11, 2026 05:11
yinlianghui
marked this pull request as ready for review
August 11, 2026 05:13
This was referenced Aug 11, 2026
This was referenced Aug 11, 2026
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.
Fixes #4167
Part of #4163
Part 1 only for #4163 — the thirteen render sites. Part 2 (what Studio should offer for authoring a per-locale label) stays unclaimed and pending design, so #4163 is
Part ofrather thanFixes. #4167 is closed in full.What this PR now delivers
Three layers, in the order they were built:
@objectstackfamily at17.0.0-rc.6— the adaptation surface beyond what objectstack#7100 could carry #4167 — the 14 rc.6 spec-symbol collisions triaged, console parity exemptions refreshed, record-picker inputs published. This is what repairs main'scheck:spec-symbolsred. PR fix(deps): triage the 14 rc.6 spec-symbol collisions and refresh the console parity exemptions (#4167) #4189's auto-merge was disarmed by the PM and its content lands here instead; that PR should be closed unmerged once this one lands.I18nLabelnow admits an inline per-locale map — audit every read the compiler cannot see, and give Studio a way to author one #4163 part 1 — the thirteenI18nLabelrender sites. This is what repairs theresolveActionParamstypetest TS2322s.The merge into main, and what conflicted
git merge origin/main(merge, not rebase — shared history with an open PR). Main had moved on by 3 commits, including #4222 and #4218. Five conflicts:packages/app-shell/src/layout/AppSidebar.tsxresolveI18nLabeltoresolveKeyedI18nLabel(the #4167 collision triage);utils/index.tsand every other call site already carry the new name post-merge.packages/app-shell/src/layout/UnifiedSidebar.tsxpackages/layout/package.json@objectstack/specfromdevDependenciestodependencies(layout now imports the spec resolver at runtime); main still had it indevDependencies.pnpm-lock.yamllayout/package.jsonmove.pnpm installthen reported "Lockfile is up to date".packages/fields/src/widgets/AddressField.tsxThe one conflict that was not a text conflict
Both sides changed
AddressValue, in ways that were individually correct and jointly incoherent:[Object]#4222) extractedAddressValue/LegacyAddressValue/readPostalCode/formatAddressout of the widget into a new pure module./address-format, so the display cell renderer and the widget share one formatting rule.@objectstackfamily at17.0.0-rc.6— the adaptation surface beyond what objectstack#7100 could carry #4167) stopped declaringAddressValueand imported it from@objectstack/spec/data, because rc.6 publishes that exact name and the local copy declared five of the seven parts (countryCodeandformattedwere missing).Taking either side alone loses the other's point, and taking main's side alone reopens main's red: the declaration simply moves to
address-format.ts, wherecheck:spec-symbolsstill sees a local declaration of a name the spec owns. The resolution keeps both —address-format.ts(the new home of the definition) imports the type from@objectstack/spec/data, andAddressField.tsxre-exports through that module rather than bare. Thefromclause matters: the guard deliberately skips a re-export carrying a relative path ("whatever it points at gets judged at its own declaration site") but treats a bareexport type { AddressValue }as a second local declaration.The residual red set — measured on the combined tree, before and after
Neither train car fixed these. Both were verified red on the merged tree before being fixed, and the reverse verification below puts them back.
1.
QUICK_REFERENCE.mdstill quoted rc.5 — 3 failures. The "Current Release" block stated^17.0.0-rc.5for both@objectstack/specand@objectstack/client, while all three manifests it names as its own anchors declare^17.0.0-rc.6. Its pin test (scripts/__tests__/quick-reference-current-release-4143.test.ts) landed on main after the train was built, so no car ever ran against it. Fix: the two rows. The third failure was the block's own "no un-derived literal" assertion, which fell out with them.2.
recordDetailsInputs.spec-parity.test.ts:123— 1 failure, and NOT aboutsections. The premise check found the real cause, which is worth stating because it is the opposite of what the failure looks like. Both fixtures in the object-sections case carriedlayout: 'custom'.layoutwas removed fromrecord:detailsin spec 17.0.0 (objectstack#6946, ADR-0087 D2), and rc.6 gave the key a namedneverrejection:So the object-form fixture failed on
layoutwhile itssectionswere perfectly valid. Worse, and invisibly: the id-list fixture directly above it kept PASSING — it asserts only that the parse fails with codeinvalid_type, andlayout's own rejection satisfies both without the parse ever reachingsections. That assertion was green for a reason unrelated to what it claims to test. Dropping the key puts both verdicts back onsections(issues now resolve to pathsections.0/sections.1), and the id-list case additionally pins the issue path, since a bare code cannot tell the two rejections apart. This is a test-fixture fix only; noplugin-detailsource was touched.The file's header prose was also updated — it opened "Under
layout: 'custom'sections are the ONLY source of the body", a sentence about a key that no longer exists.Local gate ladder
Run on the final tree (
5f5543316).NODE_OPTIONS=--max-old-space-size=4096,--maxWorkers=2/--concurrency=2, every heavy step underflock /tmp/os-heavy-verify.lock. Vitest invoked from the repo root (objectui#3378).1. Build closure
2. Type-check — both commands
Ten packages define a
tsconfig.typetests.json, and in every one the package script istsc --noEmit && tsc -p tsconfig.typetests.json, so the 78/78 covers both commands rather than only the first. Explicit receipts for the three the change touches most directly:3.
check:spec-symbols— the gate that is red on mainThe 3 untriaged collisions are the pre-existing
DEBTledger, unchanged by this PR and non-fatal by design. For contrast, the same gate on the tree mid-merge, before theaddress-formatresolution:4. Tests
apps/consoleis run separately and deliberately: it owns the repo-wideregistry-inputs-spec-parity.test.tsgate, which the merge touched.5. Reverse verification of the residual fixes
Predicted direction, stated before running: reverting the two residual files turns the quick-reference test red on 3 cases (Spec row, Client row, un-derived-literal) and
recordDetailsInputsred on 1. Taken out withgit checkout HEAD -- ...and a scoped patch file, nevergit stash. Confirmed exactly:One honest correction to the prediction: the reverted
recordDetailsInputsfailure lands on the originalobjectForm.successassertion, not on the new path assertion — reverting the file necessarily removes the new assertion along with the fix. Restoring returns to17 passed (17).6. ESLint on changed files
0 errors. The warnings are the pre-existing
no-explicit-anyconvention inpackages/types/src/objectql.tsand the_Assert…unused-type-alias pattern in thespec-symbol-batch6type tests.7. i18n + byte gates
8. Changesets
The train's three changesets are already in the branch and cover every released package this PR touches:
.changeset/spec-symbol-collisions-rc6-4167.md— minor fortypes,core,react,app-shell,layout,fields,components; patch forplugin-designer.@object-ui/fieldsis already covered here, and still accurately: theAddressValuere-sourcing it describes survives the merge, relocated intoaddress-format.ts..changeset/record-picker-sort-limit-empty-text-4167.md— minor forcomponents..changeset/i18nlabel-render-sites-4163.md— patch for the five render-site packages.No new changeset: of the three residual fixes, one is a repo doc (
QUICK_REFERENCE.md), one is test-only (plugin-detail), and the third is a re-export form change in@object-ui/fieldswhose published type is unchanged and whose package is already covered minor.Original #4163 part-1 detail, retained
What changed upstream
@objectstack/spec17.0.0-rc.6 widenedI18nLabelfromstringtostring | Record< string, string >, so an author may inline a per-locale map —label: { en: 'Owner', 'zh-CN': '负责人' }— anywhere the spec accepts a display label. (Angle brackets spaced throughout this body so GitHub's sanitizer does not eat them; tsc printed them unspaced.)PR #4169 repaired eight such sites. Thirteen more were invisible to it for a mechanical reason worth restating: the five packages involved build through vite/rolldown, not
tsc, soturbo run buildnever type-checks their sources. Onlyturbo run type-checkcompiles them. A bump can therefore be build-green, test-green, and still carry a double-digit type-error count in exactly the packages that render labels.@object-ui/layoutNavigationArea.label— area switcher button + tooltip@object-ui/plugin-listViewTab.label— pill row, dropdown trigger, menu items@object-ui/plugin-dashboardDashboardWidget.title— card heading +titleattribute@object-ui/plugin-designerDashboardWidget.title— widget card + preview tile@object-ui/app-shellActionParam.labeland eachActionParam.options[].labelThe two edges that are decisions, not mechanics
1.
DashboardEditor.tsx:231is an authoring WRITE, not a read. Resolving a map into a single-line text input and writinge.target.valueback would collapse every other locale on the first keystroke — an author who opened a dashboard to move a widget and happened to focus the title field would silently destroy the translations. So the write is guarded, not cast: a map-valued title renders resolved and read-only, and the stored map survives an unrelated edit-and-save round trip untouched. The type error dies from the guard. This is the same conservative branch #4169 took onDashboardWidgetInspector, and it is a placeholder that cannot lose data — not the answer. The answer is #4163 part 2.2.
@object-ui/layoutcarries no i18n dependency by design. Its whole i18n story is injection (NavigationRenderertakestand the label resolvers as arguments), andAppSchemaRendererPropsexposes no locale, not, and no context carrying one. So it calls the producer's own resolver —resolveI18nLabelfrom@objectstack/spec/ui— rather than hand-rolling a resolution that could drift, and runs it at the spec's documented "no locale known" default (en). Stated rather than hidden: an area labelled with a map renders itsenentry, not the viewer's language. That is strictly better than[object Object], and it is a floor — the day a consumer needs per-viewer area labels, the fix is to thread a locale in as a prop, and this call is the one place it lands. No new package dependency.Anti-drift: two resolvers over one vocabulary, now pinned
layoutandapp-shellcall the spec'sresolveI18nLabel; the three plugin packages sit inside objectui's i18n tree, already hold the live UI language, and callpickLocalized(which answers''on a miss, which is what a text node wants, where the spec's answersundefined).Two functions over one vocabulary is exactly the drift the ruling warns about, and the harm is not a crash — it is the same authored map rendering one locale on the runtime dashboard and a different one in the designer's preview of that same dashboard, with every type and every other test green. Nothing compared them, so this PR adds
packages/plugin-list/src/__tests__/i18nLabel-resolver-parity.test.ts: all six limbs of the rule (exact tag, base, regional sibling,default,en, any), asserted on both functions from one table, with the single permitted difference (the miss spelling) normalized in one named place and pinned in both directions.Also in this PR
Four
plugin-listtype-test assertions re-pointed atViewTabParsed. rc.6's…Input-alias retirement moved the bareViewTabonto the input side, so following the NAME left them comparing the authoring side against itself — passing on nothing. Same swap #4189 avoided forThemeInput/Theme, different package. Invisible until now becausetype-checkistsc --noEmit && tsc -p tsconfig.typetests.jsonand the&&never reached that project whileTabBar.tsxwas red.Generated by Claude Code