Skip to content

fix(app-shell): the translation preview draws the groups of the schema the designer validates against (objectui#11765) - #11767

Merged
objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-11765-translation-preview-categories
Oct 7, 2026
Merged

objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-11765-translation-preview-categories

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #11765

Clause-②: no

The translation designer's preview now draws one card per group of the schema the designer validates a bundle against, in that schema's order, and divides coverage by that count. A spec-valid bundle of only pages, picklists, datasets, flows or settingsCommon no longer reads "This bundle is empty", and a spec-valid bundle can reach full coverage. A key the schema refuses (validationMessages, per-app settings) is neither drawn nor counted.

What changed

  • TranslationPreview: the file-local CATEGORIES table is built from CATEGORY_DEFS, one row per group of the per-app TranslationDataSchema, in the schema's declaration order: objects, picklists, apps, messages, globalActions, dashboards, datasets, pages, flows, metadataForms, settingsCommon. The validationMessages and settings rows are gone. The coverage denominator is still CATEGORIES.length, which is now the schema's group count.
  • CATEGORY_DEFS is typed with satisfies Record over keyof (typeof TranslationDataSchema)['shape'] (a type-only import), so it is exhaustive in both directions at compile time.
  • The nested key-count row reads the singular for one key. engine.translationPreview.keyCount is split into keyCountOne and keyCountOther, the One/Other convention this table already uses (engine.datasetPreview.measureOne, engine.screenPreview.hiddenOne).
  • metadata-admin/i18n.ts, both ENGINE_STRINGS_EN and ENGINE_STRINGS_ZH: five heading rows added (Picklists / 选项列表, Datasets / 数据集, Pages / 页面, Flows / 流程, Settings UI / 设置界面); the category.validationMessages and category.settings rows removed; keyCount replaced by keyCountOne + keyCountOther. The removed rows had no other reader: git grep at HEAD finds neither key anywhere, against 4 files that still name the kept category.objects row.
  • The module docblock says which schema the categories follow, that a refused key is neither drawn nor counted, and how messages (flat), the nested groups and settingsCommon are read.
  • Tests beside them: the new pin file TranslationPreview.categories-11765.test.tsx; adminPreviews.i18n-10862-s2.test.tsx lists the eleven headings, reads keyCountOne and the 2/11 (18%) coverage; TranslationPreview.globalActions-11755.test.tsx reads keyCountOther.
  • .changeset/11765-translation-preview-categories.md, patch on @object-ui/app-shell.
  • All six files are on the amended claim surface (comment 6032111854). No export, prop, type member or language-pack key is added: CATEGORY_DEFS, CategoryDef and both dictionaries are module-local, and the package entry re-exports neither TranslationPreview nor the dictionary module. Measured on the emitted declarations as well: tsc --declaration --emitDeclarationOnly over the package at HEAD emits TranslationPreview.d.ts with only the unchanged TranslationPreview signature, and none of the 501 emitted declaration files names CATEGORY_DEFS, keyCountOne or a new heading row. So Clause-②: no holds.

Zone 2 hypotheses, measured (objectui d50f724, installed @objectstack/spec 17.7.0)

  1. Which schema the designer applies: the per-app one. clientValidation.ts's LOADERS.translation binds TranslationItemSchema. Its shape keys are the eleven TranslationDataSchema groups plus locale, name, label and the seven envelope keys. Only PlatformTranslationDataSchema declares settings, so settings goes. TranslationDataSchema refuses validationMessages and settings with unrecognized_keys, and so does the designer; the pin file asserts both refusals through validateMetadataDraft('translation', …) before it mounts them.
  2. Flat or nested. The probe TranslationDataSchema.shape[group].safeParse({ probe: 'x' }) succeeds for messages alone, the one record of strings. picklists, datasets, pages and flows are records of strict nodes and read nested, like objects. settingsCommon is one strict object, not a record; its only member today is sourceLabels (the five settings source-badge labels). Its card reads it through the same nested branch: the card counts and samples its members (sourceLabels, with its inner key count). No special branch, because it needs none to read right, it stays right if the spec adds a member beside sourceLabels, and the count means what it means on every other card. A settingsCommon: { sourceLabels: {} } counts 1 and reads {0 keys}, as an empty object node does on any nested card.
  3. Derive or pin: pinned, two ways. The schema's shape can supply the keys and their order, but not a group's icon or its heading row in two languages. A list derived at runtime would draw a group the spec adds with an echoed key for a heading, or would need a renderer-side fallback, which is the tolerance AGENTS.md #0.1 rules out. So the rows stay hand-written, and drift is made loud instead. satisfies Record fails type-check when the spec adds a group without a row here, or when a row names a key the spec does not declare (ablations A and C below show both). The pin file compares the drawn headings, their order and the denominator with Object.keys(TranslationDataSchema.shape) at runtime.
  4. Rows in both dictionaries. pnpm check:i18n-designer-parity, the parity gate over these two tables, is green. The zh case of adminPreviews.i18n-10862-s2.test.tsx reads each new heading as a real zh row (not the echoed key, not the en row).
  5. The plural. Folded in, as the card allows. With the five new nested groups, a node of one key (home: { label }) is the common case.

Measured before the change

The committed pin file was run against BASE's TranslationPreview.tsx and i18n.ts. Both were checked out from d50f724 into the worktree under a trap, then restored from HEAD. The restore was proven by blob == HEAD for both files and an empty git diff HEAD. Result: Tests 15 failed | 2 passed (17).

  • A bundle of only picklists, datasets, pages, flows or settingsCommon rendered the empty-bundle message, where the pin expects none. Each bundle is spec-valid, and validateMetadataDraft accepts each one. That is the card's reach, reproduced for all five groups.
  • A bundle of only messages or globalActions read 1/8, not 1/11. The full bundle never reached 11/11 (100%). The bundle with the refused groups drew 8 headings, not 11.
  • The one-key nodes of objects, apps, dashboards and metadataForms read {1 keys}.
  • Green: the fixture-coverage test and the messages control.

Pins: TranslationPreview.categories-11765.test.tsx (17 tests)

Every spec-valid fixture is parsed by the installed TranslationDataSchema and passed through the designer's own validateMetadataDraft('translation', …) (asserted { ok: true, issues: [] }) before it is mounted. Expected groups, order and denominator are read from TranslationDataSchema; headings and count rows are read back through t and tFormat.

  1. Every group the spec declares has a fixture in the file.
  2. One card per schema group, in schema order; the denominator is the schema's group count.
  3. to 13. One test per group, the five new ones included: a bundle of only that group reads non-empty, shows the group's card with its sample row, and reads 1/11.
  4. A bundle carrying every group reaches 11/11 (100%), and no card reads empty.
  5. Refused groups: validationMessages and settings are refused by the spec and by the designer, are not drawn, and are not counted.
  6. A nested node of one key reads keyCountOne, of two keys keyCountOther.
  7. Control: the messages card renders "Bienvenue" as before.

Ablations

All on the committed fix 7da7729, through objectstack's scripts/ablation-replace.mjs (WRAP mode: it mutates, proves the anchor count and the blob moved, runs the command, restores, and proves blob == HEAD and an empty git diff HEAD). The test imports ./TranslationPreview relatively, so it is source-resolved and needs no build.

Each count below was predicted before its run, and each matched.

  • A: validationMessages back in the table, inserted ahead of the dashboards row. The anchor went x1 to x0 and the blob 06de276d84d6 to 81562e4424ae. Result: Tests 14 failed | 3 passed (17). Every pin that reads the denominator or the headings went red: 1/11 (9%) was not found, 11/11 (100%) was not found, and 12 headings were drawn for 11 groups. The fixture-coverage test, the plural pin and the control stayed green. On the same mutation, tsc --noEmit exited 2 with TS2353: Object literal may only specify known properties, and 'validationMessages' does not exist in type 'Record... over the eleven group keys. Restored: blob == HEAD and git diff HEAD empty.
  • C: the pages row deleted. The anchor went x1 to x0 and the blob 06de276d84d6 to 9da6d764abdf. Result: Tests 15 failed | 2 passed (17). As on BASE, the pages-only test rendered the empty-bundle message. tsc --noEmit exited 2 with TS1360 ... does not satisfy the expected type and Property 'pages' is missing. Together, A and C show that satisfies holds in both directions. Restored the same way.
  • D: the singular row unread. The head n === 1 ? 'engine.translationPreview.keyCountOne' : was deleted, and the blob went 06de276d84d6 to d146e8c54a23. Result: Tests 7 failed | 10 passed (17): the six groups whose fixture node has one key, plus the plural pin. Restored the same way.
  • The first attempt at A and D was refused by the tool as a no-op, because the anchor overlapped the replacement, so the counts could not move. The tool restored the file (blob == HEAD), and both ablations were re-run with non-overlapping anchors. The results above come from the re-run.

Gates (run from the worktree root)

All on HEAD 7da7729, the only commit. Heavy runs went through objectstack's os-verify-lock.sh. Wall-clock figures are from a shared box.

command exit verdict line
pnpm exec vitest run packages/app-shell/ 0 Test Files 1056 passed, 1 skipped (1057), Tests 10338 passed, 9 skipped (10347)
pnpm exec vitest run --maxWorkers=2 on the pin, the objectui#11755 pin, adminPreviews.i18n-10862-s2, emptyCollection-8526, clientValidation.optOuts, StudioDesignSurface.standardNavEntries-11201 0 Test Files 6 passed (6), Tests 121 passed (121)
pnpm --workspace-concurrency=2 --filter '@object-ui/app-shell^...' build 0 dependency closure built for the type-check
pnpm --filter @object-ui/app-shell type-check 0 tsc --noEmit && tsc -p tsconfig.test.json; --listFilesOnly on the test project lists the new pin
pnpm exec eslint --format json on the five touched .ts / .tsx files 0 5 files, 0 errors, 0 warnings
pnpm check:i18n-designer-parity 0 Every en row has a zh row, and every shared row carries the same placeholders.
pnpm check:i18n-keys 0 Every in-scope call-site key resolves against the en pack (3297 keys), …
pnpm check:i18n-drift 0 No designer-table en value changed in this range. (designer table: 7 keys added, 3 removed)
pnpm check:i18n-dead-keys (a report) 0 no engine.translationPreview.* candidate; the designer leg is live (it lists other engine.* keys)
pnpm check:control-bytes 0 check-control-bytes: OK
pnpm check:test-path-roots 0 check-test-path-roots: OK
pnpm check:changeset-claims 0 No pending changeset names a file this change touches.
pnpm check:pending-changeset-literals 0 No test source names a pending changeset.
pnpm check:new-line-citations 0 0 new citation(s), enforcement report-only
pnpm check:spec-symbols 0 none renders an instruction its anchor card can no longer receive.
node scripts/check-changeset-presence.mjs 0 5 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)
node scripts/check-changeset-no-major.mjs, check-changeset-overwrite.mjs, check-changeset-fixed.mjs 0 each OK

Lint is narrowed to the touched files, and the narrowing rests on three facts. First, the population is eslint's own result set for the five files: the JSON output counts 5, and none is ignored. Second, eslint.config.js enables no type-aware linting: it has no projectService and no parserOptions.project, and a control grep on rules in the same file hits 13 times. Third, no rule under eslint-rules/ reads another file from disk. So this diff cannot move an untouched file's verdict. The repo-wide pnpm lint is CI's.

Not run locally:

  • check:readme-exports: no export changed, which the declaration emit above measures.
  • The apps/console project: no console file is touched.

The full farm is CI's.

Acceptance notes

  • nodeLabel stays globalActions-only. The four new record groups (picklists, datasets, pages, flows) declare label on their nodes but read their key count, as objects, apps, dashboards and metadataForms already do. This is objectui#11755's named gap, now over four more groups. Not filed.
  • engine.datasourcePreview.keyCount, a different preview's row in the same table, also reads {1 keys} for a single key. Polish; not filed.
  • The preview reads three keys that the designer's schema refuses, through d.data ?? d, d.language and d.description. Measured: TranslationItemSchema answers unrecognized_keys on data, language and description. The console preview gallery's translation sample (apps/console/src/preview-samples.ts) is written in that refused record form, and preview-samples-spec-valid.test.ts already records that row as a mapping mismatch to settle. Outside this claim's surface; not touched and not filed.

Session: https://claude.ai/code/session_01CGZy1BGCjdN5cXqL9cnvB8


Generated by Claude Code

…a the designer validates against (objectui#11765)

The preview's CATEGORIES table had drifted from the per-app
TranslationDataSchema, the groups validateMetadataDraft('translation', ...)
judges a draft by (TranslationItemSchema). It omitted picklists, datasets,
pages, flows and settingsCommon, so a spec-valid bundle of only one of them
read as empty, and it listed the refused validationMessages and settings,
which both counted toward the coverage denominator.

The table is now one row per schema group, in the schema's order, typed
with satisfies against the schema's shape keys so a group added or dropped
upstream fails type-check. Coverage divides by that count. The headings of
the five new groups are rows of the designer's own string table (en and
zh); the two dropped rows are removed. The nested key-count row reads the
singular for one key (keyCountOne / keyCountOther).

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

github-actions Bot commented Oct 7, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 336 chunks) 3515.2 KB 3551.8 KB
Main entry chunk (gzip) 156.3 KB 350 KB
Entry file index-Bfatc6uR.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) 578.94KB 139.21KB
core (index.js) 10.00KB 3.96KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 235.41KB 65.46KB
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.26KB 23.05KB
plugin-chatbot (index.js) 199.63KB 47.46KB
plugin-dashboard (index.js) 144.22KB 38.99KB
plugin-designer (index.js) 231.46KB 48.87KB
plugin-detail (index.js) 247.73KB 65.20KB
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) 238.51KB 65.53KB
plugin-kanban (index.js) 52.17KB 16.37KB
plugin-list (index.js) 116.85KB 29.12KB
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) 90.64KB 22.85KB
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

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 7, 2026 07:52
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 7, 2026 07:52
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 7, 2026
Merged via the queue into main with commit 179f6fe Oct 7, 2026
45 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-11765-translation-preview-categories branch October 7, 2026 08:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants