…labels
The invoice print page's `record:line_items` columns were identity-only
`{ name }` entries. That block draws a column exactly as declared and does
not fill one from the child field, so the printed table was headed
`description` / `quantity` / `unit_price` / `amount`. Each column now
declares the invoice line field's own label.
A column `label` is a plain string and no translation bundle key reaches a
line-items column, so the headings read in English in every locale.
The new pin holds each column to its child field's label, never the name.
Claude-Session: https://claude.ai/code/session_018VqJPGA1NDNSCZsnecsChP
Co-authored-by: Claude <noreply@anthropic.com>
Fixes #23985
Clause-②: no. An example app's column headings change. No export, type or accept set moves.
What
The invoice print page (
showcase_invoice_document) heads its line-items table with labels now, not raw field names. This is the card's acceptance option 1, Showcase. No contract changed.examples/app-showcase/src/ui/pages/invoice-document.page.ts: the fourrecord:line_itemscolumns each declare alabel, taken from the invoice line field's own label ininvoice.object.ts:descriptiondescriptionDescriptionquantityquantityQtyunit_priceunit_priceUnit PriceamountamountAmountQtyis the field's own label, and it is also the header the master-detail grid hydrates for the same field. A short comment above the array says why the labels are declared here.examples/app-showcase/test/invoice-document-line-items.test.ts(new): the pin.Why the page, not the renderer
RecordLineItemsProps.columns(packages/spec/src/ui/component.zod.ts) reads: "Unlike the master-detail carriers, this block does NOT hydrate a column from the child object's field: declarelabel,typeandoptionsyourself — an identity-only{ name }column is a text cell headed by its name." The renderer does what that says. At the pinf8267df93a, objectui'sLineItemsPanelpassesschema.columnsthroughapplyColumnPermissionsonly, andGridFieldheads each column withc.label || c.name. The page was the off-contract part, so the page is what changed.Can the zh-CN print translate these headings? No.
On a zh-CN print the four headings read in English:
Description,Qty,Unit Price,Amount. Before this PR they read as raw field names, which are also English. Measured onorigin/mainc1c0a90942:labelisInlineGridColumnSchema.label, which isz.string().optional()(packages/spec/src/data/field.zod.ts). It does not take the{ en, 'zh-CN' }map thatI18nLabelSchematakes.record:detailssections go throughobjects.showcase_invoice._sections.invoice/.amounts, and the page label throughpages.showcase_invoice_document.label. A page component's copy goes throughpages.NAME.components.ID.KEY.translatePage(packages/spec/src/system/i18n-resolver.ts) overlays onlyPAGE_COMPONENT_COPY_KEYS(title,description,label,placeholder,emptyText) ontoproperties.KEY. No key reachesproperties.columns[i].label, and the extractor (packages/cli/src/utils/i18n-extract.ts) does not walkcolumnseither.So the labels are literal English, as the dispatch directs when neither route exists. No translation mechanism was added. The translation bundle is untouched.
The zh-CN sheet is therefore mixed: translated section headings (发票, 金额), bilingual header and footer text, and English column headings. Closing that gap takes a contract change, which is the card's option 2. That is spec-lane work and is not built here; see Acceptance notes.
Tests
All runs are on head
0de6f72ee4.origin/mainis stillc1c0a90942, so the pre-PR merge was a no-op.pnpm --filter @objectstack/example-showcase exec vitest run --maxWorkers=2 test/invoice-document-line-items.test.tsgaveTest Files 1 passed (1),Tests 3 passed (3).pnpm --filter @objectstack/example-showcase testgaveTest Files 35 passed (35),Tests 427 passed (427).pnpm --filter @objectstack/example-showcase typecheck(tsc --noEmit) exited 0.tsc --listFilesincludes the new test file (count 1).turbo run build --filter='@objectstack/example-showcase^...' --concurrency=2, 62 of 62 tasks successful.The pin's three cases:
record:line_itemsblock. Its child object isshowcase_invoice_line, and its columns aredescription,quantity,unit_price,amount.labelequals its child field's ownlabel, and never equals the field name. The expected value comes fromInvoiceLine.fieldsrather than a hard-coded string, so the print stays in step with the field definitions.RecordLineItemsProps.safeParse, and the parsed output equals the authored properties, solabelis an accepted key and is not stripped.Ablation, one leg, through
scripts/ablation-replace.mjsin wrap mode on the committed fix. The four-linecolumnsarray was put back to today's single line of identity-only columns:1e5578e84b55→c9acb7dcda24.Tests 1 failed | 2 passed (3). Case 2 failed withAssertionError: column 'description' must declare its heading: expected undefined to be 'Description'.1e5578e84b55), andgit diff HEADis empty. This was re-checked independently withgit hash-objectagainstHEAD:and a cleangit status --porcelain.The direction was red, as expected. The page is imported from source by a relative path, so no
dist/sat between the mutation and the test.Gates
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 42 commands from this change set, on the treeobjectstack-ai/objectstackat0de6f72ee4. All 42 were run, and each exit was recorded before any pipe.--ranreconciled them: "42 derived famil(ies) accounted for — 42 run, 0 NOT-MEASURED".pnpm check:dual-build-cjs-loadsexited 3 on its first run,PREREQUISITE NOT MET. It reads every package'sdist/, and nine packages outside the showcase closure were not built yet. That is not a measurement.check:type-check-debt, later in the same sequence, builds every package itself. The real command was then rerun and exited 0: "111 published require entry point(s) across 69 package(s) load".pnpm check:i18n-coveragereportsOK (14 config(s), 621 baselined untranslated string(s), none new). That confirms the literal column labels add no keys to the ratchet.check:dts-closureandcheck:sourcemap-no-sources-contentfirst swept only the 62 built packages. After the full build they were rerun and exited 0 again, sweeping 75 and 72 packages.CI families are left to this PR's check-runs.
No changeset
@objectstack/example-showcaseis"private": trueand publishes nothing, so this PR carries theskip-changesetlabel.Check Changesetreads that label live; it does not read this body.Acceptance notes
record:line_itemswould hydrate an identity-only column from the child field the way the master-detail carriers do, and an objectui renderer change would follow. It is spec-lane work and out of this card's scope (triage6221505080). Nothing was filed. The report puts it to the seat as an open question.GridFieldat the pin, a cell that is notlookup,fileor temporal printsString(value). Sounit_priceandamountprint as bare numbers. Declaringtype: 'currency'would right-align them but would not format them. These columns declare notype, so they are also left-aligned. This is noted only. It was not reproduced, and no card was filed. Carrier: none.6208035645). This PR changes only declared metadata that the renderer draws verbatim, and the pin holds that metadata.Generated by Claude Code