Skip to content

fix(app-shell): Studio's field inspector says when a translation overrides the label being edited (objectui#11782) - #11859

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-11782-label-translation-hint
Oct 8, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-11782-label-translation-hint

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #11782

Clause-②: no

What changes

Studio's field inspector (ObjectFieldInspector, Basic section) now shows one muted line under the Label input when a translation overrides the label being edited. For the showcase's showcase_account.tax_id in an en session it reads:

Shown as “Tax ID” in en: the app’s translation bundle overrides this label.

In zh: 在 zh-CN 中显示为“税号”:应用的翻译包覆盖了此显示名。

  • The input is unchanged. It still shows and commits the field's source label. This is the card's option A. Option B (swapping the title and the value) is not in this PR.
  • How translations resolve is unchanged. Nothing in packages/i18n/** is touched.
  • Files: inspectors/ObjectFieldInspector.tsx (the hint line under field-label-input, plus one module-private component, LabelTranslationHint), two new rows designer.field.labelTranslated (en and zh) in metadata-admin/i18n.ts, the test beside the inspector, and a patch changeset. StudioDesignSurface.tsx is not touched.
  • Public surface: no export, prop, type member or language-pack key. The rows go into the designer's own string tables (ENGINE_STRINGS_EN / ENGINE_STRINGS_ZH, typed as plain string-to-string records).

Mechanism (the dispatch's Zone 2 hypotheses, measured)

  1. The resolver. The Data pillar's grid gets its column headers from ObjectGrid's resolveFieldLabel, which is useSafeFieldLabel().fieldLabel (re-exported through @object-ui/react). Its form canvas gets field labels from ObjectFormDesigner's fieldLabelOf, which is useSafeFieldLabel().fieldLabel from @object-ui/i18n. Both are useObjectLabel().fieldLabel(object, field, fallback) in packages/i18n/src/useObjectLabel.ts. That function reads NS.fields.OBJECT.FIELD from the bundle the console's loadLanguage installs from /api/v1/i18n/translations/LANG. The showcase's bundle has objects.showcase_account.fields.tax_id.label: 'Tax ID' (en) and '税号' (zh). The hint calls the same function, so the hint and the canvas cannot disagree.
  2. Is there a translation? The hint passes '' as the fallback. The resolver returns the fallback exactly when no translation exists. So an empty answer means nothing overrides the source label, whatever the input currently holds.
  3. Which language. Hypothesis 3 is falsified as stated: the designer's locale prop and the canvas's language are not the same value. The locale prop (useMetadataLocale) picks one of the designer's two string tables, and it is en-US for every language that is not zh. The resolver reads i18n.language from the i18next instance. The hint names the resolver's language and writes its sentence from the designer's table. A pin checks this: a ja session with the en-US designer table names ja.
  4. When it shows. Only when the translated label differs from the input's current value. With no translation, nothing shows. When the translation equals the label being edited, nothing shows. If the author types the translated text into the input, the line goes away. If they type anything else, it comes back.
  5. No link to the translation. Studio has no Translations pillar (PILLARS / OVERFLOW_PILLARS in StudioDesignSurface.tsx; the console's /studio routes are :packageId and :packageId/:tab only). The translation editor is the metadata-admin resource page, and its base path depends on the host. Finding which translation item carries the key would need a server read that the inspector does not make. A link would need a new prop on the field-inspector mount in StudioDesignSurface.tsx, which objectui#11781 holds and which is off this claim's surface, or a hard-coded route. So the line names the bundle in words instead of linking to it.

Tests

New: inspectors/ObjectFieldInspector.labelTranslation-11782.test.tsx, 7 tests. Each one renders the inspector inside a real I18nProvider with an app bundle in the runtime shape transformSpecTranslations produces.

  • The hint names the translated label and the language (en). The input still holds the source label.
  • The hint names the resolver's language (ja), not the collapsed designer locale.
  • In a zh session the hint reads from the zh table.
  • Control: a field the bundle does not translate shows no hint. A sibling field in the same bundle is translated, so the namespace is found and the absence is about this field alone.
  • Control: a translation equal to the source label shows no hint.
  • The comparison uses the input's current value: rerendering with the translated text hides the hint, and rerendering with another value shows it again.
  • Control: typing commits { type: 'text', label: 'Employer ID' } exactly as before, and the translation never enters the draft.

Ablation (node scripts/ablation-replace.mjs from ../objectstack, wrapping the run). The anchor was the hint's guard if (!translated || translated === sourceLabel) return null;, replaced by return null;. The anchor went from 1 hit to 0 and the replacement from 0 to 1. The blob went from 432eb3a6763b to bc44e7122d62, and the marker count on disk was 1. The run gave 4 failed / 3 passed: the four hint pins went red, and the three controls stayed green. The restore set the blob back to the HEAD blob 432eb3a6763b, git diff HEAD was empty, and git status was clean.

Every command below ran from the worktree root on the final head bc2bf4c (after merging origin/main 5aa7f55). The heavy runs went through os-verify-lock.sh.

  • pnpm --filter @object-ui/app-shell^... run build: exit 0 (run at 2ec68bc). After the merge, pnpm --filter @object-ui/data-objectstack run build (the merge changed that package): exit 0.
  • pnpm --filter @object-ui/app-shell type-check (it echoes tsc --noEmit && tsc -p tsconfig.test.json): exit 0. tsc -p tsconfig.test.json --listFilesOnly lists the new test file, and ObjectFieldInspector.test.tsx as a control.
  • pnpm exec vitest run --maxWorkers=2 on 211 files: 211 passed, 2707 tests passed, 1 skipped, exit 0. The files are the whole metadata-admin/inspectors/ directory, every app-shell test that imports the metadata-admin string table, the DataPillar* and Studio tests that mount the field inspector, apps/console's preview-samples-designer-options test, and the test files the merge brought in.
  • Declared narrowing. I started pnpm exec vitest run packages/app-shell/ (1085 test files) and stopped it myself after 10m33s under the lock, with no result read, because another seat was queued behind it. NOT MEASURED locally. The full package run is CI's.
  • pnpm exec eslint on the three touched source and test files: exit 0, 0 errors. All 11 warnings are existing no-explicit-any on code this diff does not add to.
  • check:control-bytes, check:test-path-roots, check:changeset-claims, check:pending-changeset-literals, check:i18n-keys, check:i18n-drift, check:i18n-dead-keys, check:i18n-designer-parity, check:new-line-citations, and node scripts/check-changeset-presence.mjs: all exit 0. Parity: "Every en row has a zh row, and every shared row carries the same placeholders." The new key is not in the dead-keys candidates.

Real-browser reading

I read this in the console's preview gallery (apps/console, vite --port 5297, /preview-gallery.html?only=object), not in Studio against the showcase backend. The sibling ../objectstack checkout has no install or build, and building the showcase backend on this shared box was out of proportion for a p3 hint. So the showcase showcase_account.tax_id Studio reading is NOT MEASURED.

The run used Chromium (/opt/pw-browsers/chromium) at 1440×900 against the gallery's sales_order sample. The bundle app.fields.sales_order.amount = 'Order Total' was installed with addResourceBundle on the provider's own i18next instance, the same call I18nProvider makes for a bundle loadLanguage fetched.

step Label input hint
before: no app bundle Amount none
after: bundle installed Amount Shown as “Order Total” in en: the app’s translation bundle overrides this label.
control: input set to Order Total Order Total none
input set to Amount (USD) Amount (USD) shown again
control: untranslated field Order Name Order Name none

With &locale=zh-CN (the zh designer table over an en i18next session), the hint read 在 en 中显示为“Order Total”:应用的翻译包覆盖了此显示名。. That is the Zone 2 hypothesis 3 split, seen in a browser. No page errors.

Acceptance notes (seen, not filed)

  • The inspector's header title and its Remove button still name the source label (def.label). This PR only touches the Label input, which is what the claim covers. Carrier: none.
  • InspectorTextField has no description slot, so the hint is not tied to the input through aria-describedby. Adding one would mean editing inspectors/_shared.tsx, which is off this claim's surface. Carrier: none.
  • In the preview gallery, react-i18next's module-level getI18n() instance was not the instance the tree renders with. A bundle added there never reached the hint, while one added to the I18nextProvider's i18n prop did. My unverified guess is that StrictMode runs the provider's useMemo twice, and the gallery's inline config makes each run call createI18n. This is a dev harness only. Carrier: none.

Generated by Claude Code

claude added 3 commits October 8, 2026 01:38
…s the label being edited

Studio's field inspector edits a field's source label, while the Data
pillar's grid headers and form canvas show the label resolved through
@object-ui/i18n's useSafeFieldLabel().fieldLabel, where a translation for the
active language wins. A muted line under the Label input now names the label
the field is shown with and the language, read through that same resolver,
whenever it differs from the input's current value.

Claude-Session: https://claude.ai/code/session_01CGZy1BGCjdN5cXqL9cnvB8
Co-authored-by: Claude <noreply@anthropic.com>
…bel-translation-hint

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

github-actions Bot commented Oct 8, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 336 chunks) 3535.1 KB 3551.8 KB
Main entry chunk (gzip) 159.9 KB 350 KB
Entry file index-CN48Xwrl.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) 17.82KB 6.58KB
app-shell (runtime-config.js) 22.59KB 7.89KB
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.15KB 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.11KB 7.97KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.28KB 2.60KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
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) 582.41KB 140.21KB
core (index.js) 10.00KB 3.96KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 235.31KB 65.44KB
fields (index.js) 266.88KB 67.46KB
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) 38.37KB 10.31KB
i18n (useSafeTranslation.js) 7.14KB 2.92KB
layout (index.js) 41.50KB 11.82KB
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.04KB 3.92KB
plugin-calendar (index.js) 53.39KB 15.52KB
plugin-charts (index.js) 84.71KB 23.25KB
plugin-chatbot (index.js) 199.63KB 47.46KB
plugin-dashboard (index.js) 144.82KB 39.17KB
plugin-designer (index.js) 231.46KB 48.87KB
plugin-detail (index.js) 248.57KB 65.47KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 176.62KB 45.75KB
plugin-gantt (index.js) 179.17KB 45.07KB
plugin-grid (index.js) 239.75KB 65.92KB
plugin-kanban (index.js) 52.17KB 16.37KB
plugin-list (index.js) 117.42KB 29.32KB
plugin-map (index.js) 25.60KB 8.62KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 44.12KB 12.29KB
plugin-timeline (index.js) 39.10KB 11.81KB
plugin-tree (index.js) 15.07KB 5.33KB
plugin-view (index.js) 91.07KB 22.93KB
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) 120.63KB 39.56KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.50KB 2.06KB
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.08KB 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.80KB
sdui-parser (provenance.js) 3.84KB 1.90KB
sdui-parser (types.js) 0.28KB 0.23KB
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 (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) 19.93KB 7.25KB
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

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

studio(data): the field inspector shows the source label while canvas and grid show the translated label, with no hint that a translation overrides it

2 participants