Skip to content

fix(i18n,a11y): the enumerated literal aria-label names read each package's translation hook, and a census pins the family (objectui#12145) - #12149

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-12145-aria-label-i18n
Oct 11, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-12145-aria-label-i18n

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #12145

Clause-②: yes

The card's enumerated aria-label sites read their accessible names through their own package's translation hook, so a screen reader under zh (or any non-English pack) hears the viewer's language. Eight keys are added to all ten @object-ui/i18n packs with real translations; every other name reuses a key that already holds the same words. The family is pinned by a census test in the ordinary suite (not a CI gate), beside per-package zh role-and-name pins.

Clause-②: yes is the claim's reading (comment 6110243674): the added keys widen the as const en pack, and so the published TranslationKeys. Confirmed on the built declarations at HEAD ebfc833: packages/i18n/dist/locales/en.d.ts declares all eight keys, and dist/index.d.ts re-exports TranslationKeys (typeof en).

Every enumerated site, and what happened to it

Site (the card's 31) Disposition Key
apps/console AppDocsIndex, BookPage, DocPage, DocsIndex, DocsSlug: "Loading documentation" (5) read through useObjectTranslation help.loadingDocumentation (new)
app-shell RecordFormPage: "Breadcrumb" read through the page's t common.breadcrumb (new)
components custom/spinner.tsx: "Loading" read through a createSafeTranslation hook; a caller's aria-label still wins common.loading (reused; the English name is now "Loading…")
components debug/DebugPanel.tsx: "Developer Debug Panel", "Close debug panel" exempt: a developer tool, not product UI. No objectui surface mounts it (a host opts in behind ?__debug / Ctrl+Shift+D), and its whole chrome is English developer copy, so translating two names would leave it half in one language none
components renderers/complex/data-table.tsx: "Row actions" read through useTableTranslation table.rowActions (new)
components ui/pagination.tsx: "Go to previous page", "Go to next page" the No-Touch primitive is not edited; its one shipped call site, the pagination renderer (renderers/basic/pagination.tsx), passes the names, which the primitive spreads after its defaults table.previousPage / table.nextPage (reused, PR objectui#12142)
components ui/sidebar.tsx: "Toggle Sidebar" (SidebarRail) exempt: the No-Touch primitive is not edited, and no objectui surface renders the rail. Its only writer is the CLI's app generator, whose generated app mounts no I18nProvider. A host names it through aria-label / title (common.toggleSidebar for a host)
fields ImageCropperDialog: "Image crop preview" useFieldTranslation fields.image.cropPreview (new)
fields PercentField: "Percentage" useFieldTranslation fields.percent.slider (new)
fields RecordPickerDialog: "Records" useFieldTranslation console.commandPalette.records (reused)
plugin-chatbot elements/message.tsx: "Previous branch", "Next branch", "Remove attachment" x2 exempt: vendored from vercel/ai-elements (the header says wrap, do not edit), and no objectui surface renders MessageBranchPrevious, MessageBranchNext or MessageAttachment; they are only re-exported as AIElements. The branch buttons spread props last none
plugin-chatbot elements/prompt-input.tsx: "Remove attachment" (PromptInputAttachment) rendered by ChatbotEnhanced, and props reach the chip, not the button. An optional removeLabel prop is added to the vendored element (the objectui#7254 statusLabels precedent: additive, the upstream English stays when omitted); ChatbotEnhanced passes the pack's name detail.removeAttachment (reused)
plugin-chatbot elements/prompt-input.tsx: "Submit" (PromptInputSubmit) the vendored default stays (exempt); its one call site, ChatbotEnhanced, already overrode it with L.submit, whose fallback WAS the English literal. That producer now reads the pack, for the stop state too console.ai.submit / console.ai.stopResponse (reused: the console already passes them)
plugin-designer HistoryPanel: "Undo", "Redo" useDesignerTranslation designer.undo / designer.redo (reused)
plugin-designer Minimap: "Canvas minimap" useDesignerTranslation designer.canvasMinimap (new)
plugin-designer VersionHistory: "Version history" useDesignerTranslation designer.versionHistory (new)
plugin-designer DashboardDesignPage: "Go back" useDesignerTranslation detail.goBack (reused)
plugin-detail record-alert.tsx: "Dismiss" the renderer's t home.recoveryReminder.dismiss (reused)
plugin-detail record-path.tsx: "Alternative terminal stages" not a site: the card's grep matched a comment that quotes the objectui#5956 repair. The comment is reworded so the card's command no longer matches it; no attribute existed none
plugin-grid BulkActionBar: "Bulk actions" the bar's t grid.bulkActions (new)
plugin-list TabBar (TabBarSelect): "Switch view" useObjectTranslation topbar.switchView (reused: the app header's view switcher reads it)

"Reused" follows triage's rule, one key per name: where the English name already had a key, the site borrows it rather than add a second (the "Dismiss" borrow picks the banner-dismiss key whose ten translations fit a dismissible alert). The card's re-check command, run on this branch, returns exactly the ten exempt literals above and nothing else.

The census: packages/i18n/src/__tests__/ariaLabelLiteralCensus-12145.test.ts

Home chosen by precedent: machineLocaleCensus-9909.test.ts in the same directory is the repo's existing one-test census over every package's shipped sources, with comment masking through scripts/js-comment-mask.mjs and declared exemptions cited by content. This census copies that shape.

  • Population: every source file under a src/ tree of a packages/* or apps/* workspace, minus tests, stories and tooling. Walked, not globbed, plus a guard that the files directly under a src/ root are in it (see the finding below).
  • Shapes judged: aria-label="…", aria-label={'…'} / {"…"} (the widening triage asked for), a braced template that interpolates nothing, and the 'aria-label': '…' object key, when the value carries a letter (\p{L}, so a name in any script counts).
  • Verdict: each literal is declared by file and name, counted exactly. A new literal is red, and so is a declared one that disappears, so its entry leaves in the same commit. The ten exempt sites carry their reasons; everything else literal is a frozen BACKLOG (next section).
  • Stated blind spots (in the test header): interpolating templates, a literal inside a larger expression, title= / placeholder= / sr-only / renamed props, and JSX text that the comment masker reads as a comment opener.

Readings (the census as committed, run on each tree):

Tree Literal sites found Undeclared Verdict
BASE 158c75d site files, packs and tests at HEAD (Leg A below) 117 20, exactly the card's translated sites red
HEAD ebfc833 97 (10 exempt + 87 backlog) 0 green

Finding: the card's re-check pathspec never read a file directly under src/

git grep … -- 'packages/*/src/**/*.tsx' has no :(glob) magic, so its pattern needs a / after src/: no file directly under a src/ root was ever read (the control: aria-label="Undo" in plugin-designer/src/DataModelDesigner.tsx is not returned, while the same string in src/components/HistoryPanel.tsx is). The same regex with :(glob) returned 114 sites on BASE, not 31. This PR translates the card's enumeration, per the claim's file surface; the 87 literal sites it did not enumerate (23 files, 11 packages, plugin-designer the bulk of them) are frozen in the census's BACKLOG, so none can grow and each must leave the list in the commit that translates it. They are reported to the seat for slicing; auth, layout and plugin-map among them do not depend on @object-ui/i18n today.

Pins: one zh role-and-name query per package, each read against the pack

  • apps/console/src/pages/docsLoadingName-12145.test.tsx: the docs index spinner under en and zh. The spinner is a lucide svg, which testing-library's role table gives no implicit role, so this one is found by its accessible name (getByLabelText).
  • packages/components/src/__tests__/ariaLabelNames.zh-12145.test.tsx: Spinner (status), the data table's row menu trigger (button), the pagination renderer's links (link), with no provider (en) and under zh, and the primitive's English defaults absent under zh.
  • packages/fields/src/__tests__/ariaLabelNames.zh-12145.test.tsx: the percent slider (slider).
  • packages/plugin-chatbot/src/__tests__/ChatbotEnhanced.composerNames.zh-12145.test.tsx: the send button with no host labels, and a host label still winning.
  • packages/plugin-designer/src/__tests__/ariaLabelNames.zh-12145.test.tsx: undo / redo (button), minimap (img), version history (region).
  • packages/plugin-grid/src/__tests__/BulkActionBar.regionName.zh-12145.test.tsx: the bar (region).
  • packages/plugin-list/src/components/__tests__/TabBarSelect.triggerName.zh-12145.test.tsx: the trigger (button).
  • packages/plugin-detail/src/renderers/__tests__/record-alert.test.tsx: one added case, the dismiss button (button).
  • packages/app-shell/src/views/RecordFormPage.listLinkPlural-11733.test.tsx: its breadcrumb lookup read the English literal; it now reads common.breadcrumb for the language under test, so its zh-CN case is the package's zh pin (navigation).

Each zh case first asserts the zh and en values differ, or it would pin nothing.

Evidence (HEAD ebfc833, one locked run, every leg restored)

Leg Mutation (landing proven on disk) Result
0, HEAD none census + 9 pin files: 10 files, 48 passed
A, BASE sites the 24 site files replaced by their BASE blobs, packs and tests at HEAD; literal names on disk 0 to 20 10 files failed, 13 failed / 35 passed: the census lists exactly the 20 translated sites as declared 0, found 1; every pin's zh case red (Unable to find role="region" and name "批量操作" and the like); the components en case red ("Loading…")
B, one site BulkActionBar's t(…) back to aria-label={'Bulk actions'} (the braced shape) via objectstack's scripts/ablation-replace.mjs: anchor 1 to 0, blob c9c2d24 to 05ef07e census red (BulkActionBar.tsx · Bulk actions — declared 0, found 1), grid zh pin red
C, backlog shrink ListView's backlog aria-label="Bulk actions" removed (anchor 1 to 0, blob db03dae to 11c9b6a), its entry left census red (ListView.tsx · Bulk actions — declared 1, found 0)

After Leg A, git diff HEAD was empty and the status clean; Legs B and C each restored to the HEAD blob with git diff HEAD empty; the run ended with the tree clean at ebfc833.

Gates (all at HEAD ebfc833)

  • Build: the console's dependency closure, turbo run build --filter=@object-ui/console^... --concurrency=2: 34 of 34 tasks; then the console itself: 35 of 35.
  • Type-check (type-check, each script name echoed): i18n, components, fields, plugin-grid, plugin-list, plugin-chatbot, plugin-designer, plugin-detail, app-shell, console: all exit 0. Each new or edited test file is in its package's type-checked program (tsconfig.test.json, or the console's tsconfig.json).
  • Tests, repo-root form: the census and pins above; packages/i18n/ plus scripts/__tests__/check-i18n-call-site-keys.test.ts: 84 files passed; the 130 existing test files that import or query a changed module, in two runs: 69 and 61 files passed.
  • i18n family: check:i18n-keys, check:i18n-drift (8 keys added, 0 en values changed), check:i18n-dead-keys (report; none of the eight, nor any reused key, listed), check:i18n-designer-parity, check:eager-locale-catalogues (only en eager), check:eager-closure: all exit 0.
  • Others, all exit 0: check-changeset-presence.mjs (44 source files of 10 released packages, 1 changeset), changeset:check, check:changeset-claims, check:pending-changeset-literals, check:control-bytes, check:new-line-citations (0 new), check:phantom-deps, check:test-path-roots, check:vi-mock-specifiers, check:vi-mock-inherit, check:vi-mock-override-shape; governed-surface guard: NOT GOVERNED.
  • Lint on the 34 touched source and test files: 0 errors; 147 warnings, none on a changed line.
  • main has not moved since BASE 158c75d, so no merge was needed. No docs page quotes these names, so none changed.

Acceptance notes (outside the card, not edited here; handed to the seat)

  • Interpolating templates carry English words the census cannot judge by design, for example aria-label={`Dismiss ${notification.title}`} in the three notification surfaces, Remove ${name} in TagsField / SelectionTray, Copy ${label ?? 'value'} in two console developer pages. 30 such sites on BASE.
  • Visible English beside a translated name: the Shadcn PaginationPrevious / PaginationNext render visible "Previous" / "Next" spans, and SidebarTrigger (rendered bare by the header-bar renderer) names itself through an English sr-only span. Code read only.
  • ChatbotEnhanced's uploadFiles fallback is still the English literal, the same shape as the submit fallback fixed here; the console passes its own label.
  • The docs pages' visible headings and empty states are English copy; out of this card, which is about accessible names.

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


Generated by Claude Code

…lation hook (objectui#12145)

The card's enumerated aria-label sites read their names from the locale
packs instead of English literals. Eight keys are added to all ten packs;
every other name reuses an existing key (one key per name).

Claude-Session: https://claude.ai/code/session_016djJF12Qt14ejKR5Vjt4sK
Co-authored-by: Claude <noreply@anthropic.com>
…e pins (objectui#12145)

The census walks every packages/* and apps/* src tree and fails on any
literal accessible name it does not declare; the card's stated exceptions
are declared with reasons, and the sites its re-check pathspec never
reached are frozen as a backlog. Each touched package gains a zh
role-and-name pin read against the pack.

Claude-Session: https://claude.ai/code/session_016djJF12Qt14ejKR5Vjt4sK
Co-authored-by: Claude <noreply@anthropic.com>
…ion (objectui#12145)

The source-count floors were guesses above the real shipped population;
they are now floors under it, plus a guard that the files directly under a
src/ root, which the card's pathspec never read, are in the population.

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

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 290 chunks) 3175.7 KB 3204.6 KB
Main entry chunk (gzip) 74.2 KB 350 KB
Entry file index-B0ZyD57p.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) 20.05KB 7.41KB
app-shell (runtime-config.js) 22.59KB 7.89KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.11KB 3.87KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.22KB 10.61KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.40KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.72KB 2.24KB
auth (SocialSignInButtons.js) 9.70KB 3.93KB
auth (UserMenu.js) 3.39KB 1.21KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 41.19KB 11.12KB
auth (createAuthenticatedFetch.js) 8.54KB 3.46KB
auth (index.js) 3.63KB 1.64KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 27.11KB 7.97KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.28KB 2.60KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.50KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 587.77KB 141.53KB
core (index.js) 10.18KB 4.04KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 240.39KB 67.09KB
fields (index.js) 275.30KB 70.07KB
i18n (LocalizationContext.js) 2.92KB 1.42KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.52KB 2.39KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.35KB 12.88KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 36.87KB 9.88KB
i18n (useSafeTranslation.js) 7.14KB 2.92KB
layout (index.js) 40.26KB 11.71KB
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) 15.35KB 5.51KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.82KB 2.38KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.33KB 3.07KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 16.04KB 3.92KB
plugin-calendar (index.js) 54.31KB 15.86KB
plugin-charts (index.js) 84.72KB 23.27KB
plugin-chatbot (index.js) 201.72KB 48.07KB
plugin-dashboard (index.js) 144.20KB 38.95KB
plugin-designer (index.js) 233.77KB 50.02KB
plugin-detail (index.js) 250.27KB 66.04KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 177.80KB 46.04KB
plugin-gantt (index.js) 179.37KB 45.17KB
plugin-grid (index.js) 255.19KB 70.94KB
plugin-kanban (index.js) 53.23KB 16.71KB
plugin-list (index.js) 122.82KB 31.24KB
plugin-map (index.js) 27.24KB 9.03KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 44.12KB 12.29KB
plugin-timeline (index.js) 39.21KB 11.85KB
plugin-tree (index.js) 15.07KB 5.33KB
plugin-view (index.js) 92.11KB 23.27KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 12.07KB 3.68KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 120.63KB 39.56KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.50KB 2.06KB
react (schema-input.js) 4.31KB 2.07KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.50KB 1.99KB
sdui-parser (codegen.js) 9.45KB 3.76KB
sdui-parser (dashboard-widget-options.js) 3.07KB 1.30KB
sdui-parser (index.js) 7.30KB 3.12KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.84KB 1.90KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 23.87KB 7.83KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 4.12KB 1.61KB
types (authoring-nodes.js) 0.20KB 0.19KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (cloud.js) 0.20KB 0.18KB
types (complex.js) 4.44KB 2.07KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (dashboard-widget-layout.js) 2.06KB 0.96KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 1.13KB 0.65KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 5.78KB 2.70KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (node-slots.js) 7.18KB 2.34KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 2.52KB 1.31KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 4.99KB 1.96KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 19.93KB 7.26KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.48KB 3.50KB
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: ebfc8330337770d28901344d222430e6d53f9433
Local-runs: none

① Derived judgments

Readings: card #12145 (body, triage 6109811077, claim 6110243674, dev report 6111181431); PR #12149 (body, the 45-file list, the net diff against main 158c75d); the 42 check-runs on the head; files at the head through the contents API. The "no call site" claims were read with git grep against the head commit's tree in the sibling repo's object store (a read of objects, no checkout, no run). Nothing built, run or re-run.

Every public-surface and accept-set change the diff implies, judged:

  1. @object-ui/i18n: the as const en pack gains eight keys (help.loadingDocumentation, common.breadcrumb, table.rowActions, fields.image.cropPreview, fields.percent.slider, designer.canvasMinimap, designer.versionHistory, grid.bulkActions). locales/en.ts ends with export type TranslationKeys = typeof en and index.ts:41 re-exports it, so the published type widens. All ten packs carry the eight with translated values, not English copies. RIGHT, a widening.
  2. @object-ui/plugin-chatbot: PromptInputAttachmentProps gains an optional removeLabel whose default is the upstream English name; the element is public through export * as AIElements from './elements' and the elements barrel. RIGHT, a widening. ChatbotEnhanced's L.submit / L.stopResponse fallbacks now read console.ai.submit / console.ai.stopResponse through the component's existing tt = useSafeTranslate(); a host labels value still wins (pinned). The en values are Submit and Stop response, the former literals, so the English name is unchanged. RIGHT. L.removeAttachment reads detail.removeAttachment only; there is no labels.removeAttachment arm, so a host cannot override that one name the way it can the others. An acceptable asymmetry, recorded as an acceptance note, not a breach.
  3. @object-ui/components: Spinner names its status region common.loading through a createSafeTranslation hook whose test key is in its one-row defaults map; aria-label sits before {...props}, so a caller's name still wins. The en value is Loading… where the literal was Loading: the package's one English-name change, and the changeset names it. The pagination renderer passes aria-label to PaginationPrevious / PaginationNext; the No-Touch primitive (read at the head) puts its English default before {...props}, so the renderer's name wins and ui/pagination.tsx is untouched, as triage required. The en names become Previous page / Next page, the data-table pager's keys, and the changeset names that too. DataTableRowActionsMenu takes t as a prop and reads table.rowActions; TABLE_DEFAULT_TRANSLATIONS gains the row. RIGHT.
  4. @object-ui/fields: the crop canvas, the percent slider and the record-picker grid read useFieldTranslation (t is already bound at RecordPickerDialog.tsx:560); FIELD_DEFAULTS gains three rows, one of them the borrowed console.commandPalette.records (en = Records). One key per name, as triage ruled. RIGHT.
  5. @object-ui/app-shell: the RecordFormPage breadcrumb nav reads common.breadcrumb through its existing t. RIGHT.
  6. @object-ui/plugin-designer: HistoryPanel, Minimap, VersionHistory and DashboardDesignPage read useDesignerTranslation; the defaults map gains five rows, detail.goBack borrowed (en = Go back). RIGHT.
  7. @object-ui/plugin-detail: the record-alert dismiss button reads home.recoveryReminder.dismiss (en = Dismiss, zh = 关闭) with an inline default; record-path.tsx changes a comment only, because the card's site there was a comment quoting an earlier repair and no attribute existed. RIGHT.
  8. @object-ui/plugin-grid: the BulkActionBar region reads grid.bulkActions through its existing t. RIGHT.
  9. @object-ui/plugin-list: the TabBarSelect trigger reads topbar.switchView (en = Switch view) through the already-imported useObjectTranslation; two .find lambdas are renamed so they stop shadowing t. RIGHT.
  10. @object-ui/console: five docs pages read help.loadingDocumentation through useObjectTranslation (a declared dependency) with an inline default. RIGHT.
  11. Exemptions, accept set unchanged, each verified against the head tree: DebugPanel (two names) is referenced only by its barrel export and a doc comment, no objectui surface mounts it; ui/sidebar.tsx SidebarRail is No-Touch and its only writer is packages/cli/src/utils/app-generator.ts, a generated-app template, with aria-label and title spread last so a host can name it; elements/message.tsx (four names) is vendored, has no call site outside the file, and spreads props after the defaults; elements/prompt-input.tsx's Submit default is vendored and its sole call site, ChatbotEnhanced, overrides it. RIGHT, and the PR states each reason as the card's Done-when asks.
  12. The vendored prompt-input.tsx edit: an additive optional prop with the upstream default preserved, on the statusLabels precedent (objectui#7254), present at elements/tool.tsx:41-60. The file header forbids editing business logic and asks for a wrapper; a wrapper cannot reach a button inside the chip, and a name prop is not business logic. The No-Touch register (.claude/skills/objectui-contributor/rules/no-touch-zones.md) covers packages/components/src/ui/** only. RIGHT. Re-sync hazard: a registry re-sync would restore the literal, and the census then reds on prompt-input.tsx · Remove attachment, so the hazard is guarded.
  13. The census, packages/i18n/src/__tests__/ariaLabelLiteralCensus-12145.test.ts: a suite test, not a gate (triage's ruling); it walks every packages/* and apps/* src/ tree minus tooling, judges four literal shapes with \p{L}, carries width guards, a lit control (a file directly under src/), and a matcher fixture; exemptions carry reasons; a frozen 87-name BACKLOG across 23 files is counted exactly, so a new literal and a removed declared one both red. Same shape and the same untyped maskComments import as the machineLocaleCensus-9909 precedent. RIGHT.
  14. The card's Done-when, re-measured on the head tree: the card's own re-check command returns exactly the ten exempt literals above and nothing else (31 at base). Per-package zh role-and-name pins exist for all nine packages touched (console by accessible name, see ③), and each zh case first asserts the two packs differ. RIGHT.

Finding confirmed: the card's pathspec, without :(glob), reads no file directly under a src/ root. aria-label="Undo" in plugin-designer/src/DataModelDesigner.tsx is unseen without it and seen with it; the same regex with :(glob) finds 114 sites at base and 93 at the head.

② Semver level

.changeset/12145-aria-label-i18n.md: @object-ui/i18n minor, @object-ui/plugin-chatbot minor, the other eight touched packages patch. That is what the diff publishes: the only two type-surface widenings are TranslationKeys and PromptInputAttachmentProps; everything else is a behaviour fix in a released package (patch, never none). All ten are in the fixed group and @object-ui/console is versioned (privatePackages.version: true). The body names the eight keys, the new prop, and exactly the two en names that change, each checked against the en pack at the head. Not breaking, so no migration or ADR-0087 marker is owed. Gates: Changeset Bump Policy, Changeset Declaration, Changeset Claim Re-read, Changeset Fixed Group Check, Changeset Overwrite Report, all success.

Clause-②: yes

The PR body's Clause-②: yes matches: a widening, yes takes at least minor, and both widened packages are minor. Triage's no was corrected in the claim on the TranslationKeys reasoning, which the head's declarations confirm.

③ Boundary flags

Dev report open_questions: none. Deviations, each answered or escalated:

  1. 87 literal sites frozen as census BACKLOG, not translated. ANSWERED for this PR: the claim's file surface is the card's 31 sites and the dev stopped at it, as the dispatch ordered; the card's Done-when is met as written. ESCALATED to the seat: the PR says Fixes #12145, so the queue would close "the family's closing card" with 87 names still literal (23 files, 11 packages; auth, layout and plugin-map carry no @object-ui/i18n dependency). Before arming, either file the follow-up card(s) from the dev's out-of-scope finding (a per-package slice is proposed) or turn Fixes into a non-closing reference so finding(i18n, a11y): 31 shipped aria-label attributes are literal English, so a non-English screen reader hears English names (the family's closing card) #12145 stays open. The seat's call, not a defect in the diff.
  2. Edits beyond the 31 lines. ANSWERED: the pagination renderer, the defaults maps and ChatbotEnhanced are the claim's "wrapper or call site" and "translation-hook wiring"; the vendored prop is judged in ① 12; the two edited tests read the pack's name (the zh-CN case resolves through the registry's zh-CN to zh fallback) and add one case.
  3. The console pin queries by accessible name, not role. ANSWERED: the spinner is an svg with no implicit role; getByLabelText reads the same accessible name a reader announces, and apps/console is an app, not one of the card's nine packages.
  4. TabBarSelect lambda rename. ANSWERED: cosmetic, required by the new t.
  5. The first commit subject says 26 names, the true count is 23. ANSWERED: history is not rewritten; the squash takes the PR title, which is right.
  6. Queue timeouts. A process note, nothing to judge.

Also escalated to the seat, from the dev's acceptance notes (carrier none): 30 interpolating-template names need a ruling on how an interpolated name is keyed; ChatbotEnhanced's uploadFiles fallback is still the English literal; visible English Previous / Next spans in the No-Touch pagination primitive and SidebarTrigger's sr-only span. None is in the card's accept set.

Check-runs on the head, the reading as taken (42 runs): 35 success, among them Lint, Type Check, Build & E2E, Build Docs, Test (dist pins), Test shards 1/8, 3/8, 6/8 and 8/8, Governed Surface Queue Guard and the five changeset gates; 3 skipped (two matrix placeholders and dependabot); 4 in_progress: Test shards 2/8, 4/8, 5/8 and 7/8. Not a governed diff. The verdict below is the contract's; the landing still waits on the four shards' conclusions and on item 1 above.

Implemented-by: claude/issue-12145-aria-label-i18n
Reviewed-by: session_016djJF12Qt14ejKR5Vjt4sK

VERDICT: PASS

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 11, 2026 16:48
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 11, 2026
Merged via the queue into main with commit e723f58 Oct 11, 2026
45 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-12145-aria-label-i18n branch October 11, 2026 17:05
zhuangjianguo pushed a commit that referenced this pull request Oct 11, 2026
Brings main to faf4ce9 (objectui#12151, #12152, #12153, #12154, #12148,
#12149, #12147, #12158, #12159); none of them touches this branch's files.

Claude-Session: https://claude.ai/code/session_01TYgwmFK1q4KJ6Qq2WRLzsD
Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants