Skip to content

fix(plugin-form,fields,i18n): the record page's line-items panel and the grid field speak the user's language (objectui#11131) - #11143

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-11131-line-items-chrome
Sep 29, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-11131-line-items-chrome

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #11131

Clause-②: no

What

Under a zh session the record page's record:line_items panel (LineItemsPanel) rendered its default title Line Items and its Save / Saving… button in English. The line-items grid (GridField) did the same for its Add button (Add line), the list-mode empty text (No items yet — click “Add” to begin.) and the read-only empty state (No items). They now read the locale packs.

Only the defaults move. An authored title or add_label still wins, and an authored add_label is also the label the empty text names. English output is byte-identical to the literals it replaces, with and without an i18n provider.

Where each string goes

Site String Key
LineItemsPanel title Line Items form.lineItems.title new: form.masterDetail.lineItem is the singular row noun (Line item), a different string
LineItemsPanel button Save common.save reused
LineItemsPanel button, in flight Saving… detail.saving reused, the key MasterDetailForm's Save already reads
GridField Add button Add line fields.grid.addLine new: no pack key says "Add line" (detail.add is Add)
GridField list-mode empty text No items yet — click “{{label}}” to begin. fields.grid.noItemsAddHint new, one key with a {{label}} hole
its hole, when no add_label is authored Add detail.add reused
GridField read-only empty state No items fields.grid.noItems new: appDesigner.navNoPreviewItems has the same English but belongs to the app designer's nav preview, and list.noItems is No items found

The four new keys are in all ten packs.

Which translation path, and why

Panel: its own useLineItemsTranslation table, not useFormChromeTranslation. The dispatch asked for one of the two, not both. The panel already owned a createSafeTranslation hook (for the malformed-filter sentence), and formChrome.ts states the precedent for exactly that case: WizardForm, DrawerForm and ModalForm have hooks of their own and carry the rows they read. The panel's table now carries form.lineItems.title, common.save and detail.saving next to view.malformedFilter. Using useFormChromeTranslation instead would have meant editing formChrome.ts, which is outside the claim's declared file surface, to add the title row and move the malformed-filter row. The triage direction names useFormChromeTranslation, and this choice departs from that name. It is flagged in the report, not settled silently here.

Grid: useFieldTranslation with an inline defaultValue at each call site. This is the fields package's own path, which the grid already reads for its date cells. An inline default is what createSafeTranslation's provider-less fallback renders for a key its table lacks. pnpm check:i18n-keys holds each one to its en value, and its interpolation-parity rule holds the call site's arguments to the {{label}} hole. Rows in FIELD_DEFAULTS (useFieldTranslation.ts) would also have worked, but that file is outside the declared surface.

Tests

Pins, new:

  • packages/plugin-form/src/LineItemsPanel.i18nChrome-11131.test.tsx: under zh, the default title reads 明细行, the idle button 保存, the in-flight button 保存中… and the grid's Add button 添加行. A read-only panel over no rows shows 暂无条目. CONTROL: an authored title renders as written.
  • packages/fields/src/widgets/GridField.i18nChrome-11131.test.tsx: under zh, the Add button reads 添加行, the list-mode empty text reads 暂无条目,点击“添加”开始添加。 and the read-only empty state reads 暂无条目. CONTROL: an authored add_label is the button label and fills the empty text's hole.
  • Two .no-provider companions, one per package. With no provider mounted they pin the English byte for byte: Line Items, Save, Add line, No items, No items yet — click “Add” to begin., and an authored label in the hole. Each is its own file because an I18nProvider mount registers react-i18next's module-global instance for the rest of its file.

An existing pin was updated: packages/i18n/src/__tests__/de-quote-pairing-3876.test.ts is a census of the de pack's „…“ spans. The new de noItemsAddHint quotes the label with a matched pair, so the count goes from 68 to 69 spans and the open / close identity from 68 / 68 / 0 to 69 / 69 / 0. Its comment names the key, following the file's own convention. This file is outside the claim's list. It moved because the de value adds a quoted span, not by choice.

Reverse verification, run with the fix committed: both source files were checked out from the base db11afd49, keeping the new keys and pins. An on-disk grep confirmed it (title key 1 → 0, 'Line Items' literal present, fields.grid.addLine 1 → 0). Run over the four pin files, the result was 7 failed | 5 passed (12), which was the predicted direction. All seven zh cases went red, the two CONTROLs included, since each also asserts pack chrome. All five provider-less cases stayed green because the English is unchanged. After restoring with git checkout HEAD --, the blob hashes matched HEAD and git diff HEAD was empty.

Gate ablation: I changed the grid's noItemsAddHint inline default by one word through ablation-replace.mjs (anchor 1 → 0). check:i18n-keys then went to exit 1 with [default-value-drift] fields.grid.noItemsAddHint. The file was restored and matched the HEAD blob.

At 370aaf443 (the final commit):

  • pnpm exec vitest run --maxWorkers=2 packages/i18n/ packages/plugin-form/src/LineItemsPanel packages/fields/src/widgets/GridField packages/app-shell/src/__tests__/defaults-maps-mirror-en-pack.test.tsx gave 96 files passed, 1410 tests passed, 13 skipped. This is a declared narrowing.
  • At 8cf648cfe I ran the whole of packages/plugin-form/, packages/fields/ and packages/i18n/ plus the mirror test: 444 files, 6466 tests passed and 2 failed. Both failures were the de census above, fixed in 5081cc63f. The only later commit adds the changeset, which no test reads.
  • pnpm --filter @object-ui/i18n type-check, then @object-ui/fields, then @object-ui/plugin-form, all exit 0. These ran after building plugin-form's dependency closure with turbo run build --filter='@object-ui/plugin-form^...' (11/11 tasks). Each package's tsconfig.test.json lists the new test files (--listFilesOnly).
  • pnpm exec eslint on the 17 changed .ts/.tsx files gave exit 0: 0 errors and 43 warnings, all on lines this diff does not touch. The file count comes from --format json, which reports all 17 as linted and none ignored. eslint.config.js configures no type-aware linting (no projectService or parserOptions.project), so this diff cannot move the verdict on an untouched file. The repo-wide pnpm lint belongs to CI.
  • Exit 0 for check:i18n-keys, check:i18n-drift (4 keys added, 0 en values changed), check:i18n-dead-keys (no new key among the candidates), check-changeset-presence, check:control-bytes, check:new-line-citations (0 new) and check:changeset-claims.

Acceptance notes

  • The rest of the line-items chrome is still English, in the same two files. This is out of scope here and goes in the report as a finding for the seat, with its reach measured by an uncommitted zh render probe. LineItemsPanel still has Loading…, Save the record first to add line items., This record’s line items have not been loaded., the Failed to load / save line items fallbacks and the no-child-object hint. GridField still has Total, Columns, Optional columns, Computed and the row actions' aria-label / title pairs. Under zh the probe saw Save the record first to add line items. and the Total footer in English. The row-action pairs disagree with each other (Duplicate row vs Duplicate line), so choosing their keys is not purely mechanical.
  • MasterDetailForm's collection heading (d.title falling back to a Line Items literal) is the same string. The probe measured it in English under zh too. That file is held by the claim on objectui#11132 (a different defect, the totals' currency sign), so it is not touched here. After this PR the fix can reuse form.lineItems.title.
  • The empty text's hole defaults to detail.add (Add), while the button defaults to Add line. That mismatch predates this PR and is kept byte-identical. Its only list-mode producer, MasterDetailForm's in-form collection, always passes add_label, so no rendered screen shows the two disagreeing.

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


Generated by Claude Code

…utton and the grid's Add / empty text read the catalogue (objectui#11131)

The record page's `record:line_items` panel rendered its default title
"Line Items" and its "Save" / "Saving…" button as English literals, and
the line-items grid (`GridField`) did the same for "Add line", the
list-mode empty text and the read-only "No items". They now resolve
through the panel's `createSafeTranslation` table and the fields
package's `useFieldTranslation`, reusing `common.save`, `detail.saving`
and `detail.add`. Four new keys land in all ten packs:
`form.lineItems.title`, `fields.grid.addLine`, `fields.grid.noItems` and
`fields.grid.noItemsAddHint`. An authored `title` / `add_label` still
wins, and English output is byte-identical to the literals it replaces.

Claude-Session: https://claude.ai/code/session_011p7ikEivgXefNDaE5S5Uec
Co-authored-by: Claude <noreply@anthropic.com>
…e in zh and without a provider (objectui#11131)

A zh render of the panel's default title, both Save button states, the
grid's Add label and both empty texts, with authored `title` and
`add_label` controls; and provider-less companions that hold the English
to the literals it replaced.

Claude-Session: https://claude.ai/code/session_011p7ikEivgXefNDaE5S5Uec
Co-authored-by: Claude <noreply@anthropic.com>
…„{{label}}“ span (objectui#11131)

`fields.grid.noItemsAddHint` quotes the Add button's label with a
matched German pair, so the census moves from 68 to 69 spans and the
open / close identity from 68 / 68 / 0 to 69 / 69 / 0.

Claude-Session: https://claude.ai/code/session_011p7ikEivgXefNDaE5S5Uec
Co-authored-by: Claude <noreply@anthropic.com>
…fields and i18n (objectui#11131)

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

Copy link
Copy Markdown
Contributor

changeset-claim-re-read

⚠️ 2 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/6625-retire-fieldmeta-decimals.md

  • names GridField.tsx → packages/fields/src/widgets/GridField.tsx — edited by this change

    buildFieldMeta computed decimals: overrides.decimals ?? meta?.decimals ?? meta?.scale on every call and the value reached nothing. Re-measured on this branch's base (efdc6c62): zero .decimals member reads across @object-ui/fields, @object-ui/i18n, @object-ui/components, @object-ui/core and plugin-dashboard itself — the only non-comment occurrence was the write being removed here. The positive control in the same query shape fires: .scale member reads hit NumberField.tsx, GridField.tsx and fields/src/index.tsx. So the zero is a finding, not a broken query. The overrides.decimals ?? head of that chain had already lost its only feeder when objectui#6425's ruling removed the authored read from ObjectDataTable.enrich(); RecordDetailDrawer, the only other buildFieldMeta caller, passes no overrides at all. Both halves retire together, so the key leaves in one move.

.changeset/6661-app-launcher-nav-menu-renderers.md

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

    Three new strings — the launcher's and the menu's accessible names, and the menu's empty state — are declared under console.nav in en.ts and its nine sibling packs. An inline defaultValue alone is not a fix: it renders English at one call site and leaves the string untranslatable everywhere (objectui#3517).

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 84b275c01 (merge-base with origin/main): 17 file(s) changed outside .changeset/, read against 1766 pending declaration(s) that publish a body (2372 pending in total). · run

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 330 chunks) 3113.2 KB 3149.4 KB
Main entry chunk (gzip) 149.5 KB 350 KB
Entry file index-CeFkybzJ.js —
Status PASS —

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


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.88KB 6.25KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.11KB 3.87KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.22KB 10.61KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.17KB 5.40KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.72KB 2.24KB
auth (SocialSignInButtons.js) 9.70KB 3.93KB
auth (UserMenu.js) 3.39KB 1.21KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.70KB 10.94KB
auth (createAuthenticatedFetch.js) 8.54KB 3.46KB
auth (index.js) 3.63KB 1.64KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 27.13KB 7.95KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 559.68KB 134.21KB
core (index.js) 9.94KB 3.94KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 227.99KB 63.22KB
fields (index.js) 261.28KB 66.40KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.24KB 2.27KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.40KB 12.91KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 34.35KB 9.18KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 39.32KB 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.58KB 4.90KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.25KB 2.17KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.33KB 3.07KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 16.01KB 3.93KB
plugin-calendar (index.js) 52.17KB 15.06KB
plugin-charts (index.js) 84.09KB 22.93KB
plugin-chatbot (index.js) 198.22KB 46.97KB
plugin-dashboard (index.js) 138.73KB 37.05KB
plugin-designer (index.js) 215.78KB 44.42KB
plugin-detail (index.js) 234.55KB 62.25KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 170.51KB 43.56KB
plugin-gantt (index.js) 171.88KB 42.61KB
plugin-grid (index.js) 229.83KB 63.15KB
plugin-kanban (index.js) 48.43KB 15.11KB
plugin-list (index.js) 115.82KB 28.67KB
plugin-map (index.js) 22.90KB 7.62KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 44.04KB 12.21KB
plugin-timeline (index.js) 31.93KB 9.33KB
plugin-tree (index.js) 11.21KB 3.89KB
plugin-view (index.js) 90.43KB 22.76KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.81KB 3.58KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 119.55KB 39.23KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.50KB 2.06KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.78KB 2.09KB
sdui-parser (codegen.js) 9.45KB 3.76KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 6.17KB 2.73KB
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.84KB 1.90KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 22.61KB 7.40KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 3.83KB 1.49KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 3.19KB 1.62KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.26KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 2.52KB 1.31KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 19.30KB 6.99KB
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: 370aaf443050835b9c8fc5f49151f512ac687d02
Local-runs: none

Inputs: card objectui#11131 (body, triage 5896632949, claim 5897282582, os-dev-report 5898356660), PR objectui#11143 (body, 18-file list, net diff against main), the head's check-runs (filter=latest), and context reads of LineItemsPanel.tsx, GridField.tsx, MasterDetailForm.tsx, formChrome.ts, useFieldTranslation.ts, useSafeTranslation.ts, fallbackInterpolation.ts, en.ts, zh.ts, check-i18n-call-site-keys.mjs, ci.yml, .changeset/config.json at the head via contents/PATH?ref=SHA. Nothing built, run or re-run.

Gates on the head. Runs were still in progress when this review began; the poll waited until every run had completed (the last at about 2026-09-29T20:49Z) before judging. All 43 check-runs on 370aaf443 are completed: 40 success, 3 skipped (Test (coverage), the coverage shard matrix and dependabot, none of which runs on a PR head), 0 failed. The successes include Type Check (the job that also runs check:i18n-keys, check:i18n-drift and check:i18n-designer-parity), Lint, Test shards 1–8 (the four new pin files, the bumped de census and defaults-maps-mirror-en-pack live there), Test (dist pins), Build & E2E, Build Docs, Changeset Declaration, Changeset Fixed Group Check, Changeset Bump Policy, Changeset Claim Re-read, Changeset Overwrite Report, Control Byte Scan, Line Citation Gate, Governed Surface Queue Guard and Spec Main Shape Gate. check:i18n-dead-keys has no CI job (a report-only sweep, per ci.yml); each of the four new keys is read by a call site in this diff.

① Derived judgments

Accept-set: nothing moves. No schema, prop, type, zod arm or accepted value changes; the diff touches two components' default strings, ten locale packs, one census pin, four new pin files and one changeset. Public surface: four new pack keys appear in @object-ui/i18n (additive), and the fields / plugin-form components read them. Every judgment below is named.

  1. New keys, four, in all ten packs, holes kept, en byte-equal — right. form.lineItems.title = Line Items, fields.grid.addLine = Add line, fields.grid.noItems = No items, fields.grid.noItemsAddHint = No items yet — click “{{label}}” to begin. Each en value is byte-equal to the literal it replaces at the base (schema.title || 'Line Items', cfg.add_label || 'Add line', the JSX text No items, and No items yet — click “{cfg.add_label || 'Add'}” to begin. whose hole is now add_label || detail.add, detail.add being Add). ar, de, es, fr, ja, ko, pt, ru, zh each add the same four keys under the same paths (form.lineItems, fields.grid), each noItemsAddHint keeps the {{label}} hole with locale-native quotes. None of the four had an existing same-meaning key: the en pack at the head holds no other Line Items value (form.masterDetail.lineItem is the singular Line item), no Add line (detail.add is Add, so reusing it would have changed the English), and No items only as appDesigner.navNoPreviewItems, which belongs to the app designer's nav preview and is translated for it (zh 无项目, not the grid's 暂无条目), while list.noItems is No items found. Adding rather than reusing is what the triage's "only where none means the same thing" permits.
  2. Reused keys — right, each the same meaning. common.save (Save / zh 保存) for the idle button; detail.saving (Saving… / zh 保存中…) for the in-flight button, the key MasterDetailForm already reads for its own in-flight Save; detail.add (Add / zh 添加) for the empty text's hole when no add_label is authored, the literal 'Add' it replaces. All three sit under the blocks their names say (common, detail) at the head.
  3. Hook choice: the panel extends its own useLineItemsTranslation rather than useFormChromeTranslation — honours the triage in substance, not a breach. The triage's direction is "the objectui#11071 pattern, reusing existing keys"; the card's own direction wrote it as "createSafeTranslation / useFormChromeTranslation in plugin-form", and the claim on the card allowed either the panel's table or useFormChromeTranslation. The pattern's contract is: pack lookup under a provider, a createSafeTranslation table held byte-equal to en for the provider-less path, and existing keys reused. The panel's hook is exactly such a table (check-i18n-call-site-keys.mjs holds every createSafeTranslation row to en, objectui#7567), formChrome.ts itself states the precedent that a component owning its own hook carries its own rows (WizardForm / DrawerForm / ModalForm) and accepts the resulting duplicate rows because the gate keeps them from drifting, and formChrome.ts was outside the claim's file surface. The observable result — zh renders of title and both button states, English byte-identical with and without a provider, common.save / detail.saving reused — is the one the triage asked for. The cost is two more duplicated rows (common.save, detail.saving) inside one package; the dev named the mechanical switch if the seat wants the name honoured literally. Flagged in the report and the PR, not settled silently. Right.
  4. GridField serving English through inline defaultValues gated by check:i18n-keys rather than FIELD_DEFAULTS rows — right. createSafeTranslation's provider-less path resolves defaults[key] || inline defaultValue || key and interpolates through interpolateFallback ({{label}} filled, defaultValue skipped), so the three inline defaults render byte-identical English with no provider, including the hole (the .no-provider pin asserts No items yet — click “New row” to begin.). Under a provider i18next answers from the pack. check:i18n-keys holds each inline default to its en value (default-value-drift) and its arguments to the en holes (interpolation-parity), and that gate runs in the Type Check job on this head. useFieldTranslation.ts was outside the declared surface; a table row would have been equivalent, not better. Right.
  5. de census pin bump (de-quote-pairing-3876, 68 → 69, 68/68/0 → 69/69/0) — right, outside the surface by necessity. The de noItemsAddHint value Noch keine Einträge – klicken Sie auf „{{label}}“, um zu beginnen. adds exactly one matched „…“ pair; the other three de rows carry no quotes. An absolute census must move by one, the file's own convention is a bump with a comment naming the key (the same comments record objectui#7699 and objectui#11000), and the comment is there. Forced by the diff, not a choice.
  6. English byte-identical with and without a provider — right. Provider-less: the panel's table rows and the grid's inline defaults are the old literals byte for byte (Line Items, Save, Saving…, Add line, No items, No items yet — click “Add” to begin.), pinned by the two .no-provider files. With a provider: the en pack carries the same bytes (check:i18n-drift: 4 keys added, 0 en values changed). Authored title / add_label still win at every site (schema.title || t(...), cfg.add_label || t(...), and label: cfg.add_label || t('detail.add')), pinned by both CONTROL cases.
  7. Pins match the card's list — right. Panel: zh title 明细行, idle 保存, in-flight 保存中… (through a held batchTransaction), the grid's 添加行, a read-only panel's 暂无条目, CONTROL authored title. Grid: zh 添加行, list-mode 暂无条目,点击“添加”开始添加。, read-only 暂无条目, CONTROL authored add_label as both the button and the hole. t is in scope at both panel sites (line 182 inside LineItemsPanel, which spans 180–781 at the head).

② Semver level

Clause-②: no — confirmed. The changeset .changeset/11131-line-items-chrome.md declares @object-ui/fields: patch, @object-ui/i18n: patch, @object-ui/plugin-form: patch. Those are the three released packages the diff touches (check-changeset-presence: 17 source files of 3 released packages, 1 changeset), all three sit in the one fixed group in .changeset/config.json, and patch is the level this family used for the same shape of change (11039-form-chrome-i18n.md, 11071-form-chrome-remainder.md: pack keys added, defaults moved, patch). No accept-set, type, prop or schema moves, so no minor is owed. The changeset's claims (four new keys, three reused, defaults only, en byte-identical) match the diff. Changeset Declaration, Fixed Group, Bump Policy, Claim Re-read and Overwrite Report all passed on the head.

③ Boundary flags

open_questions: none declared. Deviations, each answered:

  • Hook choice (useLineItemsTranslation not useFormChromeTranslation): honours the direction in substance, judged at ① 3. No escalation needed; if the seat wants the triage's name honoured literally, the dev's stated switch is a follow-up, not a blocker.
  • GridField inline defaults instead of FIELD_DEFAULTS rows: judged at ① 4, right.
  • de-quote-pairing-3876 outside the surface: judged at ① 5, forced by the diff, right.
  • Commit trailers and PR footer in the repo's model-free form rather than the harness reminder's: the repo's AGENTS.md form governs in this repo; not a contract matter.
  • The un-narrowed whole-package run at 8cf648cfe holding the lock 15m37s: a process note, no bearing on the head.

Out-of-scope notes, each answered at the head:

  1. The rest of LineItemsPanel / GridField chrome still English under zh — real at the head, same family. Read at 370aaf443: LineItemsPanel.tsx still carries Loading… (line 747), Save the record first to add line items. (750), This record’s line items have not been loaded. (762), the Failed to load line items / Failed to save line items fallbacks (528, 661) and the no-child-object hint (743). GridField.tsx still carries Columns (872), Optional columns (881), Total (991, 1407), Computed (1074) and the row-action aria-label / title pairs Drag to reorder (1292–1293), Open row / Open full form (1350–1351), Duplicate row / Duplicate line (1368–1369), Remove row (1384). The dev's zh probe measured Save the record first to add line items. and the Total footer as public-door reach. Same family (form chrome left English under zh, objectui#10999 → finding(plugin-form,console): more form chrome is hard-coded English — the Created/Saved success toast, the Thanks! title, Loading form... and Error loading form — the objectui#10999 family beyond WizardForm's footer #11039 → finding(console,plugin-form): form chrome remainder after objectui#11039 — the public form page's Submit, Submitting…, Uploading…, Redirecting… and Required: … are English (measured), plus MasterDetailForm line-item chrome #11071 → finding(plugin-form,fields): the record page's line-items panel reads "Line Items", "Save" / "Saving…" and the grid's "Add line" / "No items yet — click …" in English under zh-CN, the form-chrome family's remainder after objectui#11071 #11131). The card named its sites, so leaving these was correct, and the disagreeing aria/title pairs make the key choice non-mechanical.
  2. MasterDetailForm's section heading {d.title || 'Line Items'} — real at the head. Line 634 of MasterDetailForm.tsx at 370aaf443, inside the component whose t is useFormChromeTranslation (line 566). The file is held by the claim on objectui#11132 (named disjoint by file in this card's claim), so not touching it here was the right call. After this PR the fix is one token: t('form.lineItems.title'), a key useFormChromeTranslation's table would need as a row.
  3. The list-mode empty-text hole defaulting to detail.add (Add) while the button defaults to Add line — not a defect at the head; a latent default inconsistency with no rendered reach. It predates the PR byte for byte. GridField at the head renders list mode only when displayMode === 'list' and not readonly; its sole product producer is MasterDetailForm, which sets displayMode to list exactly when inlineMode === 'form' and in that same branch always passes add_label (d.addLabel || t('detail.add')), so the button and the hint always name the same label on every rendered screen. The two defaults only disagree for a direct list-mode embed with no add_label, which nothing in the product does. Keeping it byte-identical was right for a Clause-②: no change; aligning the hole to fields.grid.addLine would be a one-line follow-up when the grid's chrome is next touched.

(1) and (2) belong in one family remainder card. Same defect class, same two-file pattern, and (2) reuses this PR's form.lineItems.title; the card should carry a serial constraint on objectui#11132's PR for MasterDetailForm.tsx and can note (3) as an optional alignment. No carrier exists yet; this record does not file it.

Implemented-by: claude/issue-11131-line-items-chrome
Reviewed-by: session_011p7ikEivgXefNDaE5S5Uec

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 29, 2026 21:00
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 29, 2026
Merged via the queue into main with commit 385ebc5 Sep 29, 2026
45 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-11131-line-items-chrome branch September 29, 2026 21:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment