Skip to content

fix(layout): a present navigation label renders verbatim, with no translate-if-equal-to-name exception, and an inline locale-map label renders its text (objectui#11201) - #11297

Merged
objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-11201-nav-label-verbatim
Oct 1, 2026
Merged

objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-11201-nav-label-verbatim

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #11201

Clause-②: no

Stage 2 of ruling B: the director's ruling 5921167691, on the ruling record 5910404448 and its execution note 5910537969. Claim 5921933038, branch claude/issue-11201-nav-label-verbatim. Stage 1 landed as PR #11225 (cb2f6fb5b).

What changes

resolveNavItemLabel in packages/layout/src/NavigationRenderer.tsx now follows the spec's one order and keeps no rule of its own:

  1. The id-keyed bundle entry apps.APP.navigation.ID.label. translateApp applies it upstream, at /meta. Untouched here.
  2. A present label: an inline locale map reads its locale's entry, and a string renders verbatim.
  3. An absent label inherits the target's current, localized label (objectui#9868, unchanged).
  • Deleted: the isCustomized arm and its comment. The arm looked a present plain-string label up through the object, view or dashboard resolver when its text equalled the target's machine name (trimmed, case-insensitive). Nothing on label matches text against a name now.
  • Added: a present map-valued entry label ({ en: 'Accounts', 'zh-CN': '客户' }, which the spec accepts) now reads through the spec's resolveI18nLabel. Areas and the designer canvas already use that resolver. The label used to reach the keyed resolver and render empty.
  • Kept, but not read: resolveObjectLabel, resolveDashboardLabel and resolveViewLabel (the props, and the matching positional arguments). They are marked @deprecated, so callers compile and get the ruled behaviour. Retiring them is open question 2 in the report.
  • packages/i18n/src/useObjectLabel.ts: the one comment that named the guard now says the guard is gone. Comment only.
  • Changeset: @object-ui/layout patch. It states the visible effect and the one-line fix: clear the entry's label so it inherits, or set the wanted text. No conversion, no notice run.

Mechanism finding: which locale a map label reads

The ruling's map pin says "renders the locale's value". This layer is not given a locale. NavigationRenderer has no locale prop, and @object-ui/layout has no i18n dependency by design (see the docblock of resolveAreaLabel in AppSchemaRenderer.tsx). Within the ruled parameters (patch, Clause-②: no, no public prop change), the map resolves at the spec resolver's documented no-locale default: its en entry, else default, else any entry. The area switcher in this package does the same. So in the console, a zh-CN viewer sees the en entry of a map-valued nav label.

Reaching the viewer's locale needs a new optional prop, with the locale threaded from the console (UnifiedSidebar already holds language). That widens the public surface, so it is not built here. It is open question 1 in the report.

Measured pull: no app navigation in readable sources writes a map-valued entry label. This is the objectstack pointer 5913605784, re-read at objectstack 5f6b63a6f: in the files that declare navigation, the only object-valued labels are in translation bundles.

Pins

New file packages/layout/src/__tests__/NavigationRenderer.labelVerbatim-11201.test.tsx:

  • VERBATIM, 4 rows. A present label equal to its target's machine name (object account, view board, dashboard sales_overview) renders verbatim under en and under zh-CN. The console's convention resolvers are wired as UnifiedSidebar wires them, and none is called. Case and padding variants (ACCOUNT, SALES_OVERVIEW) stay as written. Control in the same tree: an absent label inherits the target's localized label in each locale, which shows the wiring is live.
  • MAP, 2 rows. Map-valued object, dashboard, group and action labels render the map's text. They never render [object Object], empty text or the target's label. Search finds them by that text.

Restated rows in NavigationRenderer.navLabelOwnership.test.tsx (they pinned the retired convention):

  • still resolves an un-customized object label through resolveObjectLabel is now renders a present object label equal to the machine name verbatim, resolveObjectLabel unasked (objectui#11201).
  • keeps the same guard on the dashboard branch is now renders both present dashboard labels verbatim, resolveDashboardLabel unasked (objectui#11201).
  • still lets an authored object label win over resolveObjectLabel: the assertion is unchanged. Its comment no longer names the guard.

Unchanged and green: NavigationRenderer.labelInheritsTarget-9868.test.tsx (absent labels inherit, and an explicit label equal to the machine name stays verbatim with echo resolvers), and the stage-1 pin useNavigationSync.standardEntryLocalizes-11201.test.tsx.

Ablation

Run from committed 54656bd7d with objectstack's scripts/ablation-replace.mjs. The layout tests import the renderer by relative path, and the root vitest config aliases @object-ui/layout to src, so no dist/ is in the path.

  • Leg A: restore the isCustomized arm. Over packages/layout/src/: 6 failed, 287 passed. The 6 are exactly the 4 new VERBATIM rows and the 2 restated rows. The MAP rows stay green.
  • Leg B: drop the map resolution, so every object label goes back through the keyed resolver. Over packages/layout/src/: 2 failed, 291 passed. The 2 are exactly the MAP rows.
  • Each leg: the anchor hit once and then zero times, and the blob changed. The restore was proven: blob equals HEAD, and git diff HEAD is empty.

Verification

All gates ran from the worktree root, on head 54656bd7d:

  • pnpm exec vitest run --reporter=verbose packages/layout/src/: 30 files, 293 passed.
  • Consumers outside layout (every test file that imports NavigationRenderer or resolveNavItemLabel, plus packages/app-shell/src/layout/, packages/app-shell/src/chrome/, the nav:menu render tests, the Studio and designer nav-label pins, packages/plugin-designer/ and packages/i18n/src/): 173 files, 1966 passed, 13 skipped.
  • Closure build turbo run build --filter=@object-ui/layout^... --filter=@object-ui/i18n^... --concurrency=2: 8 of 8 successful. Then type-check for @object-ui/layout and @object-ui/i18n: exit 0. tsc -p tsconfig.test.json --listFilesOnly lists both touched layout test files.
  • eslint on the 4 changed source and test files: 0 errors and 28 warnings, all pre-existing. Linting the base blobs gives NavigationRenderer.tsx 25 warnings against 23 at head (two as any casts went with the arm), and the other files the same counts.
  • pnpm exec vitest run scripts/__tests__/ (the whole root suite, because a changeset .md is added): 177 files passed and 2 skipped; 5371 tests passed and 2 skipped; lock VERDICT command-exit 0.
  • check-changeset-presence exit 0. check:control-bytes exit 0. check:new-line-citations reports 0 new citations, exit 0. markdown-test-inputs --audit exit 0. check-changeset-no-major exit 0. check:changeset-claims is report-only: it names 3 pending changesets that cite a touched file, and I read each paragraph; none is falsified by this diff.

Acceptance notes

  • Fork clause: not triggered. No objectui producer on main writes a machine-name label. The console sync effect passes no label. The Studio skeleton and inspector bind and the wizard's generatedEntryLabel were fixed by stage 1. Remaining label: d.name-shaped writes are the Studio rail's object list and a dashboard document seed, not nav entries.
  • Declared-type gap, measured, reported (not filed): @object-ui/types declares a nav entry's label as string (the type and the zod mirror), narrower than the spec's I18nLabel. safeValidateSchema, which objectui validate calls, refuses a map-valued nav label (navigation[0].label: expected string, received object). The spec's NavigationItemSchema accepts the same entry. The new map pin casts its fixtures for that reason, and says so.
  • The header comment of packages/app-shell/src/views/nav-menu-renderer.tsx still says its labels get "the same convention-based object / view / dashboard i18n resolution the sidebar gets". That is no longer true for any label. UnifiedSidebar and nav-menu-renderer still build the three unread resolvers. Both files are outside this claim's file surface; they belong with open question 2.
  • AppSchemaRenderer.tsx's resolveAreaLabel docblock quotes the old resolveObjectLabel prop doc ("enables convention-based i18n auto-resolution …"), which this PR rewrote. The docblock's point, injection over an i18n dependency, still stands.
  • resolveNavItemLabel still resolves objectui's keyed { key, defaultValue } reference through t, unchanged. The spec's nav label refuses that shape and objectui's type does not declare it. The new code tells it apart from a locale map by the two member names a map can never carry.

Generated by Claude Code

…ranslate-if-equal-to-name exception, and an inline locale-map label renders its text (objectui#11201, stage 2)

resolveNavItemLabel follows the spec's one order: the id-keyed bundle entry
(translateApp, upstream at /meta), then the present label (an inline locale
map's entry, else its text), then, for an absent label, the target's current
localized label. The isCustomized arm, which looked a present plain-string
label up through the object / view / dashboard resolvers when its text equalled
the target's machine name, is deleted with its comment. The three convention
resolvers are still accepted and no longer read.

A present map-valued label used to reach the keyed resolver and render empty.
It now reads through the spec's resolveI18nLabel, at its no-locale default
(the en entry, then default, then any), the floor the area switcher uses.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011p7ikEivgXefNDaE5S5Uec
@github-actions

github-actions Bot commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

changeset-claim-re-read

⚠️ 3 pending changeset(s) describe a file this change touches

Their bodies publish verbatim into the CHANGELOG at the next release, so this is a request to re-read them against your diff — addressed here because you are the one seat that can answer it without re-deriving anything.

⛔ Nothing here blocks, and nothing here is a verdict on your change. This gate exits 0, is not a required context, and judges name resolution, never meaning: it asked whether a pending body names a file you touched. "Is this sentence still true?" is the one question it will not answer, and the one you are being asked to answer.

.changeset/8871-page-node-refuses-breadcrumbs.md

  • names layout/src/NavigationRenderer.tsx → packages/layout/src/NavigationRenderer.tsx — edited by this change

    What was measured, on this branch's base 93127bd6f. Zero readers, with a point-access probe rather than a bare word: on that base \.breadcrumbs scores 0 tree-wide (exit 1) against \.breadcrumb\b's 12 files tree-wide (10 under packages/) as the lit control. At head the same two probes read 16 and 13 and \.breadcrumbs is exit 0 over 4 files — every hit one of this branch's own four files (this changeset, the refusal pin, layout.ts, zod/layout.zod.ts) quoting the probe string, and the pin's own exclusions put head back at exit 1. The base reading is the measurement; the head reading is this branch's echo of it. The bare word would have lied — it also names Sentry's own unrelated concept (app-shell/src/observability/sentry.ts) and appears in two comments listing UI surfaces (core/src/utils/record-title.ts, layout/src/NavigationRenderer.tsx), so a bare probe reports five readers that do not exist.

.changeset/objectchart-fieldoptionlabel-ref-5587.md

  • names useObjectLabel.ts → packages/i18n/src/useObjectLabel.ts — edited by this change

    Pinned by ObjectChart.fieldOptionLabelRefetch.test.tsx, which counts fetches across forced re-renders both outside and inside a provider. Reverting useObjectLabel.ts to its pre-objectui#5564 state turns the no-provider case red (2 fetches instead of 1, alongside React's "Maximum update depth exceeded"), so the removal is pinned to the fix that unlocked it rather than to a comment.

.changeset/objectpivottable-i18n-resolver-refs-5625.md

  • names useObjectLabel.ts → packages/i18n/src/useObjectLabel.ts — edited by this change

    Pinned by ObjectPivotTable.i18nResolverDeps.test.tsx, which counts derivations across forced re-renders both outside and inside a provider, checks that the two derived state maps settle, and asserts the language-switch re-derivation. Reverting useObjectLabel.ts to its pre-objectui#5585 state turns the no-provider case red (derivations in the dozens instead of 1), so the removal is pinned to the fix that unlocked it rather than to a comment.

Read the paragraph, not the line: both false halves of the objectui#8617 claim sat in one paragraph, and correcting either alone would have left it asserting the same wrong thing.

If a claim did go false, correct the body. That is precedented and prose-only, frontmatter untouched; check-changeset-overwrite.mjs will report the correction as its own case 2 ("correcting a declaration on purpose … legitimate"), which is the intended shape — one gate asks for the read, the other records the write.

Not covered, stated so nobody reads this as more: a born-false claim that spells no line address at all (objectui#9495 coordinated one by ORDINAL — "a grep finds that member first" — and deciding that means reading what the sentence means), a claim spelled as a symbol or a package rather than a backticked file name, and a file named ambiguously.

Compared the checked-out tree with 1563d3e10 (merge-base with origin/main): 4 file(s) changed outside .changeset/, read against 1867 pending declaration(s) that publish a body (2479 pending in total). · run

@github-actions

github-actions Bot commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 330 chunks) 3584.7 KB 3607.4 KB
Main entry chunk (gzip) 150.0 KB 350 KB
Entry file index-BTPKo68p.js —
Status PASS —

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.88KB 6.25KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.11KB 3.87KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.22KB 10.61KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.17KB 5.40KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.72KB 2.24KB
auth (SocialSignInButtons.js) 9.70KB 3.93KB
auth (UserMenu.js) 3.39KB 1.21KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.70KB 10.94KB
auth (createAuthenticatedFetch.js) 8.54KB 3.46KB
auth (index.js) 3.63KB 1.64KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 27.13KB 7.95KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 570.17KB 136.43KB
core (index.js) 10.00KB 3.96KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 229.96KB 63.80KB
fields (index.js) 261.11KB 66.26KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.24KB 2.27KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.40KB 12.91KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 34.49KB 9.23KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 40.95KB 11.48KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.86KB 5.00KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.52KB 2.26KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.33KB 3.07KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 16.01KB 3.93KB
plugin-calendar (index.js) 52.17KB 15.06KB
plugin-charts (index.js) 84.09KB 22.93KB
plugin-chatbot (index.js) 198.22KB 46.97KB
plugin-dashboard (index.js) 139.27KB 37.26KB
plugin-designer (index.js) 216.45KB 44.60KB
plugin-detail (index.js) 244.79KB 64.47KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 173.75KB 44.64KB
plugin-gantt (index.js) 173.03KB 43.07KB
plugin-grid (index.js) 231.61KB 63.61KB
plugin-kanban (index.js) 48.62KB 15.21KB
plugin-list (index.js) 116.63KB 28.97KB
plugin-map (index.js) 23.50KB 7.82KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 44.04KB 12.21KB
plugin-timeline (index.js) 33.05KB 9.67KB
plugin-tree (index.js) 11.20KB 3.89KB
plugin-view (index.js) 90.32KB 22.76KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.81KB 3.58KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 119.55KB 39.23KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.50KB 2.06KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.50KB 1.99KB
sdui-parser (codegen.js) 9.45KB 3.76KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 6.06KB 2.68KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.84KB 1.90KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 21.42KB 7.05KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 4.12KB 1.61KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 3.19KB 1.62KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.26KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 2.52KB 1.31KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 4.99KB 1.96KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 21.59KB 7.71KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 54656bd7df48f550ccb72b912e6f0276f5d04261
Local-runs: none

Inputs: card #11201 (body and all 12 comments; the director's ruling 5921167691 is the binding direction, claim 5921933038, dev report 5922299030), PR #11297 (body, 5-file list, net diff against main; the base 1563d3e10b is main at review time), and the head's check-runs with filter=latest. Context reads by contents at the head or base, not runs: NavigationRenderer.tsx, AppSchemaRenderer.tsx, layout package.json, types/src/app.ts, types/src/zod/app.zod.ts, cli/src/commands/validate.ts, previews/navItemLabel.ts, layout/UnifiedSidebar.tsx, views/nav-menu-renderer.tsx, react/src/index.ts, the six producer files and useNavigationSync.ts; and the spec's i18n.zod.ts, i18n-label-resolver.ts and i18n-resolver.ts from the objectstack checkout (the key lookahead landed 2026-09-21 in 5c5b67fc4, before the 17.5.0 objectui resolves).

Check-runs on the head: 43 — 40 success, 3 skipped, 0 failure, 0 in progress (polled 00:37Z to 00:45Z; the 8 test shards, Type Check, Lint and Spec Main Shape Gate all completed success). The gate verdicts are green.

① Derived judgments

  1. The deletion — right. resolveNavItemLabel is now three lines: separator, the absent arm (inheritedNavItemLabel, unchanged) and the present arm (presentNavItemLabel). The isCustomized closure, its comment and the (item as any).dashboardName reads are gone. Every delegate at the head: inheritedNavItemLabel keys on absence alone and asks targetLabel by target identity; resolveLabel compares only an i18next result to its own key; the spec's resolveI18nLabel compares locale tags. The one other text comparison in the file is filterNavigationItems, label against the search query. The four label reads in the component (group, action, item, search) and the filter's default all route through resolveNavItemLabel. No remaining code path compares a label to a name, and nothing added does: isKeyedLabel tests member names of the label object, never its text against a target.

  2. The map resolution against the ruling's words — satisfies the ruling as parameterised; the pin's literal "the locale's value" is met only as the resolver's locale default, and that gap is Q1, declared in the title, body, changeset and test header. (a) The gap the ruling cites (5913605784) is "would render empty in the runtime sidebar"; at the head a map renders text through the spec's resolveI18nLabel, the same import AppSchemaRenderer.resolveAreaLabel and navItemLabel.ts use. (b) "The way areas already do": layout's own resolveAreaLabel is the same call, resolveInlineI18nLabel(label, undefined) ?? '', with a docblock that names the no-locale default "a floor, not a ceiling" and a host prop as the only sanctioned route to more. But the two other "already do" sites resolve with a locale: UnifiedSidebar resolves AREA maps with the viewer's language (line 552), and the designer canvas takes a locale. translateApp does not flatten inline maps (it replaces by id only), so a map does reach the renderer, and in the console a zh-CN viewer sees the en entry of a map-valued ENTRY label under area labels in zh-CN. (c) Read literally, the ruling requires the viewer's locale inside resolveNavItemLabel; its own parameters ("no schema, type or public prop change", Clause-② no, level S, patch) exclude the route the package documents (a locale prop from the host), and the exported pure function's other call sites (CommandPalette, SearchResultsPage, nav-menu-renderer, navItemLabel.ts) would need a locale argument as well. One route the dev did not name: @object-ui/react, a layout dependency, re-exports useI18nContext from @object-ui/i18n, so a locale is reachable with no prop — at the cost of the coupling resolveAreaLabel documents as forbidden, and of a split (sidebar in the viewer's locale, palette and search at en) unless the signature changes. That is a design decision for the follow-up card, not for this PR. Judgment: the floor is the ceiling the ruling's parameters allow, so the PR does not fall short of the ruling; the ruling's pin and its parameters disagree with each other, and that disagreement is Q1.

  3. Map versus keyed — a shape test, sound for every map the spec accepts. isKeyedLabel is 'key' in label || 'defaultValue' in label. The spec's INLINE_LOCALE_KEY opens with a negative lookahead on exactly key and defaultValue, then default or a BCP-47 shape, and the emitted map type declares both members as never. So no spec-accepted map carries either member, every keyed reference carries one, and a map with a locale named key is not a map the spec accepts. It is not a text-against-name match: it reads neither the label's text nor a target. Key (case-differing) passes the regex and reads as a map, correctly. The keyed branch stays unreachable by contract (NavigationEntryItem.label?: string; zod z.string().optional()) and unchanged.

  4. The three resolver props and positional arguments — not a public prop change in the ruling's sense; acceptable under Clause-② no. NavigationRendererProps keeps the three members with identical optionality and types; resolveNavItemLabel keeps its six positions and types (the _objectResolver names appear in the d.ts but are not part of the call contract). The @deprecated tag is documentation: it adds, removes and retypes nothing, and it is true — the old docblock promised "convention-based i18n auto-resolution", which the ruling retired, so leaving it would have been a false doc. A documented no-op prop is a contract oddity; its retirement is Q2 and a signature change, so it is right that it is not in this PR.

  5. Comment-only edits — each true at the head. NavigationRenderer.tsx: the RETIRED (9c60144b5) note restates why the id-keyed resolvers never fired (label text against the node's id) — true; its "owned solely by /meta (rung 1)" is true of bundle translation, and the inline map is the authored label, not a translation. The t prop doc says a map never reaches t — true, presentNavItemLabel routes a map to the spec resolver. The action-arm comment says the map read lives in resolveNavItemLabel, not resolveLabel — true. The function docblock's three rungs match the code. useObjectLabel.ts (+5 −3, inside one JSDoc): "so is the guard itself" — true.

  6. The restated rows — each says what the ruling requires. "renders a present object label equal to the machine name verbatim, resolveObjectLabel unasked": resolver uncalled, account shown, Accounts absent — the ruled pin. "renders both present dashboard labels verbatim, resolveDashboardLabel unasked": sales_overview (the machine name) and Executive summary both verbatim, resolver uncalled. "still lets an authored object label win": assertion unchanged (Projects, resolver uncalled), comment now says "for any spelling". The file header is restated to "the convention resolvers unasked, including a label equal to the machine name".

  7. Ablation legs — consistent with the test files as read, not re-run. Leg A (arm restored, map read kept): the 2 sidebar VERBATIM rows, the 2 function rows and the 2 restated rows all assert uncalled resolvers and verbatim text; the third ownership row (Projects) and the MAP rows are untouched by the arm — 6, exactly those. Leg B (map back through resolveLabel): a map answers defaultValue || key, undefined, so the function MAP row and the sidebar MAP row fail and nothing else reads a map — 2, exactly those. The shards on the head are the gate.

  8. Fork clause — supported at main (1563d3e10b, this PR's base). NavigationSyncEffect calls syncPageCreated(appName, pageName) and syncDashboardCreated(appName, dashName) with no label, and newPageNavigationItem writes the key only when one is passed; buildAppSkeleton seeds { id, type, objectName }; AppNavCanvas births { id, type: 'object' } and passes navLabel(...) only to the selection callback; generatedEntryLabel writes a plural only when it spells neither the label nor the name; NavigationDesigner and the wizard write "New Group / New Link" placeholders. The label: d.name writes are the Studio rail's object list (setObjects) and the package-app header, not nav entries. No objectui producer writes a machine-name label; the fork was rightly not triggered.

  9. The new pins cover the ruling's three: verbatim under en and zh-CN (sidebar and function, with an inheriting control in the same tree proving the wiring live); the map rows (function, and sidebar with search); absent-label inheritance stays in labelInheritsTarget-9868 (its row "an explicit label EQUAL to the machine name stays verbatim" uses echo resolvers and is green before and after), and the stage-1 pin is in the consumer set.

② Semver level

  • The diff publishes, in @object-ui/layout: the behaviour of resolveNavItemLabel and NavigationRenderer (a present label verbatim; a map renders; three props inert), and one new import from @objectstack/spec/ui, already a dependency and already imported by AppSchemaRenderer; no type, member or signature changes. In @object-ui/i18n: a JSDoc edit, nothing published. The changeset '@object-ui/layout': patch matches, and @object-ui/i18n rightly carries none (Changeset Declaration, Bump Policy, Fixed Group Check and Claim Re-read are green). Its text states the visible effect and the one-line fix, and "not converted, no notice" — the ruling's own wording.
  • The PR body declares Clause-② no, line-initial, the fixed spelling, no arm. Against the built diff and the arms in clause2-line.mjs: nothing widens (the spec and the renderer's label input accepted a map before; it now renders instead of empty, which is conformance, not a new accepted input), and nothing narrows an accept set (every prop, argument and label shape accepted before is accepted now), so neither (widening) nor (narrowing) is due. The @deprecated tag and the renamed positional parameters change no exported type and do not move the line. The visible change for a stored machine-name label is the ruled fix, which the ruling itself classes patch with Clause-② no.

③ Boundary flags

  • Q1 (viewer's locale through a new prop) — not answerable on this card: the ruling fixed "no public prop change" and Clause-② no, and option A is a prop (or an argument on the exported function), Clause-② yes, minor. Escalation to one follow-up family card at ACCEPT is right. Q1 does not mean this PR fails the ruling (①.2). The follow-up should carry, beside the dev's A and B, the @object-ui/react re-export route (a locale with no prop, against the coupling resolveAreaLabel forbids) and the measured pull (zero map-valued entry labels in readable sources; a grep of the objectstack checkout finds label: { only in translation bundles).
  • Q2 (retire the three dead props) — a signature change outside the ruled parameters and the claim's surface; the same follow-up card. Right.
  • Finding 1 (safeValidateSchema refuses a map the spec accepts) — confirmed at the head: NavigationItemObject.label is z.string().optional() and NavigationEntryItem.label?: string, narrower than the spec's I18nLabel; validate.ts imports safeValidateSchema from @object-ui/types/zod. packages/types is outside this claim; the same follow-up card. Right.
  • Deviations — all three declared, none hidden: the title's "renders its text" is true at this head where the ruled wording would not be; the map pin asserts the resolver's default and says so in its header; the consumer-test narrowing is the dev's local run, and the 8 shards on the head are the gate.
  • The two stale comments. nav-menu-renderer.tsx:38-39 ("the same convention-based object / view / dashboard i18n resolution the sidebar gets") is false on main after merge; AppSchemaRenderer.tsx's resolveAreaLabel docblock quotes a prop doc this PR rewrote, but as a historical quote whose argument still holds. Neither had to be fixed inside this surface: the claim listed exactly one becoming-false comment (useObjectLabel.ts), nav-menu-renderer.tsx is app-shell, and Q2's follow-up rewrites both files. The follow-up card must name the nav-menu-renderer header explicitly, so the false sentence is not left behind when Q2 is triaged.
  • PR body — every sentence checked is true at this head, including "Fixes question(layout): does the spec's "present label ⇒ rendered verbatim" retire the nav renderer's convention that translates a PRESENT label equal to its target's name through the i18n bundle? #11201" (stage 2 is the ruling's last round, "no further decision card"), "@object-ui/layout has no i18n dependency by design" (none in package.json; the transitive re-export above is context, not a contradiction), "UnifiedSidebar already holds language" (line 163), the declared-type-gap sentences, and the pin and ablation descriptions. Its gate counts are the dev's local runs, superseded by the head's check-runs.

Implemented-by: claude/issue-11201-nav-label-verbatim
Reviewed-by: session_011p7ikEivgXefNDaE5S5Uec

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 1, 2026 00:51
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 1, 2026 00:51
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 1, 2026
Merged via the queue into main with commit 1ccb5ba Oct 1, 2026
45 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-11201-nav-label-verbatim branch October 1, 2026 01:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants