Skip to content

fix(app-shell): three designer inspectors read their words in the designer's locale (objectui#10586) - #10651

Merged
objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/issue-10586-inspector-strings-i18n
Sep 25, 2026
Merged

objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/issue-10586-inspector-strings-i18n

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

Part of #10586
Refs #10586 — PR 1 of 2. The card stays open: PR 2 carries HookDefaultInspector.tsx and ActionDefaultInspector.tsx, which seat 2's live claim on objectui#10585 edits today.

Clause-②: no

The English literals of three designer inspectors move behind the designer catalogue's t(), each with an en and a zh row, and en renders byte-identical. No schema, export or accept set moves.

What changes

file locale channel catalogue rows added (en + zh) existing rows reused
inspectors/DatasetDefaultInspector.tsx the host's locale prop (MetadataDefaultInspectorProps.locale), the channel ObjectDefaultInspector and ReportDefaultInspector read engine.inspector.dataset.* (88) engine.form.add, engine.form.searchFields
inspectors/FlowReferenceField.tsx (ReferenceCombobox, RecordLookupCell, connectorsToOptions) useMetadataLocale(), already read there for the objectui#10448 flag engine.inspector.reference.* (10) none
inspectors/ConditionBuilder.tsx useMetadataLocale(), through the existing tLocal engine.inspector.condition.* (15) none
metadata-admin/i18n.ts the same rows in ENGINE_STRINGS_EN and ENGINE_STRINGS_ZH; pnpm check:i18n-drift reads "0 en value(s) changed (113 key(s) added, 0 removed)"
  • Sentences that carry a value use {token} slots. tFormat fills plain values. A file-local withSlots fills React nodes (a code span, the emphasised tab name) with the split-and-interleave that ResourceEditPage already uses for engine.edit.readOnlyBanner. The lookup mechanism is the existing one.
  • Plurals: …One / …Other row pairs, picked by count === 1, which is the rule the en code applied. zh carries its single form in both rows.
  • Option labels are keyed by the stored value (KeyedOption) and resolved at render, so stored values do not move. Deliberately left literal: ≥ / ≤ (symbols), currency codes with their signs, decimal digits.
  • connectorsToOptions(connectors, locale?): the optional locale is for the · declarative annotation. Without it the answer is en, so FlowReferenceField.connector.test.ts is unchanged. useConnectorListOptions keys its effect on the locale string, a primitive (AGENTS.md rule 10). DatasetDefaultInspector's tr is a plain function, and nothing keys on its identity.
  • zh wording follows the catalogue's own rows: 数据集 / 维度 / 度量 / 聚合方式 / 显示名 / 货币 / 查找关系. ConditionBuilder's joins 且 / 或 follow the packs' filterBuilder.and / filterBuilder.or, and its 搭建器 matches engine.condition.advancedHint.

Premise check: the dispatch's mechanism hypotheses, measured

  • H1 (floor census at e0f8202). The floor regex reproduces 32 literals / 0 t() for the dataset inspector and 5 / 1 for FlowReferenceField. For ConditionBuilder my spelling of the regex reads 3 literals, because the two join text nodes, AND and OR, also match. The three files and i18n.ts are byte-identical between e0f8202 and this branch's base 668dd59. Reading the files end to end gives the full set, which is what the pin lists: 77 rendered strings over the dataset fixtures, 10 for the reference picker and 15 for the condition builder. That includes lower-case option labels, aria-label / title / placeholder values and composed sentences the regex cannot see.
  • H2 confirmed. The translated siblings import t / tFormat from ../i18n.js and key into engine.inspector.KIND.* / engine.form.*. This PR does the same.
  • H3 falsified. engine.inspector.* / engine.form.* rows do not live in the ten packs under packages/i18n/src/locales/. They live in the module-local two-locale table packages/app-shell/src/views/metadata-admin/i18n.ts (en-US and zh-CN). packages/i18n/README.md, section "Scope — the engine.* carve-out" (objectui#4662), records that no engine.* key exists in any pack, by design. Writing them into the packs would add a second mechanism and contradict that ruling. So every new key goes into both locales that namespace has, en and zh, and packages/i18n is untouched. The consequence is the carve-out's accepted price: the other eight shipped locales render these strings in English, like every other engine.* string. File-surface consequence: the claim named the packs, and the keys went to metadata-admin/i18n.ts instead. When this change was made, no open PR and no pushed claim branch (objectui#10585's included) touched i18n.ts.
  • H4. This table's completeness gate exists, pnpm check:i18n-designer-parity (EN/ZH rows and placeholders), and it is green. The pack gates are outside its subject and also green (packages/i18n/ run below).

en byte-identical: one-off measurement, not a committed test

A throwaway harness rendered the three inspectors under en-US in 21 fixture states and wrote document.body.innerHTML after each. Those states were: dataset bound, unbound, create mode, advanced filter and open popover; every reference kind with a hint, the directory lookup and its manual-entry toggle; the condition builder empty, with rows, with both joins and in raw mode. It ran once on the base sources and once on the fix. Both dumps were 169917 bytes with sha256 920a835809270db29527d4f0183a7ba518ec48cdcccf4d68c6256de6cab0b7c5, and cmp exited 0. The harness was deleted before the first commit.

Pin: red on base, green on head

inspectors/inspectorStrings.i18n-10586.test.tsx has 10 cases:

  • per inspector, a zh case: a sweep asserts that none of the pre-fix strings renders, using exact membership over element text, text nodes, aria-label, title and placeholder; then named zh samples are read back from the catalogue behind a not-the-key / not-the-en-row guard;
  • per inspector, an en case: every pre-fix string still renders, word for word;
  • a slot case for the dataset format sample;
  • three lit controls on text that was already translated, through the same probe: ObjectDefaultInspector (the locale prop channel), the membership-tier flag and the raw CEL editor's label (the I18nProvider channel).

Measurements:

  • Base leg: the four source files checked out from 668dd59, the pin from head 30171d2. Result Tests 4 failed | 6 passed (10), exit 1. The three zh sweeps list the English still rendered: dataset 77 of 77, reference 10 of 10, condition 15 of 15. The slot case read Sample: . The 6 passes are the three en cases and the three lit controls, so the probe does see a translation where one exists.
  • Keys-only leg: i18n.ts from head, the three inspectors from 668dd59, at 86d7ffe. Result Tests 4 failed | 6 passed (10), exit 1. The catalogue rows alone change nothing on screen.
  • Head: Tests 10 passed (10), re-run at ae5c50c.
  • Restore after each leg: git checkout HEAD -- on the mutated paths, then git diff HEAD empty and each path's git hash-object equal to its HEAD blob.

Gates

The branch merged origin/main three times: 2803fac, 4a3d500, then f99f9cd. Final head is ae5c50c.

gate exit reading at
pnpm exec vitest run packages/app-shell/src/views/metadata-admin/, as 4 path-split runs (inspectors/; previews/ + external/ + __tests__/; the top-level files in two halves) 0, 0, 0, 0 100 + 63 + 72 + 72 = 307 files; 1392 passed (1 skipped) + 733 + 500 + 876 tests d656ea6
same: the pin and previews/ again, after the second merge (which touched previews/ToolPreview.tsx) 0 62 files, 712 tests 50a3991
pnpm exec vitest run packages/i18n/ 0 71 files, 1178 tests d656ea6
readers outside that directory: 2 studio-design tests and the check-i18n-call-site-keys, check-i18n-dead-keys and vite-dts-explicit-extensions script tests 0 5 files, 292 tests d656ea6
pnpm turbo run type-check --filter @object-ui/app-shell --filter @object-ui/i18n --concurrency=2 0 31/31 tasks; both tsc --noEmit and tsc -p tsconfig.test.json ran, and --listFiles shows the test project compiles the new test ae5c50c
eslint, plain form, on the 5 changed TS files 0 --format json: 5 files, 0 errors, 20 warnings. The per-file warning counts equal base (0, 7, 3, 10; the new test 0) ae5c50c
check:control-bytes, check:test-path-roots, check:vi-mock-specifiers, check:vi-mock-inherit, check:vi-mock-override-shape, check:new-line-citations (0 new), check:changeset-claims, check:pending-changeset-literals, check:i18n-keys, check:i18n-drift, check:i18n-designer-parity, check:i18n-dead-keys, check-changeset-presence (5 source files of 1 released package, 1 changeset), check-changeset-no-major 0 each ae5c50c
  • The eslint narrowing is a measurement, not a skip. The population is the changed .ts / .tsx files, which the config's **/*.{ts,tsx} block covers; the JSON output counts 5 files. eslint.config.js configures no type-aware linting (no parserOptions.project / projectService), so this diff cannot move any untouched file's verdict.
  • check:changeset-claims reports 3 pending bodies that name i18n.ts: 6310-designer-formula-key-retired, 7125-dashboard-empty-state-keys-retired and 8632-malformed-picklist-option-loud. I read each paragraph, and all three still hold: the first two are about keys this PR does not touch, and the third restates the carve-out this PR follows.
  • NOT MEASURED: check:eager-locale-catalogues exited 2 with PREREQUISITE NOT MET, because it reads a built apps/console bundle; the eager-closure budget needs the same build. performance-budget.yml runs both. The repo-wide pnpm lint and the full pnpm test shards belong to CI.

Round 2 (89852f7): merged origin/main 9d89c6f (conflict-free; the incoming diff is plugin-gantt and its changeset only). At 89852f7, through os-verify-lock: the pin inspectorStrings.i18n-10586.test.tsx Tests 10 passed (10), exit 0; check:i18n-designer-parity exit 0; pnpm turbo run type-check --filter @object-ui/app-shell 30/30 tasks, exit 0 (app-shell cache miss, so it executed).

Acceptance notes

  • engine.condition.celLabel / engine.condition.advancedHint stay under engine.condition.*. The new row-mode keys went under engine.inspector.condition.*, per the triage's namespaces. Moving the two older keys is outside this card.
  • Strings drawn by shared children are outside this PR's three files: InspectorComboField's search and empty text, and the FilterBuilder inside the filter popover (the packs' filterBuilder.*).
  • The format sample's value still follows useDisplayLocale() (objectui#4575), unchanged. Only its "Sample:" lead-in moved to the catalogue.
  • The changeset declares @object-ui/app-shell: patch only. The dispatch asked for @object-ui/i18n too, but nothing in packages/i18n changes (H3).
  • PR 2 (HookDefaultInspector.tsx, ActionDefaultInspector.tsx) completes the card.

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


Generated by Claude Code

…igner's locale (objectui#10586)

DatasetDefaultInspector, FlowReferenceField (ReferenceCombobox) and
ConditionBuilder route their labels, placeholders, hints, option labels
and accessible names through the designer catalogue under
engine.inspector.dataset.*, engine.inspector.reference.* and
engine.inspector.condition.*, with en and zh rows. en renders unchanged.

Claude-Session: https://claude.ai/code/session_01KUxVUa7e39aNjhkKi1gsoy
Co-authored-by: Claude <noreply@anthropic.com>
…e run lists the leaked English

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

github-actions Bot commented Sep 25, 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/6310-designer-formula-key-retired.md

  • names packages/app-shell/src/views/metadata-admin/i18n.ts → packages/app-shell/src/views/metadata-admin/i18n.ts — edited by this change

    Not touched: designer.field.formula ('Formula (CEL)') in packages/app-shell/src/views/metadata-admin/i18n.ts, a different and live key belonging to metadata-admin's ObjectFieldInspector — the surface that still authors formula expressions.

.changeset/7125-dashboard-empty-state-keys-retired.md

  • names packages/app-shell/src/views/metadata-admin/i18n.ts → packages/app-shell/src/views/metadata-admin/i18n.ts — edited by this change

    Not touched: table.noRows ('No rows to display') and engine.form.noRows (packages/app-shell/src/views/metadata-admin/i18n.ts, read at widgets.tsx) — two different, same-named keys in different namespaces. Nor the comments in WidgetEmptyState.tsx, DatasetWidget.tsx, ObjectDataTable.tsx and PivotTable.tsx that record WHY three widgets with three strings became one shared empty state; the packs' own comment keeps that rationale and now names the retirement instead of a row that is gone.

.changeset/8632-malformed-picklist-option-loud.md

  • names views/metadata-admin/i18n.ts → packages/app-shell/src/views/metadata-admin/i18n.ts — edited by this change

    Two new strings land in the designer's own en / zh tables — the metadata-admin console owns its strings in views/metadata-admin/i18n.ts and is deliberately outside the ten locale packs (packages/i18n/README.md, "Scope — the engine.* carve-out").

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 785d3a731 (merge-base with origin/main): 5 file(s) changed outside .changeset/, read against 1505 pending declaration(s) that publish a body (2089 pending in total). · run

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 329 chunks) 3054.8 KB 3104.5 KB
Main entry chunk (gzip) 148.0 KB 350 KB
Entry file index-DXuUTUF8.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.57KB 6.15KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
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.17KB 10.58KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
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.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.52KB 3.45KB
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) 546.99KB 130.83KB
core (index.js) 9.22KB 3.71KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 223.91KB 62.28KB
fields (index.js) 260.06KB 65.86KB
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 (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 39.28KB 11.09KB
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.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
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) 51.49KB 14.64KB
plugin-charts (index.js) 74.94KB 20.89KB
plugin-chatbot (index.js) 198.36KB 47.20KB
plugin-dashboard (index.js) 133.60KB 35.41KB
plugin-designer (index.js) 216.25KB 44.39KB
plugin-detail (index.js) 232.96KB 61.65KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 152.20KB 39.05KB
plugin-gantt (index.js) 169.64KB 41.91KB
plugin-grid (index.js) 215.37KB 58.92KB
plugin-kanban (index.js) 48.35KB 15.08KB
plugin-list (index.js) 114.58KB 28.31KB
plugin-map (index.js) 22.42KB 7.38KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.55KB 11.99KB
plugin-timeline (index.js) 30.67KB 8.95KB
plugin-tree (index.js) 10.52KB 3.69KB
plugin-view (index.js) 87.84KB 21.96KB
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.66KB 3.50KB
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) 116.21KB 38.14KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.03KB 1.86KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.78KB 2.09KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.78KB 2.56KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 18.27KB 6.20KB
types (ai.js) 4.11KB 2.06KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
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.25KB
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) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 17.15KB 6.32KB
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

CI red at ae5c50c: two shards, neither this PR's

domain:ui seat 5, session_01KUxVUa7e39aNjhkKi1gsoy, 2026-09-25T14:51Z. The at-tier contract review of this head PASSed; the record follows. Two required test shards are red, each with one failing file that this PR does not touch (it touches only packages/app-shell/src/views/metadata-admin/):

  • Test (shard 2/8): apps/console/src/pages/developer/ApiConsolePage.requestPreset-10591.test.tsx:299, the findByPlaceholderText timing race objectui#10645 records. The same test evicted PR objectui#10635 from the merge queue today, and its measured root cause (a cold React.lazy inside the default 1000 ms window) is on that card.
  • Test (shard 3/8): packages/plugin-form/src/editDirtyPayload-10156.test.tsx:313, the ModalForm edit case. The typed value was lost and the whole record was sent. plugin-form does not depend on app-shell. The DrawerForm row passed in the same run, and main's own CI passed every shard at this PR's merge-base f99f9cd. The seat files it as a card, since ModalForm has none of the first-read gate objectui#10190 gave DrawerForm.

Neither fix exists yet, and this PR does not widen to carry them. The relay has no re-run op, and a push only to kick CI is not allowed. So this PR's one re-run comes from the next origin/main merge, once main moves past f99f9cd. The PR stays a draft until then, and the seat holds its ACCEPT for a green head.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: ae5c50c9c540eff77f10d456fee9e8d3e416b01b

① Derived judgments

Binding texts. Triage 5832133290: 「route the strings through engine.inspector.* / engine.form.* with keys in every built-in locale; en stays byte-identical.」 Amended claim 5834151314 (amends 5832991567): PR 1 covers inspectors/DatasetDefaultInspector.tsx, inspectors/FlowReferenceField.tsx, inspectors/ConditionBuilder.tsx, 「their new rows in the designer catalogue i18n.ts (ENGINE_STRINGS_EN and ENGINE_STRINGS_ZH), pins beside them, and one .changeset/10586-…md」; 「⛔ Not packages/i18n/src/locales/」; 「Refs #10586 (PR 1)」. Merge-base is f99f9cde75bc563b18b39a37b867214bc1866fc7 (also main's tip at fetch time); the diff is 6 files, +929/-138, every one inside that surface: the changeset, metadata-admin/i18n.ts (+236/-0), the three inspectors, and the new pin inspectors/inspectorStrings.i18n-10586.test.tsx. Nothing under packages/i18n/.

Carve-out check (the amended claim's correction 1). packages/i18n/README.md:257-297 says what the claim says: the metadata-admin surface 「resolves its engine.* keys through a module-local two-locale table in packages/app-shell/src/views/metadata-admin/i18n.ts: a plain Record lookup over en-US and zh-CN, not an i18next namespace」, 「no engine.* key exists in any of the ten packs」, 「Only en and zh are covered … That is the price of the carve-out, not a backfill someone forgot」, and the reopening condition is 「a stated demand for an admin console in a language other than en or zh」, citing objectui#4662. So 「keys in every built-in locale」 for this namespace is en-US plus zh-CN, and the PR supplies both. Siblings key into the same table: at the head, the non-test inspectors that import t from ../i18n.js and key engine.inspector.KIND.* include ActionDefaultInspector (engine.inspector.action), AppNavInspector (appNav), DashboardDefaultInspector (dashboard), DashboardWidgetInspector (widget), FlowEdgeInspector (flowEdge), FlowNodeConfigField / FlowNodeInspector (flowNode), HookDefaultInspector (hook), PageBlockInspector (pageBlock), ReportDefaultInspector (report), ViewColumnInspector (viewColumn), ViewVariantInspector (view), plus ObjectDefaultInspector, ObjectFieldInspector, PageDefaultInspector, FlowObjectListField and _shared importing the same module. Right: the three files now do exactly that.

Catalogue rows, metadata-admin/i18n.ts. EN: engine.inspector.condition.* 15 rows at 532-546, engine.inspector.dataset.* 88 rows at 809-896, engine.inspector.reference.* 10 rows at 898-907. ZH: 2636-2650, 2888-2975, 2977-2986. Per-namespace counts measured 88 / 10 / 15 = 113 in each table, matching the PR body. Collision check on the head file: ENGINE_STRINGS_EN 1794 keys, 1794 unique; ENGINE_STRINGS_ZH 1849 keys, 1849 unique; no duplicate object key in either, so no silent last-one-wins. Every EN key has a ZH row (EN minus ZH is empty); the ZH-only extras (engine.enum.type.*, engine.flowNode.*) already exist at the merge-base and are not this PR's. Placeholder sets match on all 113 pairs. The diff to this file is additions only (+236/-0), so no existing en value changed. Right.

zh-CN is real translation, not an English copy. Programmatic: 0 of the 113 zh rows equal their en row; 0 lack a CJK character. Spot-check, 21 rows across the three inspectors: dataset kind 数据集, aggregate.count_distinct 去重计数, granularity.none — 无 —, formatKind.currency 货币 —— $1,234.50, usage.bound 已被 {reports} · {dashboards} 绑定 —— 修改会影响它们。, baseObjectChangeHint 更改基础对象会清空其维度、度量、关联与筛选。, noJoins 暂无关联。添加一个关联({object} 上的查找字段),即可使用 {path} 形式的维度 / 度量。, measureFilterHelp 只有满足此筛选的行才计入该度量(…)。, relWarning 关联 {rel} 不在「包含的关联」中。, sample 示例:{sample}, filterNeedsObject 先选择基础对象,才能添加筛选条件。; reference managerHint 运行时根据提交人的上级自动解析 —— 无需填写。, queueUnsupported 运行时暂不支持队列审批人 —— 该位置不会解析到任何人。, setTriggerObject 请先在开始节点上设置流程的触发对象,才能列出字段。, declarative 声明式, fieldsOf {object} 的字段。; condition always 始终生效 —— 无条件。, and 且 / or 或 (the packs' filterBuilder.and / or at packages/i18n/src/locales/zh.ts:3631-3632 are 且 / 或, so the i18n.ts comment is true), op.truthy 已设置 / 为真, op.falsy 为空 / 为假, subjectPlaceholder 字段 / 上下文. All real. Right.

ConditionBuilder.tsx. COMPARE_OPS at 40-48 becomes a union of labelKey rows and two bare symbol rows for the greater-or-equal and less-or-equal signs; the render at 801 picks 'symbol' in o ? o.symbol : tLocal(o.labelKey). The stored value of each option is unchanged, so no accepted set moves. tLocal at 636-637 is the existing useMetadataLocale() channel. Replacements at 713 (Builder), 758 (Expression), 763 (Always — no condition.), 773-774 (AND / OR), 782 (field / context), 791 (Remove condition), 806 (value), 817 (Add condition). The pre-existing engine.condition.celLabel / advancedHint rows are untouched. Right against the triage's namespaces (engine.inspector.condition.*).

FlowReferenceField.tsx. Import at 49 adds tFormat and SupportedLocale. connectorsToOptions(connectors, locale?) at 362-371: the · declarative annotation now reads t('engine.inspector.reference.declarative', locale); with no locale pickTable (i18n.ts:4654-4662) falls to the en table, so the untouched FlowReferenceField.connector.test.ts, which calls it with one argument and expects · declarative, still holds. useConnectorListOptions(enabled, locale) at 464 keys its effect on [enabled, locale] at 485 (a string, so no identity churn). RecordLookupCell gains a required locale prop at 501-512 and names its two toggles at 540-541 and 584-585. ReferenceCombobox's locale is the pre-existing useMetadataLocale() at 627 (base line 620), handed to the hook at 643 and the cell at 700. Manager placeholder 744, manager hint 748, queue warning 770, fieldsOf / setTriggerObject / actionsOf / chooseConnector at 797-808. Right.

DatasetDefaultInspector.tsx. KeyedOption / localizeOptions / withSlots at 51-68; the five option tables at 70-112 carry labelKey and keep their stored values; DECIMALS_OPTIONS / CURRENCY_OPTIONS at 113-118 stay literal (digits and ISO codes). The component reads locale from MetadataDefaultInspectorProps at 350 (the prop the registry declares at default-inspector-registry.ts:58-59 and ObjectDefaultInspector / ReportDefaultInspector already read) and builds a plain tr at 354. Advanced (159), MeasureFormatField (200), RelWarning (244) and DatasetFilterField (263) take locale. Kind label 420; usage line 430 composes usage.bound from the …One / …Other rows picked by === 1, the rule the base applied; engine.form.add reuse at 481 and engine.form.searchFields at 579 / 633 (rows exist: Add / Search fields… and 添加 / 搜索字段…); noJoins 486; dimensionN 556, measureN 609 with { index: i + 1 }; derivedNeedTwo / derivedNeedOne at 660 with op: derived.op ?? '', which renders the same empty slot the base's {derived.op} rendered when undefined. InspectorSelectField (_shared.tsx:338) keys no hook on options, so the per-render arrays from localizeOptions are harmless. Right.

en stays byte-identical (triage). Measured from the diff, not from the PR's harness. For the 25 ConditionBuilder and FlowReferenceField literals, each hand-extracted old string equals its new en row: 0 mismatches. For the 88 dataset rows, every en row was split on its {slot}s and each fragment looked up verbatim in the merge-base source (with the JSX forms &amp; and &apos; normalised): all found, except the two fragments of derivedNeedTwo / derivedNeedOne, which the base composed at runtime as Select {need === 2 ? 'exactly 2 measures' : 'at least 1 measure'} for {derived.op}. and which the rows reproduce word for word. Special characters verified present in the base with the same code points: the em dashes, the U+2019 in flow’s, the U+2212 minus in difference (a − b), ÷, ×, the U+2026 ellipses in Select an object… / + Add filter… and the rest, — none —, Currency — $1,234.50. Interpolated strings render the same text: withSlots splits on {token} and interleaves the same nodes the base inlined (Sample: + the mono span at 227; Relationship + code + isn't in Included relationships. at 248; noJoins with its two code elements; filterAdvanced with the emphasised Source), and tFormat substitutes through a callback so a value with $ is not reinterpreted. Existing DatasetDefaultInspector*.test.tsx files pass locale: 'en-US' and so read the en rows. Holds.

Missing asks. None in PR 1's scope. The claim's 「re-measure the census first」 is in the PR body (H1). Every literal the base files carried is behind t(): the card's floor regex over the three head files returns 0 hits, and the pin enumerates 77 + 10 + 15 rendered strings. The shared children that still read English (InspectorComboField, _shared.tsx defaults) are assigned to PR 2 by the amended claim's correction 2, so they are not a gap in PR 1.

Done but not asked. connectorsToOptions gains an optional second parameter and five internal components gain a locale prop; all are needed by the ask and none is a published export. Nothing else.

Existing pins edited or deleted. None: the file list adds one test and edits no test.

New pins can fail. inspectorStrings.i18n-10586.test.tsx (481 lines): per inspector a zh case (expectNone over exact membership of element text, text nodes, aria-label, title, placeholder, then named zh samples through zhRow, which rejects an echoed key and an en copy, lines 141-146) and an en case (expectAll, so the fixtures are proven to reach each branch); a slot case for Sample: (316-326). Negative leg: on the base sources the same fixtures render the en literals under zh, so expectNone's leaked list is non-empty and the case fails; since commit 30171d2 the sweep runs before zhRow, so a base run names the leaked English rather than a missing key. Lit controls, one per locale channel: ObjectDefaultInspector's designer.object.section.basic through the locale prop (328-333), flagUnknownValue's engine.form.invalid through I18nProvider (409-412), and engine.condition.celLabel (477-480). The reference and condition fixtures fold a pre-click state into a module-level extra set (370, 375, 426) that readReference / readCondition clear first, so the union is correct. Green at head in CI: shards 1, 4, 5, 6, 7, 8 passed and the two red shards each report exactly one failing file, neither this one.

② Semver level

.changeset/10586-inspector-strings-i18n.md declares '@object-ui/app-shell': patch. Right: a bug fix with no API change; AGENTS.md 版本号策略 forbids major in any changeset and this declares none; the fixed group in .changeset/config.json bumps all 39 packages together regardless. @object-ui/i18n is not listed and rightly so: packages/i18n/ is untouched (H3). Not a content/docs/**-only change, so a package-level entry is owed and present.

Changeset prose, sentence by sentence: (1) 「DatasetDefaultInspector, FlowReferenceField (with the ReferenceCombobox it wraps) and ConditionBuilder rendered their section titles, field labels, placeholders, hints, option labels and accessible names as English literals, so a zh-CN author read an English form inside an otherwise Chinese designer.」 True: the base files carry those literals; FlowReferenceField renders ReferenceCombobox at head line 833. (2) 「Those strings now come from the designer's own string catalogue, under engine.inspector.dataset.*, engine.inspector.reference.* and engine.inspector.condition.*, plus the existing engine.form.add and engine.form.searchFields, each with an en and a zh row.」 True per the rows and call sites cited above. (3) 「The dataset inspector reads the locale its host already passes; the other two follow the active language through useMetadataLocale().」 True: Dataset.tsx:350, FlowRef.tsx:627, Cond.tsx:636. (4) 「The en-US text is unchanged.」 True, measured above. (5) 「Stored values, the greater-or-equal / less-or-equal operator symbols, currency codes and decimal digits are not words and stay as they were.」 True: KeyedOption keeps value, Cond.tsx:45-46, Dataset.tsx:113-118.

The diff adds no docs or README prose. The two catalogue comments it adds (i18n.ts: 「{…} tokens in a sentence are slots the inspector fills」; 「The join words follow the packs' own filterBuilder.and / filterBuilder.or」; 「zh has one plural form, so each …One / …Other pair carries the same row」) are true against the code and zh.ts:3631-3632.

③ Boundary flags

  • PR body flags. Clause-②: no: holds (below). H3 falsified: confirmed by the README (above); the amended claim adopted it. The report's open question (keep the carve-out, A, or reopen objectui#4662, B): the amended claim ruled A, and the PR implements A; nothing for this review to decide. Acceptance notes: engine.condition.celLabel / advancedHint staying under engine.condition.* is correct and outside this card; the shared children are PR 2's by the amended claim; the format sample's value still comes from useDisplayLocale() (only the Sample: lead-in moved, Dataset.tsx:227); the app-shell-only changeset is right. The body's own gate readings (check:i18n-drift, check:i18n-designer-parity, eslint counts) were not re-run here; the facts they describe (0 en rows changed, EN/ZH parity, placeholder parity) were measured independently above and hold.
  • CI at the head. Poll 1: 42 check-runs, 30 success, 3 skipped, 9 in progress. Poll 2 (14:38Z): 35 success, 3 skipped, 2 in progress, 2 failure. Poll 3 (14:40Z): 43 check-runs, 37 success, 3 skipped, 3 failure: Test (shard 2/8), Test (shard 3/8) and the Test rollup that depends on them. Type Check, Lint, Changeset Bump Policy, Changeset Declaration, Changeset Fixed Group Check, Build & E2E, Spec Main Shape Gate and the rest are green. The two red shards each hold one failing file, and neither is in this PR's six paths: shard 2 apps/console/src/pages/developer/ApiConsolePage.requestPreset-10591.test.tsx:299 (findByPlaceholderText('/api/v1/...') timed out on the console boot screen 「Initializing application… Connecting to data source」), shard 3 packages/plugin-form/src/editDirtyPayload-10156.test.tsx:313 (a vi.fn call expectation). The PR touches nothing under apps/ or packages/plugin-form/; plugin-form does not depend on app-shell; the pin's never-torn-down vi.stubGlobal('fetch') cannot reach another file because the dom projects run with isolate: true (vitest.config.mts:375-382, 474-475) and ten sibling inspector tests already stub globals the same way. Main at the merge-base f99f9cd passed all eight shards, though shard membership shifts with the file count so a shard number is not a file identity; the two most recent failed main runs (d22b37b 2026-09-25, 58d65c5 2026-09-24) failed coverage test shards, so test-run flakiness on main is a measured fact, while these two tests' own history was not measured. A rerun of the two shards is owed before merge; not a contract defect of this diff.
  • Draft. Yes: draft: true, mergeable_state: blocked.
  • Closing keyword. None. Body line 1 Part of #10586, line 2 Refs #10586; a regex scan for Fixes / Closes / Resolves (any inflection) before #10586 over the title, the body and all five commit messages (86d7ffe, 30171d2, d656ea6, 50a3991, ae5c50c) finds nothing. The squash will not close the card.
  • Clause-② holds. @object-ui/app-shell package.json exports only . and ./styles.css; src/index.ts re-exports six metadata-admin pages (lines 369-374) and none of the three files, and no index.ts under src/ names FlowReferenceField, so connectorsToOptions is module-level only. Option values, stored draft shapes and the spec are untouched; no accept set or published export moves.
  • Main moved under the PR's files? No. The merge-base equals refs/review/main at fetch time, so nothing under the six paths moved.

Implemented-by: claude/issue-10586-inspector-strings-i18n
Reviewed-by: session_01KUxVUa7e39aNjhkKi1gsoy

VERDICT: PASS

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 329 chunks) 3055.0 KB 3104.5 KB
Main entry chunk (gzip) 147.9 KB 350 KB
Entry file index-3274JQL5.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.57KB 6.15KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
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.17KB 10.58KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
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.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.52KB 3.45KB
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) 546.99KB 130.83KB
core (index.js) 9.22KB 3.71KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 223.91KB 62.28KB
fields (index.js) 260.14KB 65.89KB
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 (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 39.28KB 11.09KB
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.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
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) 51.49KB 14.64KB
plugin-charts (index.js) 74.94KB 20.89KB
plugin-chatbot (index.js) 198.36KB 47.20KB
plugin-dashboard (index.js) 133.60KB 35.41KB
plugin-designer (index.js) 216.25KB 44.39KB
plugin-detail (index.js) 232.85KB 61.58KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 152.20KB 39.05KB
plugin-gantt (index.js) 169.71KB 41.93KB
plugin-grid (index.js) 215.37KB 58.92KB
plugin-kanban (index.js) 48.35KB 15.08KB
plugin-list (index.js) 114.58KB 28.31KB
plugin-map (index.js) 22.42KB 7.38KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.55KB 11.99KB
plugin-timeline (index.js) 30.67KB 8.95KB
plugin-tree (index.js) 10.52KB 3.69KB
plugin-view (index.js) 87.77KB 21.99KB
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.66KB 3.50KB
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) 116.21KB 38.14KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.03KB 1.86KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.78KB 2.09KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.78KB 2.56KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 18.27KB 6.20KB
types (ai.js) 4.11KB 2.06KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
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.25KB
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) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 17.15KB 6.32KB
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: 89852f79e09f6458e54af878845385773518fd60

① Derived judgments

Re-review frame (what moved since the PASSed head). The head 89852f7 is a merge commit with parents ae5c50c (the head that record 5834435778 PASSed) and 9d89c6f (main). The merge-base against refs/review/main (785d3a7 at fetch time) is 9d89c6f, which is also the PR's base.sha. The PR's own diff at the new merge-base (git diff 9d89c6f 89852f7) is byte-identical to its diff at the old merge-base (git diff f99f9cd ae5c50c): the same 6 files, +929/-138, both patch files sha256 f10d416bfe2541ef41317f2ca19a638b8441e9d27c5d16570e14b0d506ee3c00, cmp exit 0. What the merge brought in (git diff ae5c50c 89852f7) is exactly three files, .changeset/10633-gantt-silent-overtake.md, packages/plugin-gantt/src/ObjectGantt.silentOvertake-10633.test.tsx and packages/plugin-gantt/src/ObjectGantt.tsx (+422/-3); the same diff restricted to packages/app-shell/src/views/metadata-admin/ and the PR's changeset is empty, so the merge touched none of the PR's files. I replayed the merge in memory with git merge-tree --write-tree ae5c50c 9d89c6f: it reports no conflict and its tree 933a667ccad2df3b65335c8fadb370d60bbed7fb equals the head commit's tree, so the merge commit carries no hand edits of its own. The six blobs at 89852f7 equal those at ae5c50c, so the line numbers below address the same text as before; I re-derived every judgment from the diff rather than inheriting the prior record.

Binding texts. Triage 5832133290: 「route the strings through engine.inspector.* / engine.form.* with keys in every built-in locale; en stays byte-identical.」 Amended claim 5834151314 (amends 5832991567): file surface in packages/app-shell/src/views/metadata-admin/: inspectors/DatasetDefaultInspector.tsx, inspectors/FlowReferenceField.tsx, inspectors/ConditionBuilder.tsx, 「their new rows in the designer catalogue i18n.ts (ENGINE_STRINGS_EN and ENGINE_STRINGS_ZH), pins beside them, and one .changeset/10586-…md」; 「⛔ Not packages/i18n/src/locales/, and ⛔ not HookDefaultInspector.tsx or ActionDefaultInspector.tsx in PR 1」; 「Refs #10586 (PR 1, PR objectui#10651); Fixes #10586 rides PR 2」. The PR's six paths are the changeset, metadata-admin/i18n.ts (+236/-0), the three inspectors, and the new pin inspectors/inspectorStrings.i18n-10586.test.tsx (+481). Every one is inside that surface; nothing under packages/i18n/, nothing in the Hook or Action inspectors.

Carve-out (the amended claim's correction 1). packages/i18n/README.md at the head, section "Scope — the engine.* carve-out (metadata-admin / Studio)" (lines 257-297), says: the metadata-admin surface 「resolves its engine.* keys through a module-local two-locale table in packages/app-shell/src/views/metadata-admin/i18n.ts: a plain Record lookup over en-US and zh-CN」; 「no engine.* key exists in any of the ten packs」; 「Only en and zh are covered … That is the price of the carve-out, not a backfill someone forgot」; the reopening condition is 「a stated demand for an admin console in a language other than en or zh」, citing objectui#4662. So 「keys in every built-in locale」 for this namespace means en-US plus zh-CN, and the PR supplies both. Right.

Catalogue rows, metadata-admin/i18n.ts. ENGINE_STRINGS_EN spans lines 181-2302 and ENGINE_STRINGS_ZH 2304-4353. New EN rows: engine.inspector.condition.* 15 at 532-546, engine.inspector.dataset.* 88 at 809-896, engine.inspector.reference.* 10 at 898-907; the same keys in ZH at 2636-2650, 2888-2975 and 2977-2986. Measured with a key-only regex over the head file: EN 1794 keys, 1794 unique; ZH 1849 keys, 1849 unique; no duplicate object key in either table, so no silent last-one-wins. Base versus head: 113 keys added and 0 removed in each table; the ZH-only remainder (55 keys, engine.flowNode.* and friends) is the identical set at base and head and holds no engine.inspector.* key. Every one of the 113 EN keys has a ZH row; the {slot} sets match on all 113 pairs; 0 of the 113 zh rows equal their en row; 0 lack a CJK character. The two reused rows exist in both tables: engine.form.add (Add / 添加) and engine.form.searchFields (Search fields… / 搜索字段…). The diff to this file is additions only, so no existing en value changed. Right.

zh-CN is real translation. Beyond the programmatic check, read: dataset kind 数据集, aggregate.count_distinct 去重计数, granularity.none — 无 —, formatKind.currency 货币 —— $1,234.50, usage.bound 已被 {reports} · {dashboards} 绑定 —— 修改会影响它们。, baseObjectChangeHint 更改基础对象会清空其维度、度量、关联与筛选。, noJoins 暂无关联。添加一个关联({object} 上的查找字段),即可使用 {path} 形式的维度 / 度量。, relWarning 关联 {rel} 不在「包含的关联」中。, sample 示例:{sample}; reference managerHint 运行时根据提交人的上级自动解析 —— 无需填写。, queueUnsupported 运行时暂不支持队列审批人 —— 该位置不会解析到任何人。, declarative 声明式; condition always 始终生效 —— 无条件。, and 且, or 或, op.truthy 已设置 / 为真, subjectPlaceholder 字段 / 上下文. The packs' filterBuilder.and / filterBuilder.or at packages/i18n/src/locales/zh.ts:3631-3632 are 且 / 或, so the ZH comment at i18n.ts 2634-2635 is true. Right.

ConditionBuilder.tsx. COMPARE_OPS at 40-48 becomes a readonly union of labelKey rows and two bare symbol rows for the greater-or-equal and less-or-equal signs (45-46); the render at 801 picks 'symbol' in o ? o.symbol : tLocal(o.labelKey); each option's stored value is unchanged and COMPARE_OPS is not exported. tLocal at 636-637 is the pre-existing useMetadataLocale() channel (base 634-635). Replacements: 713 Builder, 758 Expression, 763 Always — no condition., 773-774 AND / OR, 782 subject placeholder, 791 Remove condition aria-label, 806 value placeholder, 817 Add condition. The pre-existing engine.condition.celLabel (720) and engine.condition.advancedHint (746) rows and call sites are untouched. The new keys sit under the triage's engine.inspector.*. The module's export list is identical at base and head (8 exports). Right.

FlowReferenceField.tsx. Import at 49 adds tFormat and the SupportedLocale type. connectorsToOptions(connectors, locale?) at 362 reads the · declarative annotation through t('engine.inspector.reference.declarative', locale) at 371; pickTable (i18n.ts 4654-4662) falls to the EN table when the locale is undefined, so the untouched FlowReferenceField.connector.test.ts (one-argument calls, expecting Billing API (billing) · declarative) still holds. useConnectorListOptions(enabled, locale) at 464 keys its effect on [enabled, locale] at 485, both primitives; AGENTS.md #10 (never depend on a memoised identity) is respected since nothing depends on an identity. RecordLookupCell at 501-512 gains a required locale and names its two toggles at 540-541 and 584-585; it is a module-private function. ReferenceCombobox's locale is the pre-existing useMetadataLocale() at 627 (base 620), handed to the hook at 643 and the cell at 700. Manager placeholder 744 and hint 748, queue warning 770, fieldsOf 797, setTriggerObject 801, actionsOf 805, chooseConnector 808. FlowReferenceField (827) still renders ReferenceCombobox (833). Export list identical at base and head (14 exports). Right.

DatasetDefaultInspector.tsx. KeyedOption 51, localizeOptions 53 and withSlots 63; the five option tables at 70-112 carry labelKey and keep their stored values; DECIMALS_OPTIONS 113 and CURRENCY_OPTIONS 118 stay literal (digits, ISO codes). The component reads locale from MetadataDefaultInspectorProps at 350, a required locale: SupportedLocale on the registry's prop type (default-inspector-registry.ts:59), and builds tr at 354 as a plain function; no hook in the file lists tr or locale as a dependency (the four useMemos at 376-390 are the pre-existing option builders). Advanced 159, MeasureFormatField 200, RelWarning 244 and DatasetFilterField 263 take locale. Kind label and title fallback 420; the usage line at 430 composes usage.bound from the …One / …Other rows picked by === 1, the rule the base applied; engine.form.add at 481; noJoins at 486 with the two code elements as slots; engine.form.searchFields at 579 and 633; dimensionN 556 and measureN 609 with { index: i + 1 }; derivedNeedTwo / derivedNeedOne at 660 with op: derived.op ?? '', which renders the same empty slot the base rendered for an undefined op; Sample 227, RelWarning 248, filterAdvanced 308, the condition count 318. InspectorSelectField (_shared.tsx:338) takes a plain options array and keys no hook on it, so the per-render arrays from localizeOptions are harmless. Export list identical at base and head (2 exports). Right.

en stays byte-identical (triage). Measured from the diff, not from the PR's harness: each of the 113 new en rows was split on its {slot}s and every fragment looked up verbatim in the merge-base source of its file (JSX entities &amp; and &apos; normalised). 111 rows are found whole; the two exceptions, derivedNeedTwo and derivedNeedOne, are the base's runtime composition Select {need === 2 ? 'exactly 2 measures' : 'at least 1 measure'} for {derived.op}., which the rows reproduce word for word. The other composed base strings (Bound by … report(s) · … dashboard(s) — changes affect them., ${count} condition(s), Dimension {i + 1}, Measure {i + 1}, Sample: before the mono span, Fields of {objectName}., Actions of {connectorName}., ${base} · declarative) map to rows that render the same text. Code points verified present in the base with the same values: U+2019 in flow’s, U+2212 in difference (a − b), U+2026 ellipses, U+2014 em dashes, ÷, ×; the ≥ / ≤ signs stay in source. withSlots splits on {token} and interleaves the same code and span nodes the base inlined, and tFormat (i18n.ts 4975-4983) substitutes through a callback so a value holding $ is not reinterpreted. The existing DatasetDefaultInspector*.test.tsx files pass locale: 'en-US' and read the en rows. Holds.

Missing asks. None in PR 1's scope. The claim's 「re-measure the census first」 is in the PR body (H1) and reproduces here: the card's floor regex over the three base files gives Dataset 30 attribute hits plus 2 text-node hits and 0 t() calls, FlowReferenceField 5 and 1, ConditionBuilder 1 attribute hit (3 with the AND / OR text nodes, as the body says) and 3 tLocal calls; over the three head files it gives 0 hits. The body's statement that the three files and i18n.ts are byte-identical between e0f8202 and 668dd59 holds (git diff --stat empty), and they are also identical between 668dd59 and the merge-base 9d89c6f, so the base I compared against is the base the pin was measured on. The shared children that still read English (InspectorComboField, _shared.tsx defaults) are PR 2's by the amended claim's correction 2, so they are not a gap in PR 1.

Done but not asked. connectorsToOptions gains an optional second parameter, and five module-private components gain a locale prop; both are what the ask needs, and none is a published export. The merge commit adds nothing of its own. Nothing else.

Existing pins edited or deleted. None: the file list is five source or changeset files plus one new test; no existing test is modified. The existing zh test on this surface, FlowReferenceField.flag.i18n-10448.test.tsx, asserts only the engine.form.invalid flag row and is green in CI at the head.

New pins can fail. inspectorStrings.i18n-10586.test.tsx (481 lines): surface() at 103 collects every element's text, every text node and every aria-label / title / placeholder, with exact membership; zhRow at 141-146 rejects an echoed key (143) and an en copy (144); expectAll 148 and expectNone 153. Per inspector a zh case (304, 398, 464), an en case (312, 405, 473), the slot case 316, and three lit controls: 328 ObjectDefaultInspector's designer.object.section.basic through the locale prop, 409 engine.form.invalid through I18nProvider, 477 engine.condition.celLabel; 470 also pins that ≥ and ≤ still render under zh. Negative leg: on the base sources the identical fixtures render the English literals under zh, so leaked is non-empty and the three zh cases fail, and the slot case reads Sample: under zh and fails; the en cases and the lit controls pass on base because the fixtures and the pre-existing translations are unchanged. The en leg on the same fixtures proves every listed literal is actually reached, so the zh leg cannot pass vacuously. Lit control: yes, one per locale channel. I did not execute the pin (the brief forbids a working tree); at 89852f7 CI's eight Test shards and the Test rollup are green, and this file is in them.

② Semver level

.changeset/10586-inspector-strings-i18n.md declares '@object-ui/app-shell': patch. Right: AGENTS.md line 168 requires a changeset whenever src/ of a fixed-group package changes (five source files under packages/app-shell/src/, one changeset; check-changeset-presence and Changeset Declaration are green at the head); 版本号策略 forbids major in any changeset and none is declared (Changeset Bump Policy green); a fix with no API move is patch; the fixed group in .changeset/config.json bumps all 39 packages together regardless. @object-ui/i18n is not listed, and rightly: packages/i18n/ is untouched. Not a content/docs/**-only change, so a package-level entry is owed and present.

Changeset prose, sentence by sentence: (1) 「DatasetDefaultInspector, FlowReferenceField (with the ReferenceCombobox it wraps) and ConditionBuilder rendered their section titles, field labels, placeholders, hints, option labels and accessible names as English literals, so a zh-CN author read an English form inside an otherwise Chinese designer.」 True: the base files carry those literals in title, label, placeholder, help, option label and aria-label positions; FlowReferenceField renders ReferenceCombobox at 833. (2) 「Those strings now come from the designer's own string catalogue, under engine.inspector.dataset.*, engine.inspector.reference.* and engine.inspector.condition.*, plus the existing engine.form.add and engine.form.searchFields, each with an en and a zh row.」 True per the rows and call sites above. (3) 「The dataset inspector reads the locale its host already passes; the other two follow the active language through useMetadataLocale().」 True: Dataset 350 with the required prop at default-inspector-registry.ts:59; FlowReferenceField 627 and ConditionBuilder 636, both pre-existing calls. (4) 「The en-US text is unchanged.」 True, measured above. (5) 「Stored values, the ≥ / ≤ operator symbols, currency codes and decimal digits are not words and stay as they were.」 True: KeyedOption keeps value, ConditionBuilder 45-46, Dataset 113-118.

The diff adds no docs or README prose. The comments it adds are true against the code: i18n.ts EN 806-808 (option labels keyed by the stored value; {…} tokens are slots the inspector fills), ZH 2634-2635 (the join words follow the packs' filterBuilder.and / filterBuilder.or, verified at zh.ts:3631-3632), ZH 2886-2887 (zh has one plural form, so each …One / …Other pair carries the same row: both reportsOne / reportsOther are {count} 个报表, and likewise dashboards and conditions), ConditionBuilder 38-39 (a bare symbol for the two signs, which read the same in every locale), Dataset 351-353 (tr is a plain function and nothing keys on its identity: no dependency list names it), FlowReferenceField 360 (omitted, the locale is en: pickTable). No added line carries a path:line citation (AGENTS.md #11; Line Citation Gate green).

③ Boundary flags

  • PR body flags and open questions. Clause-②: no holds (below). H3 falsified: confirmed by the README section quoted above; the amended claim adopted it. The report's open question (keep the carve-out, A, or reopen objectui#4662, B): the amended claim ruled A and the PR implements A; nothing for this review to decide. Acceptance notes: engine.condition.celLabel / advancedHint staying under engine.condition.* is correct and outside this card; the shared children are PR 2's by the amended claim; the format sample's value still comes from useDisplayLocale() and only its Sample: lead-in moved (Dataset 227); the app-shell-only changeset is right; PR 2 completes the card. The body's own local gate readings were not re-run here; the facts they describe (0 en rows changed, EN / ZH parity, placeholder parity) were measured independently above and hold, and the CI job that runs pnpm check:i18n-designer-parity (the type-check job, display name Type Check, step "Verify the metadata-admin designer string tables stay paired") is green at the head. The new Round 2 paragraph is true where it states facts about the tree: 「merged origin/main 9d89c6f (conflict-free; the incoming diff is plugin-gantt and its changeset only)」 matches the merge-tree replay and the three-file incoming diff measured above. Its three gate readings at 89852f7 (the pin Tests 10 passed (10), check:i18n-designer-parity exit 0, turbo run type-check --filter @object-ui/app-shell 30/30) are local runs I could not repeat; each is corroborated by a green check-run at the same sha (Test shards and rollup, Type Check). One sentence earlier in the Gates section, 「Final head is ae5c50c」, is now stale; the Round 2 paragraph two paragraphs later names the real head, so this is a wording lag, not a false claim about the diff.
  • CI at the head. Read 2026-09-25T15:27Z on 89852f7: 43 check-runs, all completed: 40 success, 3 skipped (dependabot, Test (coverage), Test (coverage shard …/4)), 0 failure, 0 in progress, so no re-poll was needed. All eight Test (shard n/8) jobs and the Test rollup are green, including shard 2/8 and shard 3/8, which were red at ae5c50c on files this PR does not touch (objectui#10645, objectui#10659; the seat's note 5834427039). Type Check, Lint, Build & E2E, Test (dist pins), Changeset Bump Policy, Changeset Declaration, Changeset Fixed Group Check, Changeset Claim Re-read, Line Citation Gate, Spec Main Shape Gate, Bundle Analysis and the rest are green.
  • Draft. Yes: draft: true; mergeable: true, mergeable_state: clean.
  • Closing keyword. None. Title fix(app-shell): three designer inspectors read their words in the designer's locale (objectui#10586); body line 1 Part of #10586, line 2 Refs #10586 — PR 1 of 2. A regex for close / closes / closed / fix / fixes / fixed / resolve / resolves / resolved followed by an optional objectstack-ai/objectui and #10586 over the title, the body and all six commit messages (86d7ffe, 30171d2, d656ea6, 50a3991, ae5c50c, 89852f7) finds nothing; the only fix tokens are the conventional-commit prefix fix(app-shell): in the title and the first commit, which is not followed by #10586. The merge commit's message is Merge commit '9d89c6f4c12184c2a8b6ded10b8534b20b510f9b' into claude/issue-10586-inspector-strings-i18n. The card stays open after merge.
  • Clause-② holds. @object-ui/app-shell package.json exports only . and ./styles.css; src/index.ts re-exports metadata-admin pages and registries (369-426) and none of the three files; the only barrel that names one of them is inspectors/index.ts, which imports DatasetDefaultInspector for registerMetadataDefaultInspector('dataset', …), a registration rather than an export. The three modules' export lists are identical at base and head, so connectorsToOptions's optional parameter is module-level only. Stored option values and draft shapes are untouched; the diff imports nothing from @objectstack/spec. No accept set or published export moves.
  • Did main move under the PR's files? Main is at 785d3a7, four commits past the merge-base 9d89c6f: 3d17a97 (plugin-view), a9c5ea0 (fields), 8848ce4 (plugin-detail) and 785d3a7 (app-shell hook object picker, objectui#10585 / PR fix(app-shell): the hook object picker flags and says "publish" only from an answered roster (objectui#10585) #10644, the serial constraint PR 2 waits on). git diff --stat 9d89c6f refs/review/main restricted to the PR's six paths is empty, so nothing under them moved. The merge into the branch also touched none of them (above).

Implemented-by: claude/issue-10586-inspector-strings-i18n
Reviewed-by: session_01KUxVUa7e39aNjhkKi1gsoy

VERDICT: PASS

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.

2 participants