Skip to content
This repository was archived by the owner on Oct 26, 2026. It is now read-only.

fix(app-shell,components): resolve a page's inline locale label and description to the viewer's language (objectui#13488) - #13501

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-e99xcp
Oct 25, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-e99xcp

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Elon · project thread

  • Adds no product-specific widget, dialog, hook or string (see AGENTS.md › Product-neutral renderer)

Fixes #13488
Clause-②: no
Metadata-protocol: none

What changed

PageSchema in @objectstack/spec types a page's label and description as I18nLabel, so an inline locale map such as { 'zh-CN', en } is valid metadata, and the /meta read serves it unchanged. Two packages drew it raw on a page route, and React refused the object as a child (error #31), so the route was blank for every viewer:

  • @object-ui/app-shell, AppHeader's page branch. It handed the served label to pageLabel as its fallback, and pageLabel returns an unmatched fallback untouched, so the map became the phone bar's title. It now resolves through metadataText in the viewer's language (useObjectTranslation().language), the call the report branch has used since objectui#13470, and still passes through the pages.NAME.label bundle lookup. The humanized slug stays the fallback when no label resolves.
  • @object-ui/components, PageRenderer. The page's own implicit heading (schema.title ?? schema.label), the line under it (schema.description), and the description it hands a titled page:header through PageDescriptionContext were all drawn raw. Each now resolves through pickLocalized in the viewer's language, the resolver page:header already uses for its own title in the same package. With only the app bar fixed, an app, home or utility page with a map label stayed blank through this heading (measured, below), so the claim was widened to this file (claim amendment on App page route goes blank (React error #31) when a page's label is the spec's inline locale map (I18nLabel { zh-CN, en }) #13488).
  • Swept, same file, same call: AppHeader's dashboard branch drew a served dashboard label straight into the phone bar's title. DashboardSchema.label is I18nLabel too, so it is now metadataText(…) ?? dashboardLabel(…).

A plain-string label or description draws exactly as before: metadataText and pickLocalized both pass a string through, and an empty one still falls to the slug or hides the description. Studio's page previews (SourcePageEditor, PagePreview) render through the same PageRenderer, so they get the fix too. One changeset: @object-ui/app-shell and @object-ui/components patch.

Tests

New pins, each mounting the real component under a real I18nProvider, each leg in en and zh-CN reading different strings out of one map, with plain strings as the control:

  • packages/app-shell/src/layout/__tests__/AppHeader.pageDashboardTitleInlineLocale-13488.test.tsx: the AppHeader.reportCrumbInlineLocale-13470 harness with useObjectLabel real. Legs PAGE_LABEL_MAP_RESOLVES, DASHBOARD_LABEL_MAP_RESOLVES, two PLAIN_STRING_CONTROLs.
  • packages/components/src/__tests__/page-inline-locale-label-13488.test.tsx: an app page through the real SchemaRenderer and PageRenderer. Legs HEADING_MAP_RESOLVES, DESCRIPTION_MAP_RESOLVES, HEADER_DESCRIPTION_MAP_RESOLVES (titled page:header), PLAIN_STRING_CONTROL.

Readings, final head 2c82e03 unless stated:

  • The two new files: Test Files 2 passed (2), Tests 16 passed (16).
  • Every test file that names AppHeader, PageView, PageRenderer, SourcePageEditor, PagePreview or pageKindNode, plus every packages/components test that names PageRenderer, page:header, pageType, the page renderer module or an app/home/utility page node (212 files, found by git grep -lE over test files), with pnpm exec vitest run --maxWorkers=2: Test Files 212 passed (212), Tests 2161 passed (2161). Run at bf37518; the only later commit, 2c82e03, changes the type annotation of one new test's fixture, and both new files were re-run at 2c82e03 above.
  • turbo run type-check --filter=@object-ui/app-shell --filter=@object-ui/components --concurrency=2 --continue: 31 successful, 31 total. Both packages' type-check chain tsconfig.test.json, so the new test files are type-checked.
  • eslint on the five touched files: 0 errors. Every warning is on a line this PR does not add; the one on a changed line in page.tsx is the (schema as any).label read the old line already had.
  • node scripts/check-changeset-presence.mjs: pass (4 source files of 2 released packages, 1 changeset). node scripts/check-changeset-no-major.mjs: pass. pnpm check:new-line-citations: 0 new citations.

Narrowed, declared: the whole packages/app-shell/ and packages/components/ suites were not run locally; CI's sharded pnpm test runs them. The narrowed set's blind spot is a test that mounts these components through another import path without naming them.

Probe before the fix, on main 85b6a48, through the real SchemaRenderer and PageRenderer (a throwaway file, not committed): an app page with a map label, one with a map description, and one with a titled page:header and a map description all failed on Objects are not valid as a React child (found: object with keys {zh-CN, en}).

Ablation, one site at a time on the committed fix, through scripts/ablation-replace.mjs (anchor must hit once, blob must change, restore proven by blob equal to HEAD and an empty git diff HEAD), running the two new files each time:

  • page branch reverted (pageDef?.label || humanizeSlug(pageName)): 2 failed, 14 passed. Only PAGE_LABEL_MAP_RESOLVES fails, in both languages, on React error Task: Refactor Monorepo to Support Lazy-Loaded Plugins #31.
  • dashboard branch reverted (dashboardDef?.label || dashboardLabel(…)): 2 failed, 14 passed. Only DASHBOARD_LABEL_MAP_RESOLVES fails, on the same error.
  • PageRenderer heading reverted (schema.title ?? schema.label raw): 2 failed, 14 passed. Only HEADING_MAP_RESOLVES fails, on the same error.
  • PageRenderer description reverted (raw schema.description): 4 failed, 12 passed. DESCRIPTION_MAP_RESOLVES and HEADER_DESCRIPTION_MAP_RESOLVES fail, on the same error.

Acceptance notes

  • InterfaceListPage (an interface page's list surface) reads typeof page.label === 'string' ? page.label : page.name for its heading and its record drawer's title, and draws page.description only when it is a string. A page with a map label does not blank there, but it shows the page's machine name instead of its label, and a map description is not shown. Read from the code on 85b6a48, not measured; outside this claim's file surface. Handed to the seat.
  • AppHeader's view branch hands viewDef?.label || viewDef?.title to viewTabLabel, and a list view's label is I18nLabel in the spec as well. The object page's tab bar (ObjectView) hands the same label to the same function, so whether a map view label blanks a view route is a question about ObjectView, not this header alone. Read from the code, not measured; not changed here.
  • The page's phone-bar title still passes the resolved label through the pages.NAME.label bundle lookup, while the page's own heading draws the served label. That split predates this PR and is unchanged, as on the report route.

🤖 Generated with Claude Code

https://claude.ai/code/session_012oUFcAgU5BgTKe6fa7RxD8

…l in the app bar title (objectui#13488)

The page and dashboard branches of AppHeader handed a served `label` to the
phone bar's title unresolved. `PageSchema.label` and `DashboardSchema.label`
are `string | I18nLabel`, so an inline locale map reached JSX and React
refused it (error #31), blanking the route. Both now resolve through
`metadataText` in the viewer's language, as the report branch does since
objectui#13470.

Claude-Session: https://claude.ai/code/session_012oUFcAgU5BgTKe6fa7RxD8
Co-authored-by: Claude <noreply@anthropic.com>
… in its own heading (objectui#13488)

`PageRenderer` drew a page's `label` as its implicit h1 and its
`description` under it raw, and handed the raw description to a titled
`page:header`. Both are `I18nLabel` in `PageSchema`, so an inline locale
map reached JSX and React refused it (error #31). They now resolve through
`pickLocalized` in the viewer's language, as `page:header` resolves its own
title. One changeset covers this and the app bar fix.

Claude-Session: https://claude.ai/code/session_012oUFcAgU5BgTKe6fa7RxD8
Co-authored-by: Claude <noreply@anthropic.com>
The inline-locale page fixture arrives as data, so it is cast once to the
`SchemaRenderer` prop type instead of through `any`.

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

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 85 chunks) 3038.8 KB 3496.0 KB
Main entry chunk (gzip) 80.8 KB 350 KB
Entry file index-uycSeZcZ.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 (buildIdentity.js) 0.80KB 0.49KB
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 21.41KB 7.90KB
app-shell (runtime-config.js) 24.52KB 8.48KB
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) 45.59KB 12.03KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 20.67KB 6.12KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.72KB 2.24KB
auth (SocialSignInButtons.js) 9.74KB 3.94KB
auth (UserMenu.js) 3.39KB 1.21KB
auth (auth-gate-events.js) 1.62KB 0.83KB
auth (authStyles.js) 5.27KB 1.86KB
auth (createAuthClient.js) 42.84KB 11.66KB
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) 8.84KB 3.61KB
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.26KB 8.03KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.28KB 2.60KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (commentAuthorship.js) 1.39KB 0.78KB
collaboration (index.js) 1.93KB 0.85KB
collaboration (useCollaborationTranslation.js) 6.50KB 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) 615.24KB 150.64KB
core (index.js) 10.51KB 4.16KB
create-plugin (index.js) 27.92KB 9.50KB
data-objectstack (index.js) 257.46KB 72.08KB
fields (index.js) 296.40KB 75.98KB
i18n (LocalizationContext.js) 2.92KB 1.42KB
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.52KB 2.39KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.35KB 12.88KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 36.87KB 9.88KB
i18n (useSafeTranslation.js) 7.14KB 2.92KB
layout (index.js) 41.17KB 11.96KB
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) 15.35KB 5.51KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.82KB 2.38KB
permissions (ServerReadScope.js) 4.03KB 1.82KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.33KB 3.07KB
permissions (index.js) 0.99KB 0.43KB
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.91KB
plugin-calendar (index.js) 55.31KB 16.16KB
plugin-charts (index.js) 88.30KB 24.33KB
plugin-chatbot (core.js) 5.22KB 2.18KB
plugin-chatbot (index.js) 238.05KB 56.30KB
plugin-dashboard (index.js) 147.15KB 39.95KB
plugin-designer (index.js) 232.83KB 50.01KB
plugin-detail (index.js) 268.92KB 71.56KB
plugin-editor (index.js) 2.21KB 1.04KB
plugin-form (index.js) 185.46KB 48.48KB
plugin-gantt (index.js) 178.95KB 45.22KB
plugin-grid (index.js) 277.83KB 77.44KB
plugin-kanban (index.js) 64.27KB 19.81KB
plugin-list (index.js) 149.51KB 39.15KB
plugin-map (index.js) 27.54KB 9.10KB
plugin-markdown (index.js) 13.79KB 4.79KB
plugin-report (index.js) 46.42KB 13.06KB
plugin-timeline (index.js) 39.74KB 12.00KB
plugin-tree (index.js) 15.25KB 5.38KB
plugin-view (index.js) 93.37KB 24.00KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 12.07KB 3.68KB
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) 129.13KB 42.23KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.71KB 2.11KB
react (schema-input.js) 4.31KB 2.07KB
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.07KB 1.30KB
sdui-parser (index.js) 7.30KB 3.12KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 25.28KB 7.81KB
sdui-parser (provenance.js) 3.84KB 1.90KB
sdui-parser (types.js) 0.29KB 0.24KB
sdui-parser (validate.js) 23.87KB 7.83KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 4.12KB 1.61KB
types (authoring-nodes.js) 0.20KB 0.19KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (cloud.js) 0.20KB 0.18KB
types (complex.js) 4.44KB 2.07KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (dashboard-widget-layout.js) 2.06KB 0.96KB
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) 1.13KB 0.65KB
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) 5.78KB 2.70KB
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 (node-slots.js) 7.18KB 2.34KB
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 (product-blocks.js) 0.20KB 0.19KB
types (record-approval-decision.js) 0.20KB 0.20KB
types (record-components.js) 0.20KB 0.19KB
types (record-panels.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) 19.93KB 7.26KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.48KB 3.50KB
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

席位核对 — ACCEPT · PR #13501 @ 2c82e0385dff6b4bd590d98afb16fd426156aac3 · domain:ui#4 · 2026-10-25T04:40Z

Reviewer of record: session_01XWGTsmqrtrt1Zqh1ioWqsT (seat 4). Implemented-by: session_012oUFcAgU5BgTKe6fa7RxD8 on claude/project-thread-e99xcp. Card objectui#13488 (p1), claim 6393173731, amendment 6393215883, report 6393426684.

  1. Shape: draft, base main, Fixes #13488. Five files (+339 −8): AppHeader.tsx (page and dashboard branches), components/renderers/layout/page.tsx, two new test files, one changeset. All inside the claim as amended.
  2. Card against diff:
    • AppHeader: the page branch's fallback becomes metadataText(pageDef?.label, language) ?? humanizeSlug(pageName), and the dashboard branch's title becomes metadataText(dashboardDef?.label, language) ?? dashboardLabel(…). metadataText (hooks/useNavTargetLabel.ts:74) resolves an inline map in the viewer's language and returns undefined for an empty string, so the old || fall-through to the slug is kept.
    • PageRenderer: pickLocalized now resolves the title (title ?? label) and description, the resolver page:header already uses. That covers the implicit h1, the line under it, and the description handed to page:header through PageDescriptionContext. useObjectTranslation() sits with the component's other top-level hooks, before any return. A plain string passes through unchanged.
    • Pins: a map renders the viewer's-locale string on all four sites, and a plain string is unchanged. The dev reverted each site in turn through ablation-replace.mjs; each revert turned only its own legs red, on React Task: Refactor Monorepo to Support Lazy-Loaded Plugins #31.
  3. Gates on this head: 33 check-runs success and 3 skipped (coverage jobs, dependabot). mergeable_state: clean. Locally: the two new files (16/16), 212 related test files (2,161 tests), type-check 31/31, and eslint (0 errors).
  4. Accepted deviations: none beyond the amendment.
  5. Out of scope: two read-only findings, carried by the seat and filed separately. InterfaceListPage shows a page's machine name for a map label and drops a map description. AppHeader's view branch and ObjectView's tab bar pass a ListView label raw into viewTabLabel (not measured).
  6. Landing: not governed. Changeset: @object-ui/app-shell + @object-ui/components patch, Clause-② no / Metadata-protocol none; it matches the diff. The maintainer's standing landing word was given in this seat's project thread at 2026-10-25T01:48Z, so the seat lands it.

Generated by Claude Code

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

App page route goes blank (React error #31) when a page's label is the spec's inline locale map (I18nLabel { zh-CN, en })

2 participants