fix(plugin-chatbot): translate the whole AI pending-actions inbox - #7330

Merged
os-litant merged 3 commits into
mainfrom
claude/issue-7173-ai-inbox-relative-time-i18n
Sep 2, 2026
Merged

fix(plugin-chatbot): translate the whole AI pending-actions inbox#7330
os-litant merged 3 commits into
mainfrom
claude/issue-7173-ai-inbox-relative-time-i18n

Conversation

@os-litant

Copy link
Copy Markdown
Collaborator

Fixes#7173

AiPendingActionsInbox held its own relative-time helper returning hardcoded English, so a zh / ja / ar session read English relative times on every row. The file also had no translation wiring of its own — the unwired-component shape, not the lookup-swap shape — so it is swept whole rather than having only its timestamps converted. That is the triage ruling of 2026-09-01 ("Sweep the file whole or leave it"), carrying forward what #7142 cost and #7149 had to pay back.

Verified on 5b4a3074d.

The namespace question was a measurement, and it came back non-empty

The triage ruled: establish whether cross-package key borrowing already has precedent in this repo, and follow it if it does. It does, overwhelmingly. Packages resolving detail.* from outside plugin-detail today:

packageexample call site
app-shellRecordDetailView.tsx (14 keys), RecordAttachmentsPanel.tsx (15), RecordApprovalsPanel.tsx, writeWarningToast.ts
plugin-listListView.tsx (9)
plugin-gridObjectGrid.tsxdetail.recordDetail sits in its own defaults map, the exact shape used here
plugin-viewObjectView.tsx (7)
plugin-kanbanObjectKanban.tsx (6)
plugin-treeObjectTree.tsx (5)
componentsnavigation-overlay.tsx (7), containers.tsx, RecordTitleChip.tsx, empty.tsx, action-menu.tsx
collaboration, fields, apps/consolefurther call sites

So the four relative-time branches borrow detail.justNow / minutesAgo / hoursAgo / daysAgo and no new pack rows are needed for them. Key presence re-verified by reading the pack OBJECTS, not a dotted grep (the packs are nested): 10/10 for each of the four, positive controls detail.back / detail.noActivity 10/10, negative control detail.zzzAbsentControl7163 0/10.

This does not escalate. No open question comes back.

The file census, and what each literal became

Every user-visible English literal in the 526-line file, in one table. Nothing is left half-wired.

surfaceliteralsresolution
card headerAI Approvals, the description sentenceaiApprovals.title / .description
tabsPending, Decided, AllaiApprovals.tabPending / .tabDecided / .tabAll
toolbarRefreshreused common.refresh
status badgesPending, Approved, Executed, Failed, RejectedaiApprovals.status*
outcome bannerApprove … for …: …, Reject …, OK, Failed, Executed, Rejected, Action failed during executionaiApprovals.outcomeApprove / .outcomeReject / .outcomeExecuteFailed, reused common.ok
empty stateNo actions waiting + its sentenceaiApprovals.emptyTitle / .emptyDescription
table headingsTool, Action, Object, Status, Proposed, DecisionaiApprovals.col*
row buttonsView, Approve, Reject, Working…aiApprovals.view / .approve / .reject / .working
drawer headerPending action, Loading…, Tool … on …aiApprovals.drawerFallbackTitle / .drawerSubtitle, reused common.loading
drawer fieldsStatus, Proposed, Proposed by, Decided by, Conversation, Tool input, Result, Error, Rejection reasonaiApprovals.colStatus / .colProposed / .field*
drawer buttonsApprove & Execute, RejectaiApprovals.approveAndExecute / .reject
reject dialogtitle, body, placeholder, Cancel, RejectaiApprovals.rejectTitle / .rejectBody / .rejectPlaceholder, reused common.cancel
relative timesjust now, {count}m ago, {count}h ago, {count}d agoborrowed detail.*

Deliberately not keyed: the placeholder for a null field, and the toLocaleDateString() tail past 30 days. Neither is English copy — the first is punctuation, the second is already locale-formatted, and the sibling helpers in plugin-detail leave their own tails unkeyed for the same reason.

38 new keys, translated in all ten packs (aiApprovals). It is deliberately separate from approvalsInbox, which is the human approval-process inbox: different surface, different feature, so no rows are shared with it. Four generic verbs are reused rather than forked.

The five helpers are NOT unified

Per the ruling. Math.round here against Math.floor in plugin-detail; thresholds 45s / 60min / 24h / 30d against 60s / 7d; different tails. The arithmetic is byte-for-byte unchanged and three test rows exist only to pin it, so a later "unification" cannot change it silently:

  • 50 s renders 1m ago — a 60 s threshold would still say "just now".
  • 90 s renders 2m agoMath.floor gives 1m ago.
  • 20 d renders 20d ago — a 7 d threshold would already have fallen through to a date.

Two assembled sentences became single interpolated keys

The outcome banner was {verb} for <code>{id}</code>: {message} and the drawer subtitle was Tool <code>{tool}</code> on <code>{object}</code> — English word order assembled from JSX fragments, which several locales cannot reorder. They are now aiApprovals.outcomeApprove (Approve for {{id}}: {{message}}) and aiApprovals.drawerSubtitle (Tool {{tool}} on {{object}}), following the repo's own precedent (approvalsInbox.inlineApproved).

The visible cost, stated rather than hidden: the three identifiers lose their <code> monospace styling. The id keeps its 8-character truncation. Restoring monospace would require splitting the sentence back into fragments, which is the defect.

Two smaller decisions, both forced by measurement

createSafeTranslation, not useSafeTranslate. The card requires the provider-less path to yield English from the defaults map and to interpolate {{count}}. useSafeTranslate(key, fallback) has no options argument, so {{count}} would reach the DOM raw; it is also the per-call-site inline-English shape #3517 rules out. createSafeTranslation is what plugin-detail, plugin-grid, plugin-kanban and components all use. No inline defaultValue anywhere.

Status labels are a switch over literal keys, not a map of labelKey values. Measured on this change: with the keys held as map values, check:i18n-dead-keys listed aiApprovals.statusPending and aiApprovals.statusApproved as needs-review — precisely the two whose only appearance was that map, while the three that also had a literal call site elsewhere were seen. That is #7149's lesson ("a key visible only as a map value has no call site the i18n scanners can resolve"). After the change the namespace is absent from the report entirely. The Badge variant stays a map, because a variant is not copy.

Evidence

An en-only test cannot discriminate here — every key's en value is byte-identical to the literal it replaced. So the load-bearing assertions are zh and ar.

Pre-fix red, measured on the unconverted file:Tests 8 failed | 4 passed (12). The four survivors were exactly the en / provider-less cases, which is the card's point restated as data. Sample failure: Unable to find an element with the text: 刚刚.

Ablation, after commit — hardcoded English put back into all four branches, then restored:

HEAD blob: cc690304a545194774da9a945c3d41cb42b90c79
injected 'ABLATION-7173': 4
deleted "t('detail.justNow')": 0
mutated blob: 77514218602baef1ba50b06d9bb74a0ed4ee63c4
× 8 of 14 cases red (all the zh/ar ones)
Tests 8 failed | 6 passed (14)
RESTORE OK: on-disk blob cc690304a545194774da9a945c3d41cb42b90c79 == HEAD blob cc690304a545194774da9a945c3d41cb42b90c79

No rebuild leg is needed for it: both suites import the component by relative source path (../AiPendingActionsInbox) and the root vitest config aliases @object-ui/i18n to packages/i18n/src, so nothing on the tested path resolves through dist/. The mutation was proven on disk by grep counts on both the injected and the deleted text plus a blob-hash change; the restore by blob-hash equality with the HEAD blob, under an EXIT INT TERM trap using absolute paths.

Why the provider-less leg is in its own file.createI18n installs itself as react-i18next's module-level global, so a provider-less render in a file that has already mounted an I18nProvider reads that pack instead of the defaults map. Measured while writing this: two mounts in one test produced two zh trees, and the "provider-less" assertion was quietly measuring the previous test's pack. Vitest isolates module state per file, so AiPendingActionsInbox.noProviderFallback.test.tsx never mounts a provider.

Runs, all at 5b4a3074d:

pnpm exec vitest run packages/plugin-chatbot/ packages/i18n/
→ Test Files 89 passed (89) · Tests 1336 passed (1336)
pnpm --filter @object-ui/plugin-chatbot run type-check → EXIT=0
pnpm --filter @object-ui/i18n run type-check → EXIT=0
(tsc -p tsconfig.test.json --listFiles confirms both new suites are IN the program —
3 hits for AiPendingActionsInbox, so this is not a "typecheck excludes tests" green)
pnpm check:i18n-keys → EXIT=0 "Every in-scope call-site key resolves against the en pack (2906 keys) …"
pnpm check:i18n-drift → EXIT=0 "0 en value(s) changed (38 key(s) added, 0 removed)"
pnpm check:i18n-dead-keys → EXIT=0 report-only; aiApprovals absent from the findings
pnpm check:control-bytes → EXIT=0 "scanned 6019 tracked text file(s)"
pnpm check:phantom-deps / check:self-import / check:side-effects-array / check:esm-specifiers → EXIT=0
node scripts/check-changeset-presence.mjs → EXIT=0 "15 source file(s) of 2 released package(s) … declares 1 changeset(s)"
node scripts/check-changeset-no-major.mjs → EXIT=0
node scripts/check-governed-queue-guard.mjs --test → "NOT GOVERNED — 16 path(s) checked"

Every exit code was captured by redirecting first (cmd > out 2>&1; EXIT=$?), never read across a pipe.

Repo-wide pnpm lint was narrowed, and the narrowing is declared. Ran eslint --no-inline-config --format json over the 16 changed files: 16 files linted, 0 errors, 0 warnings, the count read from the JSON output rather than assumed. The narrowing excludes nothing relevant: this repo's eslint.config.js enables no type-aware linting, so a diff cannot move the verdict on a file it does not touch. CI runs the full farm regardless.

One ratchet moved

packages/i18n/src/__tests__/de-quote-pairing-3876.test.ts: 58 → 59 matched German „…“ pairs. The de aiApprovals.rejectPlaceholder quotes the example rejection reason it suggests. Both numeric pins were updated together with a comment naming the key, and rdq stays at 0 — the load-bearing half, since a stray closer would make close === open true for the wrong reason.


Draft on purpose: the dispatching PM lands this.

Generated by Claude Code


Generated by Claude Code

)
`AiPendingActionsInbox` held its own relative-time helper returning hardcoded
English, and had no translation wiring of its own — the unwired-component shape.
Swept whole rather than converting only its timestamps: #7142 wired one string
into an otherwise untranslated component and shipped something visibly
half-done, and the triage ruling on this card carried that forward.
The four relative-time branches borrow `detail.justNow` / `minutesAgo` /
`hoursAgo` / `daysAgo`, already translated in all ten packs; cross-package
`detail.*` borrowing is settled convention here (ObjectGrid, ObjectKanban,
ObjectTree, ListView, ObjectView, NavigationOverlay, RecordDetailView). The rest
of the sweep needed copy no pack had, so `@object-ui/i18n` gains an
`aiApprovals` namespace — 38 keys, translated in all ten packs, deliberately
separate from `approvalsInbox` (the human approval-process inbox).
The five relative-time helpers are NOT unified: they differ in real behaviour
(`Math.round` vs `Math.floor`, 45s/30d vs 60s/7d, different tails). This copy's
arithmetic is untouched and three test rows exist only to pin it.
Assertions are in zh and ar — each key's `en` value is byte-identical to the
literal it replaced, so an `en`-only test is green before the fix too. The
provider-less leg lives in its own file because `createI18n` installs itself as
react-i18next's module-level global. No inline `defaultValue` (#3517).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NRRumy89BYdW9ogbcdHTho
@github-actions

Copy link
Copy Markdown
Contributor

❌ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3165.4 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-BeDRt1bU.js
StatusFAIL

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.

Which half objected:

Eager-closure halfVerdict
Aggregate closure ceiling✅ pass
Per-chunk ceilings❌ over its ceiling
Ceiling sensitivity (headroom)✅ pass
Ceiling freshness (checkout vs. base branch)✅ pass

📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)514.48KB117.36KB
core (index.js)5.55KB2.23KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.25KB61.73KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)70.02KB19.44KB
plugin-chatbot (index.js)193.26KB45.80KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)250.63KB63.90KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.78KB32.58KB
plugin-gantt (index.js)166.77KB40.76KB
plugin-grid (index.js)208.88KB56.59KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.51KB27.67KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

…mework chunk
Two consequences of translating AiPendingActionsInbox, both this PR's own.
`ConversationsSidebar.test.tsx` froze its `vi.mock('@object-ui/i18n', ...)`
factory to a hand-written object. Its import graph reaches `plugin-chatbot`,
which resolves `createSafeTranslation` at module scope, so the frozen surface
made that read `undefined` and the file died during COLLECTION — objectui#6849's
shape, which does not look like a test failure. It now spreads
`importOriginal()` and overrides only `useObjectTranslation`. Measured: of the
41 frozen `@object-ui/i18n` factories in the repo, running every one of them
showed this to be the only file whose graph reaches the package.
The ten pack blocks are locale DATA and land in the console's eager `framework`
chunk. Its ceiling moves 512,000 -> 524,000 gzipped bytes and its baseline is
re-pinned onto a fresh measurement, 502,405 -> 514,863. Attributed by three
console builds: the merge parent reads 510,192, this branch with the ten
`aiApprovals` blocks cut reads 510,192 again, and this branch reads 514,863, so
the whole 4,671-byte delta is the pack data and nothing else. Headroom is kept
at the line's own convention (9,137 bytes, 0.10x the regression the gate must
catch), not widened; the merge parent was already at 510,192 of the 512,000
allowed, so most of the overage was pre-existing drift.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NRRumy89BYdW9ogbcdHTho
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3165.5 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-IXjpYdG_.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)514.48KB117.36KB
core (index.js)5.55KB2.23KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.25KB61.73KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)70.02KB19.44KB
plugin-chatbot (index.js)193.26KB45.80KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)250.63KB63.90KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.78KB32.58KB
plugin-gantt (index.js)166.77KB40.76KB
plugin-grid (index.js)208.88KB56.59KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.51KB27.67KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-litant
os-litant marked this pull request as ready for review September 2, 2026 08:37
@os-litant
os-litant added this pull request to the merge queueSep 2, 2026
Merged via the queue into main with commit 01c9023Sep 2, 2026
32 checks passed
@os-litant
os-litant deleted the claude/issue-7173-ai-inbox-relative-time-i18n branch September 2, 2026 08:51
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

AiPendingActionsInbox.formatRelative is a fifth untranslated copy of the same relative-time helper — the keys it needs are already in all ten packs

2 participants

@os-litant@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

fix(plugin-chatbot): translate the whole AI pending-actions inbox - #7330

Merged
os-litant merged 3 commits into
mainfrom
claude/issue-7173-ai-inbox-relative-time-i18n
Sep 2, 2026
Merged

fix(plugin-chatbot): translate the whole AI pending-actions inbox#7330
os-litant merged 3 commits into
mainfrom
claude/issue-7173-ai-inbox-relative-time-i18n

Conversation

@os-litant

Copy link
Copy Markdown
Collaborator

Fixes#7173

AiPendingActionsInbox held its own relative-time helper returning hardcoded English, so a zh / ja / ar session read English relative times on every row. The file also had no translation wiring of its own — the unwired-component shape, not the lookup-swap shape — so it is swept whole rather than having only its timestamps converted. That is the triage ruling of 2026-09-01 ("Sweep the file whole or leave it"), carrying forward what #7142 cost and #7149 had to pay back.

Verified on 5b4a3074d.

The namespace question was a measurement, and it came back non-empty

The triage ruled: establish whether cross-package key borrowing already has precedent in this repo, and follow it if it does. It does, overwhelmingly. Packages resolving detail.* from outside plugin-detail today:

packageexample call site
app-shellRecordDetailView.tsx (14 keys), RecordAttachmentsPanel.tsx (15), RecordApprovalsPanel.tsx, writeWarningToast.ts
plugin-listListView.tsx (9)
plugin-gridObjectGrid.tsxdetail.recordDetail sits in its own defaults map, the exact shape used here
plugin-viewObjectView.tsx (7)
plugin-kanbanObjectKanban.tsx (6)
plugin-treeObjectTree.tsx (5)
componentsnavigation-overlay.tsx (7), containers.tsx, RecordTitleChip.tsx, empty.tsx, action-menu.tsx
collaboration, fields, apps/consolefurther call sites

So the four relative-time branches borrow detail.justNow / minutesAgo / hoursAgo / daysAgo and no new pack rows are needed for them. Key presence re-verified by reading the pack OBJECTS, not a dotted grep (the packs are nested): 10/10 for each of the four, positive controls detail.back / detail.noActivity 10/10, negative control detail.zzzAbsentControl7163 0/10.

This does not escalate. No open question comes back.

The file census, and what each literal became

Every user-visible English literal in the 526-line file, in one table. Nothing is left half-wired.

surfaceliteralsresolution
card headerAI Approvals, the description sentenceaiApprovals.title / .description
tabsPending, Decided, AllaiApprovals.tabPending / .tabDecided / .tabAll
toolbarRefreshreused common.refresh
status badgesPending, Approved, Executed, Failed, RejectedaiApprovals.status*
outcome bannerApprove … for …: …, Reject …, OK, Failed, Executed, Rejected, Action failed during executionaiApprovals.outcomeApprove / .outcomeReject / .outcomeExecuteFailed, reused common.ok
empty stateNo actions waiting + its sentenceaiApprovals.emptyTitle / .emptyDescription
table headingsTool, Action, Object, Status, Proposed, DecisionaiApprovals.col*
row buttonsView, Approve, Reject, Working…aiApprovals.view / .approve / .reject / .working
drawer headerPending action, Loading…, Tool … on …aiApprovals.drawerFallbackTitle / .drawerSubtitle, reused common.loading
drawer fieldsStatus, Proposed, Proposed by, Decided by, Conversation, Tool input, Result, Error, Rejection reasonaiApprovals.colStatus / .colProposed / .field*
drawer buttonsApprove & Execute, RejectaiApprovals.approveAndExecute / .reject
reject dialogtitle, body, placeholder, Cancel, RejectaiApprovals.rejectTitle / .rejectBody / .rejectPlaceholder, reused common.cancel
relative timesjust now, {count}m ago, {count}h ago, {count}d agoborrowed detail.*

Deliberately not keyed: the placeholder for a null field, and the toLocaleDateString() tail past 30 days. Neither is English copy — the first is punctuation, the second is already locale-formatted, and the sibling helpers in plugin-detail leave their own tails unkeyed for the same reason.

38 new keys, translated in all ten packs (aiApprovals). It is deliberately separate from approvalsInbox, which is the human approval-process inbox: different surface, different feature, so no rows are shared with it. Four generic verbs are reused rather than forked.

The five helpers are NOT unified

Per the ruling. Math.round here against Math.floor in plugin-detail; thresholds 45s / 60min / 24h / 30d against 60s / 7d; different tails. The arithmetic is byte-for-byte unchanged and three test rows exist only to pin it, so a later "unification" cannot change it silently:

  • 50 s renders 1m ago — a 60 s threshold would still say "just now".
  • 90 s renders 2m agoMath.floor gives 1m ago.
  • 20 d renders 20d ago — a 7 d threshold would already have fallen through to a date.

Two assembled sentences became single interpolated keys

The outcome banner was {verb} for <code>{id}</code>: {message} and the drawer subtitle was Tool <code>{tool}</code> on <code>{object}</code> — English word order assembled from JSX fragments, which several locales cannot reorder. They are now aiApprovals.outcomeApprove (Approve for {{id}}: {{message}}) and aiApprovals.drawerSubtitle (Tool {{tool}} on {{object}}), following the repo's own precedent (approvalsInbox.inlineApproved).

The visible cost, stated rather than hidden: the three identifiers lose their <code> monospace styling. The id keeps its 8-character truncation. Restoring monospace would require splitting the sentence back into fragments, which is the defect.

Two smaller decisions, both forced by measurement

createSafeTranslation, not useSafeTranslate. The card requires the provider-less path to yield English from the defaults map and to interpolate {{count}}. useSafeTranslate(key, fallback) has no options argument, so {{count}} would reach the DOM raw; it is also the per-call-site inline-English shape #3517 rules out. createSafeTranslation is what plugin-detail, plugin-grid, plugin-kanban and components all use. No inline defaultValue anywhere.

Status labels are a switch over literal keys, not a map of labelKey values. Measured on this change: with the keys held as map values, check:i18n-dead-keys listed aiApprovals.statusPending and aiApprovals.statusApproved as needs-review — precisely the two whose only appearance was that map, while the three that also had a literal call site elsewhere were seen. That is #7149's lesson ("a key visible only as a map value has no call site the i18n scanners can resolve"). After the change the namespace is absent from the report entirely. The Badge variant stays a map, because a variant is not copy.

Evidence

An en-only test cannot discriminate here — every key's en value is byte-identical to the literal it replaced. So the load-bearing assertions are zh and ar.

Pre-fix red, measured on the unconverted file:Tests 8 failed | 4 passed (12). The four survivors were exactly the en / provider-less cases, which is the card's point restated as data. Sample failure: Unable to find an element with the text: 刚刚.

Ablation, after commit — hardcoded English put back into all four branches, then restored:

HEAD blob: cc690304a545194774da9a945c3d41cb42b90c79
injected 'ABLATION-7173': 4
deleted "t('detail.justNow')": 0
mutated blob: 77514218602baef1ba50b06d9bb74a0ed4ee63c4
× 8 of 14 cases red (all the zh/ar ones)
Tests 8 failed | 6 passed (14)
RESTORE OK: on-disk blob cc690304a545194774da9a945c3d41cb42b90c79 == HEAD blob cc690304a545194774da9a945c3d41cb42b90c79

No rebuild leg is needed for it: both suites import the component by relative source path (../AiPendingActionsInbox) and the root vitest config aliases @object-ui/i18n to packages/i18n/src, so nothing on the tested path resolves through dist/. The mutation was proven on disk by grep counts on both the injected and the deleted text plus a blob-hash change; the restore by blob-hash equality with the HEAD blob, under an EXIT INT TERM trap using absolute paths.

Why the provider-less leg is in its own file.createI18n installs itself as react-i18next's module-level global, so a provider-less render in a file that has already mounted an I18nProvider reads that pack instead of the defaults map. Measured while writing this: two mounts in one test produced two zh trees, and the "provider-less" assertion was quietly measuring the previous test's pack. Vitest isolates module state per file, so AiPendingActionsInbox.noProviderFallback.test.tsx never mounts a provider.

Runs, all at 5b4a3074d:

pnpm exec vitest run packages/plugin-chatbot/ packages/i18n/
→ Test Files 89 passed (89) · Tests 1336 passed (1336)
pnpm --filter @object-ui/plugin-chatbot run type-check → EXIT=0
pnpm --filter @object-ui/i18n run type-check → EXIT=0
(tsc -p tsconfig.test.json --listFiles confirms both new suites are IN the program —
3 hits for AiPendingActionsInbox, so this is not a "typecheck excludes tests" green)
pnpm check:i18n-keys → EXIT=0 "Every in-scope call-site key resolves against the en pack (2906 keys) …"
pnpm check:i18n-drift → EXIT=0 "0 en value(s) changed (38 key(s) added, 0 removed)"
pnpm check:i18n-dead-keys → EXIT=0 report-only; aiApprovals absent from the findings
pnpm check:control-bytes → EXIT=0 "scanned 6019 tracked text file(s)"
pnpm check:phantom-deps / check:self-import / check:side-effects-array / check:esm-specifiers → EXIT=0
node scripts/check-changeset-presence.mjs → EXIT=0 "15 source file(s) of 2 released package(s) … declares 1 changeset(s)"
node scripts/check-changeset-no-major.mjs → EXIT=0
node scripts/check-governed-queue-guard.mjs --test → "NOT GOVERNED — 16 path(s) checked"

Every exit code was captured by redirecting first (cmd > out 2>&1; EXIT=$?), never read across a pipe.

Repo-wide pnpm lint was narrowed, and the narrowing is declared. Ran eslint --no-inline-config --format json over the 16 changed files: 16 files linted, 0 errors, 0 warnings, the count read from the JSON output rather than assumed. The narrowing excludes nothing relevant: this repo's eslint.config.js enables no type-aware linting, so a diff cannot move the verdict on a file it does not touch. CI runs the full farm regardless.

One ratchet moved

packages/i18n/src/__tests__/de-quote-pairing-3876.test.ts: 58 → 59 matched German „…“ pairs. The de aiApprovals.rejectPlaceholder quotes the example rejection reason it suggests. Both numeric pins were updated together with a comment naming the key, and rdq stays at 0 — the load-bearing half, since a stray closer would make close === open true for the wrong reason.


Draft on purpose: the dispatching PM lands this.

Generated by Claude Code


Generated by Claude Code

)
`AiPendingActionsInbox` held its own relative-time helper returning hardcoded
English, and had no translation wiring of its own — the unwired-component shape.
Swept whole rather than converting only its timestamps: #7142 wired one string
into an otherwise untranslated component and shipped something visibly
half-done, and the triage ruling on this card carried that forward.
The four relative-time branches borrow `detail.justNow` / `minutesAgo` /
`hoursAgo` / `daysAgo`, already translated in all ten packs; cross-package
`detail.*` borrowing is settled convention here (ObjectGrid, ObjectKanban,
ObjectTree, ListView, ObjectView, NavigationOverlay, RecordDetailView). The rest
of the sweep needed copy no pack had, so `@object-ui/i18n` gains an
`aiApprovals` namespace — 38 keys, translated in all ten packs, deliberately
separate from `approvalsInbox` (the human approval-process inbox).
The five relative-time helpers are NOT unified: they differ in real behaviour
(`Math.round` vs `Math.floor`, 45s/30d vs 60s/7d, different tails). This copy's
arithmetic is untouched and three test rows exist only to pin it.
Assertions are in zh and ar — each key's `en` value is byte-identical to the
literal it replaced, so an `en`-only test is green before the fix too. The
provider-less leg lives in its own file because `createI18n` installs itself as
react-i18next's module-level global. No inline `defaultValue` (#3517).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NRRumy89BYdW9ogbcdHTho
@github-actions

Copy link
Copy Markdown
Contributor

❌ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3165.4 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-BeDRt1bU.js
StatusFAIL

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.

Which half objected:

Eager-closure halfVerdict
Aggregate closure ceiling✅ pass
Per-chunk ceilings❌ over its ceiling
Ceiling sensitivity (headroom)✅ pass
Ceiling freshness (checkout vs. base branch)✅ pass

📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)514.48KB117.36KB
core (index.js)5.55KB2.23KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.25KB61.73KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)70.02KB19.44KB
plugin-chatbot (index.js)193.26KB45.80KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)250.63KB63.90KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.78KB32.58KB
plugin-gantt (index.js)166.77KB40.76KB
plugin-grid (index.js)208.88KB56.59KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.51KB27.67KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

…mework chunk
Two consequences of translating AiPendingActionsInbox, both this PR's own.
`ConversationsSidebar.test.tsx` froze its `vi.mock('@object-ui/i18n', ...)`
factory to a hand-written object. Its import graph reaches `plugin-chatbot`,
which resolves `createSafeTranslation` at module scope, so the frozen surface
made that read `undefined` and the file died during COLLECTION — objectui#6849's
shape, which does not look like a test failure. It now spreads
`importOriginal()` and overrides only `useObjectTranslation`. Measured: of the
41 frozen `@object-ui/i18n` factories in the repo, running every one of them
showed this to be the only file whose graph reaches the package.
The ten pack blocks are locale DATA and land in the console's eager `framework`
chunk. Its ceiling moves 512,000 -> 524,000 gzipped bytes and its baseline is
re-pinned onto a fresh measurement, 502,405 -> 514,863. Attributed by three
console builds: the merge parent reads 510,192, this branch with the ten
`aiApprovals` blocks cut reads 510,192 again, and this branch reads 514,863, so
the whole 4,671-byte delta is the pack data and nothing else. Headroom is kept
at the line's own convention (9,137 bytes, 0.10x the regression the gate must
catch), not widened; the merge parent was already at 510,192 of the 512,000
allowed, so most of the overage was pre-existing drift.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NRRumy89BYdW9ogbcdHTho
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3165.5 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-IXjpYdG_.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)514.48KB117.36KB
core (index.js)5.55KB2.23KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.25KB61.73KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)70.02KB19.44KB
plugin-chatbot (index.js)193.26KB45.80KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)250.63KB63.90KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.78KB32.58KB
plugin-gantt (index.js)166.77KB40.76KB
plugin-grid (index.js)208.88KB56.59KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.51KB27.67KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-litant
os-litant marked this pull request as ready for review September 2, 2026 08:37
@os-litant
os-litant added this pull request to the merge queueSep 2, 2026
Merged via the queue into main with commit 01c9023Sep 2, 2026
32 checks passed
@os-litant
os-litant deleted the claude/issue-7173-ai-inbox-relative-time-i18n branch September 2, 2026 08:51
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

AiPendingActionsInbox.formatRelative is a fifth untranslated copy of the same relative-time helper — the keys it needs are already in all ten packs

2 participants

@os-litant@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(plugin-chatbot): translate the whole AI pending-actions inbox - #7330

Merged
os-litant merged 3 commits into
mainfrom
claude/issue-7173-ai-inbox-relative-time-i18n
Sep 2, 2026
Merged

fix(plugin-chatbot): translate the whole AI pending-actions inbox#7330
os-litant merged 3 commits into
mainfrom
claude/issue-7173-ai-inbox-relative-time-i18n

Conversation

@os-litant

Copy link
Copy Markdown
Collaborator

Fixes#7173

AiPendingActionsInbox held its own relative-time helper returning hardcoded English, so a zh / ja / ar session read English relative times on every row. The file also had no translation wiring of its own — the unwired-component shape, not the lookup-swap shape — so it is swept whole rather than having only its timestamps converted. That is the triage ruling of 2026-09-01 ("Sweep the file whole or leave it"), carrying forward what #7142 cost and #7149 had to pay back.

Verified on 5b4a3074d.

The namespace question was a measurement, and it came back non-empty

The triage ruled: establish whether cross-package key borrowing already has precedent in this repo, and follow it if it does. It does, overwhelmingly. Packages resolving detail.* from outside plugin-detail today:

packageexample call site
app-shellRecordDetailView.tsx (14 keys), RecordAttachmentsPanel.tsx (15), RecordApprovalsPanel.tsx, writeWarningToast.ts
plugin-listListView.tsx (9)
plugin-gridObjectGrid.tsxdetail.recordDetail sits in its own defaults map, the exact shape used here
plugin-viewObjectView.tsx (7)
plugin-kanbanObjectKanban.tsx (6)
plugin-treeObjectTree.tsx (5)
componentsnavigation-overlay.tsx (7), containers.tsx, RecordTitleChip.tsx, empty.tsx, action-menu.tsx
collaboration, fields, apps/consolefurther call sites

So the four relative-time branches borrow detail.justNow / minutesAgo / hoursAgo / daysAgo and no new pack rows are needed for them. Key presence re-verified by reading the pack OBJECTS, not a dotted grep (the packs are nested): 10/10 for each of the four, positive controls detail.back / detail.noActivity 10/10, negative control detail.zzzAbsentControl7163 0/10.

This does not escalate. No open question comes back.

The file census, and what each literal became

Every user-visible English literal in the 526-line file, in one table. Nothing is left half-wired.

surfaceliteralsresolution
card headerAI Approvals, the description sentenceaiApprovals.title / .description
tabsPending, Decided, AllaiApprovals.tabPending / .tabDecided / .tabAll
toolbarRefreshreused common.refresh
status badgesPending, Approved, Executed, Failed, RejectedaiApprovals.status*
outcome bannerApprove … for …: …, Reject …, OK, Failed, Executed, Rejected, Action failed during executionaiApprovals.outcomeApprove / .outcomeReject / .outcomeExecuteFailed, reused common.ok
empty stateNo actions waiting + its sentenceaiApprovals.emptyTitle / .emptyDescription
table headingsTool, Action, Object, Status, Proposed, DecisionaiApprovals.col*
row buttonsView, Approve, Reject, Working…aiApprovals.view / .approve / .reject / .working
drawer headerPending action, Loading…, Tool … on …aiApprovals.drawerFallbackTitle / .drawerSubtitle, reused common.loading
drawer fieldsStatus, Proposed, Proposed by, Decided by, Conversation, Tool input, Result, Error, Rejection reasonaiApprovals.colStatus / .colProposed / .field*
drawer buttonsApprove & Execute, RejectaiApprovals.approveAndExecute / .reject
reject dialogtitle, body, placeholder, Cancel, RejectaiApprovals.rejectTitle / .rejectBody / .rejectPlaceholder, reused common.cancel
relative timesjust now, {count}m ago, {count}h ago, {count}d agoborrowed detail.*

Deliberately not keyed: the placeholder for a null field, and the toLocaleDateString() tail past 30 days. Neither is English copy — the first is punctuation, the second is already locale-formatted, and the sibling helpers in plugin-detail leave their own tails unkeyed for the same reason.

38 new keys, translated in all ten packs (aiApprovals). It is deliberately separate from approvalsInbox, which is the human approval-process inbox: different surface, different feature, so no rows are shared with it. Four generic verbs are reused rather than forked.

The five helpers are NOT unified

Per the ruling. Math.round here against Math.floor in plugin-detail; thresholds 45s / 60min / 24h / 30d against 60s / 7d; different tails. The arithmetic is byte-for-byte unchanged and three test rows exist only to pin it, so a later "unification" cannot change it silently:

  • 50 s renders 1m ago — a 60 s threshold would still say "just now".
  • 90 s renders 2m agoMath.floor gives 1m ago.
  • 20 d renders 20d ago — a 7 d threshold would already have fallen through to a date.

Two assembled sentences became single interpolated keys

The outcome banner was {verb} for <code>{id}</code>: {message} and the drawer subtitle was Tool <code>{tool}</code> on <code>{object}</code> — English word order assembled from JSX fragments, which several locales cannot reorder. They are now aiApprovals.outcomeApprove (Approve for {{id}}: {{message}}) and aiApprovals.drawerSubtitle (Tool {{tool}} on {{object}}), following the repo's own precedent (approvalsInbox.inlineApproved).

The visible cost, stated rather than hidden: the three identifiers lose their <code> monospace styling. The id keeps its 8-character truncation. Restoring monospace would require splitting the sentence back into fragments, which is the defect.

Two smaller decisions, both forced by measurement

createSafeTranslation, not useSafeTranslate. The card requires the provider-less path to yield English from the defaults map and to interpolate {{count}}. useSafeTranslate(key, fallback) has no options argument, so {{count}} would reach the DOM raw; it is also the per-call-site inline-English shape #3517 rules out. createSafeTranslation is what plugin-detail, plugin-grid, plugin-kanban and components all use. No inline defaultValue anywhere.

Status labels are a switch over literal keys, not a map of labelKey values. Measured on this change: with the keys held as map values, check:i18n-dead-keys listed aiApprovals.statusPending and aiApprovals.statusApproved as needs-review — precisely the two whose only appearance was that map, while the three that also had a literal call site elsewhere were seen. That is #7149's lesson ("a key visible only as a map value has no call site the i18n scanners can resolve"). After the change the namespace is absent from the report entirely. The Badge variant stays a map, because a variant is not copy.

Evidence

An en-only test cannot discriminate here — every key's en value is byte-identical to the literal it replaced. So the load-bearing assertions are zh and ar.

Pre-fix red, measured on the unconverted file:Tests 8 failed | 4 passed (12). The four survivors were exactly the en / provider-less cases, which is the card's point restated as data. Sample failure: Unable to find an element with the text: 刚刚.

Ablation, after commit — hardcoded English put back into all four branches, then restored:

HEAD blob: cc690304a545194774da9a945c3d41cb42b90c79
injected 'ABLATION-7173': 4
deleted "t('detail.justNow')": 0
mutated blob: 77514218602baef1ba50b06d9bb74a0ed4ee63c4
× 8 of 14 cases red (all the zh/ar ones)
Tests 8 failed | 6 passed (14)
RESTORE OK: on-disk blob cc690304a545194774da9a945c3d41cb42b90c79 == HEAD blob cc690304a545194774da9a945c3d41cb42b90c79

No rebuild leg is needed for it: both suites import the component by relative source path (../AiPendingActionsInbox) and the root vitest config aliases @object-ui/i18n to packages/i18n/src, so nothing on the tested path resolves through dist/. The mutation was proven on disk by grep counts on both the injected and the deleted text plus a blob-hash change; the restore by blob-hash equality with the HEAD blob, under an EXIT INT TERM trap using absolute paths.

Why the provider-less leg is in its own file.createI18n installs itself as react-i18next's module-level global, so a provider-less render in a file that has already mounted an I18nProvider reads that pack instead of the defaults map. Measured while writing this: two mounts in one test produced two zh trees, and the "provider-less" assertion was quietly measuring the previous test's pack. Vitest isolates module state per file, so AiPendingActionsInbox.noProviderFallback.test.tsx never mounts a provider.

Runs, all at 5b4a3074d:

pnpm exec vitest run packages/plugin-chatbot/ packages/i18n/
→ Test Files 89 passed (89) · Tests 1336 passed (1336)
pnpm --filter @object-ui/plugin-chatbot run type-check → EXIT=0
pnpm --filter @object-ui/i18n run type-check → EXIT=0
(tsc -p tsconfig.test.json --listFiles confirms both new suites are IN the program —
3 hits for AiPendingActionsInbox, so this is not a "typecheck excludes tests" green)
pnpm check:i18n-keys → EXIT=0 "Every in-scope call-site key resolves against the en pack (2906 keys) …"
pnpm check:i18n-drift → EXIT=0 "0 en value(s) changed (38 key(s) added, 0 removed)"
pnpm check:i18n-dead-keys → EXIT=0 report-only; aiApprovals absent from the findings
pnpm check:control-bytes → EXIT=0 "scanned 6019 tracked text file(s)"
pnpm check:phantom-deps / check:self-import / check:side-effects-array / check:esm-specifiers → EXIT=0
node scripts/check-changeset-presence.mjs → EXIT=0 "15 source file(s) of 2 released package(s) … declares 1 changeset(s)"
node scripts/check-changeset-no-major.mjs → EXIT=0
node scripts/check-governed-queue-guard.mjs --test → "NOT GOVERNED — 16 path(s) checked"

Every exit code was captured by redirecting first (cmd > out 2>&1; EXIT=$?), never read across a pipe.

Repo-wide pnpm lint was narrowed, and the narrowing is declared. Ran eslint --no-inline-config --format json over the 16 changed files: 16 files linted, 0 errors, 0 warnings, the count read from the JSON output rather than assumed. The narrowing excludes nothing relevant: this repo's eslint.config.js enables no type-aware linting, so a diff cannot move the verdict on a file it does not touch. CI runs the full farm regardless.

One ratchet moved

packages/i18n/src/__tests__/de-quote-pairing-3876.test.ts: 58 → 59 matched German „…“ pairs. The de aiApprovals.rejectPlaceholder quotes the example rejection reason it suggests. Both numeric pins were updated together with a comment naming the key, and rdq stays at 0 — the load-bearing half, since a stray closer would make close === open true for the wrong reason.


Draft on purpose: the dispatching PM lands this.

Generated by Claude Code


Generated by Claude Code

)
`AiPendingActionsInbox` held its own relative-time helper returning hardcoded
English, and had no translation wiring of its own — the unwired-component shape.
Swept whole rather than converting only its timestamps: #7142 wired one string
into an otherwise untranslated component and shipped something visibly
half-done, and the triage ruling on this card carried that forward.
The four relative-time branches borrow `detail.justNow` / `minutesAgo` /
`hoursAgo` / `daysAgo`, already translated in all ten packs; cross-package
`detail.*` borrowing is settled convention here (ObjectGrid, ObjectKanban,
ObjectTree, ListView, ObjectView, NavigationOverlay, RecordDetailView). The rest
of the sweep needed copy no pack had, so `@object-ui/i18n` gains an
`aiApprovals` namespace — 38 keys, translated in all ten packs, deliberately
separate from `approvalsInbox` (the human approval-process inbox).
The five relative-time helpers are NOT unified: they differ in real behaviour
(`Math.round` vs `Math.floor`, 45s/30d vs 60s/7d, different tails). This copy's
arithmetic is untouched and three test rows exist only to pin it.
Assertions are in zh and ar — each key's `en` value is byte-identical to the
literal it replaced, so an `en`-only test is green before the fix too. The
provider-less leg lives in its own file because `createI18n` installs itself as
react-i18next's module-level global. No inline `defaultValue` (#3517).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NRRumy89BYdW9ogbcdHTho
@github-actions

Copy link
Copy Markdown
Contributor

❌ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3165.4 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-BeDRt1bU.js
StatusFAIL

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.

Which half objected:

Eager-closure halfVerdict
Aggregate closure ceiling✅ pass
Per-chunk ceilings❌ over its ceiling
Ceiling sensitivity (headroom)✅ pass
Ceiling freshness (checkout vs. base branch)✅ pass

📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)514.48KB117.36KB
core (index.js)5.55KB2.23KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.25KB61.73KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)70.02KB19.44KB
plugin-chatbot (index.js)193.26KB45.80KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)250.63KB63.90KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.78KB32.58KB
plugin-gantt (index.js)166.77KB40.76KB
plugin-grid (index.js)208.88KB56.59KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.51KB27.67KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

…mework chunk
Two consequences of translating AiPendingActionsInbox, both this PR's own.
`ConversationsSidebar.test.tsx` froze its `vi.mock('@object-ui/i18n', ...)`
factory to a hand-written object. Its import graph reaches `plugin-chatbot`,
which resolves `createSafeTranslation` at module scope, so the frozen surface
made that read `undefined` and the file died during COLLECTION — objectui#6849's
shape, which does not look like a test failure. It now spreads
`importOriginal()` and overrides only `useObjectTranslation`. Measured: of the
41 frozen `@object-ui/i18n` factories in the repo, running every one of them
showed this to be the only file whose graph reaches the package.
The ten pack blocks are locale DATA and land in the console's eager `framework`
chunk. Its ceiling moves 512,000 -> 524,000 gzipped bytes and its baseline is
re-pinned onto a fresh measurement, 502,405 -> 514,863. Attributed by three
console builds: the merge parent reads 510,192, this branch with the ten
`aiApprovals` blocks cut reads 510,192 again, and this branch reads 514,863, so
the whole 4,671-byte delta is the pack data and nothing else. Headroom is kept
at the line's own convention (9,137 bytes, 0.10x the regression the gate must
catch), not widened; the merge parent was already at 510,192 of the 512,000
allowed, so most of the overage was pre-existing drift.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NRRumy89BYdW9ogbcdHTho
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3165.5 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-IXjpYdG_.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)514.48KB117.36KB
core (index.js)5.55KB2.23KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.25KB61.73KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)70.02KB19.44KB
plugin-chatbot (index.js)193.26KB45.80KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)250.63KB63.90KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.78KB32.58KB
plugin-gantt (index.js)166.77KB40.76KB
plugin-grid (index.js)208.88KB56.59KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.51KB27.67KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-litant
os-litant marked this pull request as ready for review September 2, 2026 08:37
@os-litant
os-litant added this pull request to the merge queueSep 2, 2026
Merged via the queue into main with commit 01c9023Sep 2, 2026
32 checks passed
@os-litant
os-litant deleted the claude/issue-7173-ai-inbox-relative-time-i18n branch September 2, 2026 08:51
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

AiPendingActionsInbox.formatRelative is a fifth untranslated copy of the same relative-time helper — the keys it needs are already in all ten packs

2 participants

@os-litant@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(plugin-chatbot): translate the whole AI pending-actions inbox - #7330

Merged
os-litant merged 3 commits into
mainfrom
claude/issue-7173-ai-inbox-relative-time-i18n
Sep 2, 2026
Merged

fix(plugin-chatbot): translate the whole AI pending-actions inbox#7330
os-litant merged 3 commits into
mainfrom
claude/issue-7173-ai-inbox-relative-time-i18n

Conversation

@os-litant

Copy link
Copy Markdown
Collaborator

Fixes#7173

AiPendingActionsInbox held its own relative-time helper returning hardcoded English, so a zh / ja / ar session read English relative times on every row. The file also had no translation wiring of its own — the unwired-component shape, not the lookup-swap shape — so it is swept whole rather than having only its timestamps converted. That is the triage ruling of 2026-09-01 ("Sweep the file whole or leave it"), carrying forward what #7142 cost and #7149 had to pay back.

Verified on 5b4a3074d.

The namespace question was a measurement, and it came back non-empty

The triage ruled: establish whether cross-package key borrowing already has precedent in this repo, and follow it if it does. It does, overwhelmingly. Packages resolving detail.* from outside plugin-detail today:

packageexample call site
app-shellRecordDetailView.tsx (14 keys), RecordAttachmentsPanel.tsx (15), RecordApprovalsPanel.tsx, writeWarningToast.ts
plugin-listListView.tsx (9)
plugin-gridObjectGrid.tsxdetail.recordDetail sits in its own defaults map, the exact shape used here
plugin-viewObjectView.tsx (7)
plugin-kanbanObjectKanban.tsx (6)
plugin-treeObjectTree.tsx (5)
componentsnavigation-overlay.tsx (7), containers.tsx, RecordTitleChip.tsx, empty.tsx, action-menu.tsx
collaboration, fields, apps/consolefurther call sites

So the four relative-time branches borrow detail.justNow / minutesAgo / hoursAgo / daysAgo and no new pack rows are needed for them. Key presence re-verified by reading the pack OBJECTS, not a dotted grep (the packs are nested): 10/10 for each of the four, positive controls detail.back / detail.noActivity 10/10, negative control detail.zzzAbsentControl7163 0/10.

This does not escalate. No open question comes back.

The file census, and what each literal became

Every user-visible English literal in the 526-line file, in one table. Nothing is left half-wired.

surfaceliteralsresolution
card headerAI Approvals, the description sentenceaiApprovals.title / .description
tabsPending, Decided, AllaiApprovals.tabPending / .tabDecided / .tabAll
toolbarRefreshreused common.refresh
status badgesPending, Approved, Executed, Failed, RejectedaiApprovals.status*
outcome bannerApprove … for …: …, Reject …, OK, Failed, Executed, Rejected, Action failed during executionaiApprovals.outcomeApprove / .outcomeReject / .outcomeExecuteFailed, reused common.ok
empty stateNo actions waiting + its sentenceaiApprovals.emptyTitle / .emptyDescription
table headingsTool, Action, Object, Status, Proposed, DecisionaiApprovals.col*
row buttonsView, Approve, Reject, Working…aiApprovals.view / .approve / .reject / .working
drawer headerPending action, Loading…, Tool … on …aiApprovals.drawerFallbackTitle / .drawerSubtitle, reused common.loading
drawer fieldsStatus, Proposed, Proposed by, Decided by, Conversation, Tool input, Result, Error, Rejection reasonaiApprovals.colStatus / .colProposed / .field*
drawer buttonsApprove & Execute, RejectaiApprovals.approveAndExecute / .reject
reject dialogtitle, body, placeholder, Cancel, RejectaiApprovals.rejectTitle / .rejectBody / .rejectPlaceholder, reused common.cancel
relative timesjust now, {count}m ago, {count}h ago, {count}d agoborrowed detail.*

Deliberately not keyed: the placeholder for a null field, and the toLocaleDateString() tail past 30 days. Neither is English copy — the first is punctuation, the second is already locale-formatted, and the sibling helpers in plugin-detail leave their own tails unkeyed for the same reason.

38 new keys, translated in all ten packs (aiApprovals). It is deliberately separate from approvalsInbox, which is the human approval-process inbox: different surface, different feature, so no rows are shared with it. Four generic verbs are reused rather than forked.

The five helpers are NOT unified

Per the ruling. Math.round here against Math.floor in plugin-detail; thresholds 45s / 60min / 24h / 30d against 60s / 7d; different tails. The arithmetic is byte-for-byte unchanged and three test rows exist only to pin it, so a later "unification" cannot change it silently:

  • 50 s renders 1m ago — a 60 s threshold would still say "just now".
  • 90 s renders 2m agoMath.floor gives 1m ago.
  • 20 d renders 20d ago — a 7 d threshold would already have fallen through to a date.

Two assembled sentences became single interpolated keys

The outcome banner was {verb} for <code>{id}</code>: {message} and the drawer subtitle was Tool <code>{tool}</code> on <code>{object}</code> — English word order assembled from JSX fragments, which several locales cannot reorder. They are now aiApprovals.outcomeApprove (Approve for {{id}}: {{message}}) and aiApprovals.drawerSubtitle (Tool {{tool}} on {{object}}), following the repo's own precedent (approvalsInbox.inlineApproved).

The visible cost, stated rather than hidden: the three identifiers lose their <code> monospace styling. The id keeps its 8-character truncation. Restoring monospace would require splitting the sentence back into fragments, which is the defect.

Two smaller decisions, both forced by measurement

createSafeTranslation, not useSafeTranslate. The card requires the provider-less path to yield English from the defaults map and to interpolate {{count}}. useSafeTranslate(key, fallback) has no options argument, so {{count}} would reach the DOM raw; it is also the per-call-site inline-English shape #3517 rules out. createSafeTranslation is what plugin-detail, plugin-grid, plugin-kanban and components all use. No inline defaultValue anywhere.

Status labels are a switch over literal keys, not a map of labelKey values. Measured on this change: with the keys held as map values, check:i18n-dead-keys listed aiApprovals.statusPending and aiApprovals.statusApproved as needs-review — precisely the two whose only appearance was that map, while the three that also had a literal call site elsewhere were seen. That is #7149's lesson ("a key visible only as a map value has no call site the i18n scanners can resolve"). After the change the namespace is absent from the report entirely. The Badge variant stays a map, because a variant is not copy.

Evidence

An en-only test cannot discriminate here — every key's en value is byte-identical to the literal it replaced. So the load-bearing assertions are zh and ar.

Pre-fix red, measured on the unconverted file:Tests 8 failed | 4 passed (12). The four survivors were exactly the en / provider-less cases, which is the card's point restated as data. Sample failure: Unable to find an element with the text: 刚刚.

Ablation, after commit — hardcoded English put back into all four branches, then restored:

HEAD blob: cc690304a545194774da9a945c3d41cb42b90c79
injected 'ABLATION-7173': 4
deleted "t('detail.justNow')": 0
mutated blob: 77514218602baef1ba50b06d9bb74a0ed4ee63c4
× 8 of 14 cases red (all the zh/ar ones)
Tests 8 failed | 6 passed (14)
RESTORE OK: on-disk blob cc690304a545194774da9a945c3d41cb42b90c79 == HEAD blob cc690304a545194774da9a945c3d41cb42b90c79

No rebuild leg is needed for it: both suites import the component by relative source path (../AiPendingActionsInbox) and the root vitest config aliases @object-ui/i18n to packages/i18n/src, so nothing on the tested path resolves through dist/. The mutation was proven on disk by grep counts on both the injected and the deleted text plus a blob-hash change; the restore by blob-hash equality with the HEAD blob, under an EXIT INT TERM trap using absolute paths.

Why the provider-less leg is in its own file.createI18n installs itself as react-i18next's module-level global, so a provider-less render in a file that has already mounted an I18nProvider reads that pack instead of the defaults map. Measured while writing this: two mounts in one test produced two zh trees, and the "provider-less" assertion was quietly measuring the previous test's pack. Vitest isolates module state per file, so AiPendingActionsInbox.noProviderFallback.test.tsx never mounts a provider.

Runs, all at 5b4a3074d:

pnpm exec vitest run packages/plugin-chatbot/ packages/i18n/
→ Test Files 89 passed (89) · Tests 1336 passed (1336)
pnpm --filter @object-ui/plugin-chatbot run type-check → EXIT=0
pnpm --filter @object-ui/i18n run type-check → EXIT=0
(tsc -p tsconfig.test.json --listFiles confirms both new suites are IN the program —
3 hits for AiPendingActionsInbox, so this is not a "typecheck excludes tests" green)
pnpm check:i18n-keys → EXIT=0 "Every in-scope call-site key resolves against the en pack (2906 keys) …"
pnpm check:i18n-drift → EXIT=0 "0 en value(s) changed (38 key(s) added, 0 removed)"
pnpm check:i18n-dead-keys → EXIT=0 report-only; aiApprovals absent from the findings
pnpm check:control-bytes → EXIT=0 "scanned 6019 tracked text file(s)"
pnpm check:phantom-deps / check:self-import / check:side-effects-array / check:esm-specifiers → EXIT=0
node scripts/check-changeset-presence.mjs → EXIT=0 "15 source file(s) of 2 released package(s) … declares 1 changeset(s)"
node scripts/check-changeset-no-major.mjs → EXIT=0
node scripts/check-governed-queue-guard.mjs --test → "NOT GOVERNED — 16 path(s) checked"

Every exit code was captured by redirecting first (cmd > out 2>&1; EXIT=$?), never read across a pipe.

Repo-wide pnpm lint was narrowed, and the narrowing is declared. Ran eslint --no-inline-config --format json over the 16 changed files: 16 files linted, 0 errors, 0 warnings, the count read from the JSON output rather than assumed. The narrowing excludes nothing relevant: this repo's eslint.config.js enables no type-aware linting, so a diff cannot move the verdict on a file it does not touch. CI runs the full farm regardless.

One ratchet moved

packages/i18n/src/__tests__/de-quote-pairing-3876.test.ts: 58 → 59 matched German „…“ pairs. The de aiApprovals.rejectPlaceholder quotes the example rejection reason it suggests. Both numeric pins were updated together with a comment naming the key, and rdq stays at 0 — the load-bearing half, since a stray closer would make close === open true for the wrong reason.


Draft on purpose: the dispatching PM lands this.

Generated by Claude Code


Generated by Claude Code

)
`AiPendingActionsInbox` held its own relative-time helper returning hardcoded
English, and had no translation wiring of its own — the unwired-component shape.
Swept whole rather than converting only its timestamps: #7142 wired one string
into an otherwise untranslated component and shipped something visibly
half-done, and the triage ruling on this card carried that forward.
The four relative-time branches borrow `detail.justNow` / `minutesAgo` /
`hoursAgo` / `daysAgo`, already translated in all ten packs; cross-package
`detail.*` borrowing is settled convention here (ObjectGrid, ObjectKanban,
ObjectTree, ListView, ObjectView, NavigationOverlay, RecordDetailView). The rest
of the sweep needed copy no pack had, so `@object-ui/i18n` gains an
`aiApprovals` namespace — 38 keys, translated in all ten packs, deliberately
separate from `approvalsInbox` (the human approval-process inbox).
The five relative-time helpers are NOT unified: they differ in real behaviour
(`Math.round` vs `Math.floor`, 45s/30d vs 60s/7d, different tails). This copy's
arithmetic is untouched and three test rows exist only to pin it.
Assertions are in zh and ar — each key's `en` value is byte-identical to the
literal it replaced, so an `en`-only test is green before the fix too. The
provider-less leg lives in its own file because `createI18n` installs itself as
react-i18next's module-level global. No inline `defaultValue` (#3517).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NRRumy89BYdW9ogbcdHTho
@github-actions

Copy link
Copy Markdown
Contributor

❌ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3165.4 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-BeDRt1bU.js
StatusFAIL

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.

Which half objected:

Eager-closure halfVerdict
Aggregate closure ceiling✅ pass
Per-chunk ceilings❌ over its ceiling
Ceiling sensitivity (headroom)✅ pass
Ceiling freshness (checkout vs. base branch)✅ pass

📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)514.48KB117.36KB
core (index.js)5.55KB2.23KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.25KB61.73KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)70.02KB19.44KB
plugin-chatbot (index.js)193.26KB45.80KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)250.63KB63.90KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.78KB32.58KB
plugin-gantt (index.js)166.77KB40.76KB
plugin-grid (index.js)208.88KB56.59KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.51KB27.67KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

…mework chunk
Two consequences of translating AiPendingActionsInbox, both this PR's own.
`ConversationsSidebar.test.tsx` froze its `vi.mock('@object-ui/i18n', ...)`
factory to a hand-written object. Its import graph reaches `plugin-chatbot`,
which resolves `createSafeTranslation` at module scope, so the frozen surface
made that read `undefined` and the file died during COLLECTION — objectui#6849's
shape, which does not look like a test failure. It now spreads
`importOriginal()` and overrides only `useObjectTranslation`. Measured: of the
41 frozen `@object-ui/i18n` factories in the repo, running every one of them
showed this to be the only file whose graph reaches the package.
The ten pack blocks are locale DATA and land in the console's eager `framework`
chunk. Its ceiling moves 512,000 -> 524,000 gzipped bytes and its baseline is
re-pinned onto a fresh measurement, 502,405 -> 514,863. Attributed by three
console builds: the merge parent reads 510,192, this branch with the ten
`aiApprovals` blocks cut reads 510,192 again, and this branch reads 514,863, so
the whole 4,671-byte delta is the pack data and nothing else. Headroom is kept
at the line's own convention (9,137 bytes, 0.10x the regression the gate must
catch), not widened; the merge parent was already at 510,192 of the 512,000
allowed, so most of the overage was pre-existing drift.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NRRumy89BYdW9ogbcdHTho
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3165.5 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-IXjpYdG_.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)514.48KB117.36KB
core (index.js)5.55KB2.23KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.25KB61.73KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)70.02KB19.44KB
plugin-chatbot (index.js)193.26KB45.80KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)250.63KB63.90KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.78KB32.58KB
plugin-gantt (index.js)166.77KB40.76KB
plugin-grid (index.js)208.88KB56.59KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.51KB27.67KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-litant
os-litant marked this pull request as ready for review September 2, 2026 08:37
@os-litant
os-litant added this pull request to the merge queueSep 2, 2026
Merged via the queue into main with commit 01c9023Sep 2, 2026
32 checks passed
@os-litant
os-litant deleted the claude/issue-7173-ai-inbox-relative-time-i18n branch September 2, 2026 08:51
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

AiPendingActionsInbox.formatRelative is a fifth untranslated copy of the same relative-time helper — the keys it needs are already in all ten packs

2 participants

@os-litant@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

fix(plugin-chatbot): translate the whole AI pending-actions inbox - #7330

Merged
os-litant merged 3 commits into
mainfrom
claude/issue-7173-ai-inbox-relative-time-i18n
Sep 2, 2026
Merged

fix(plugin-chatbot): translate the whole AI pending-actions inbox#7330
os-litant merged 3 commits into
mainfrom
claude/issue-7173-ai-inbox-relative-time-i18n

Conversation

@os-litant

Copy link
Copy Markdown
Collaborator

Fixes#7173

AiPendingActionsInbox held its own relative-time helper returning hardcoded English, so a zh / ja / ar session read English relative times on every row. The file also had no translation wiring of its own — the unwired-component shape, not the lookup-swap shape — so it is swept whole rather than having only its timestamps converted. That is the triage ruling of 2026-09-01 ("Sweep the file whole or leave it"), carrying forward what #7142 cost and #7149 had to pay back.

Verified on 5b4a3074d.

The namespace question was a measurement, and it came back non-empty

The triage ruled: establish whether cross-package key borrowing already has precedent in this repo, and follow it if it does. It does, overwhelmingly. Packages resolving detail.* from outside plugin-detail today:

packageexample call site
app-shellRecordDetailView.tsx (14 keys), RecordAttachmentsPanel.tsx (15), RecordApprovalsPanel.tsx, writeWarningToast.ts
plugin-listListView.tsx (9)
plugin-gridObjectGrid.tsxdetail.recordDetail sits in its own defaults map, the exact shape used here
plugin-viewObjectView.tsx (7)
plugin-kanbanObjectKanban.tsx (6)
plugin-treeObjectTree.tsx (5)
componentsnavigation-overlay.tsx (7), containers.tsx, RecordTitleChip.tsx, empty.tsx, action-menu.tsx
collaboration, fields, apps/consolefurther call sites

So the four relative-time branches borrow detail.justNow / minutesAgo / hoursAgo / daysAgo and no new pack rows are needed for them. Key presence re-verified by reading the pack OBJECTS, not a dotted grep (the packs are nested): 10/10 for each of the four, positive controls detail.back / detail.noActivity 10/10, negative control detail.zzzAbsentControl7163 0/10.

This does not escalate. No open question comes back.

The file census, and what each literal became

Every user-visible English literal in the 526-line file, in one table. Nothing is left half-wired.

surfaceliteralsresolution
card headerAI Approvals, the description sentenceaiApprovals.title / .description
tabsPending, Decided, AllaiApprovals.tabPending / .tabDecided / .tabAll
toolbarRefreshreused common.refresh
status badgesPending, Approved, Executed, Failed, RejectedaiApprovals.status*
outcome bannerApprove … for …: …, Reject …, OK, Failed, Executed, Rejected, Action failed during executionaiApprovals.outcomeApprove / .outcomeReject / .outcomeExecuteFailed, reused common.ok
empty stateNo actions waiting + its sentenceaiApprovals.emptyTitle / .emptyDescription
table headingsTool, Action, Object, Status, Proposed, DecisionaiApprovals.col*
row buttonsView, Approve, Reject, Working…aiApprovals.view / .approve / .reject / .working
drawer headerPending action, Loading…, Tool … on …aiApprovals.drawerFallbackTitle / .drawerSubtitle, reused common.loading
drawer fieldsStatus, Proposed, Proposed by, Decided by, Conversation, Tool input, Result, Error, Rejection reasonaiApprovals.colStatus / .colProposed / .field*
drawer buttonsApprove & Execute, RejectaiApprovals.approveAndExecute / .reject
reject dialogtitle, body, placeholder, Cancel, RejectaiApprovals.rejectTitle / .rejectBody / .rejectPlaceholder, reused common.cancel
relative timesjust now, {count}m ago, {count}h ago, {count}d agoborrowed detail.*

Deliberately not keyed: the placeholder for a null field, and the toLocaleDateString() tail past 30 days. Neither is English copy — the first is punctuation, the second is already locale-formatted, and the sibling helpers in plugin-detail leave their own tails unkeyed for the same reason.

38 new keys, translated in all ten packs (aiApprovals). It is deliberately separate from approvalsInbox, which is the human approval-process inbox: different surface, different feature, so no rows are shared with it. Four generic verbs are reused rather than forked.

The five helpers are NOT unified

Per the ruling. Math.round here against Math.floor in plugin-detail; thresholds 45s / 60min / 24h / 30d against 60s / 7d; different tails. The arithmetic is byte-for-byte unchanged and three test rows exist only to pin it, so a later "unification" cannot change it silently:

  • 50 s renders 1m ago — a 60 s threshold would still say "just now".
  • 90 s renders 2m agoMath.floor gives 1m ago.
  • 20 d renders 20d ago — a 7 d threshold would already have fallen through to a date.

Two assembled sentences became single interpolated keys

The outcome banner was {verb} for <code>{id}</code>: {message} and the drawer subtitle was Tool <code>{tool}</code> on <code>{object}</code> — English word order assembled from JSX fragments, which several locales cannot reorder. They are now aiApprovals.outcomeApprove (Approve for {{id}}: {{message}}) and aiApprovals.drawerSubtitle (Tool {{tool}} on {{object}}), following the repo's own precedent (approvalsInbox.inlineApproved).

The visible cost, stated rather than hidden: the three identifiers lose their <code> monospace styling. The id keeps its 8-character truncation. Restoring monospace would require splitting the sentence back into fragments, which is the defect.

Two smaller decisions, both forced by measurement

createSafeTranslation, not useSafeTranslate. The card requires the provider-less path to yield English from the defaults map and to interpolate {{count}}. useSafeTranslate(key, fallback) has no options argument, so {{count}} would reach the DOM raw; it is also the per-call-site inline-English shape #3517 rules out. createSafeTranslation is what plugin-detail, plugin-grid, plugin-kanban and components all use. No inline defaultValue anywhere.

Status labels are a switch over literal keys, not a map of labelKey values. Measured on this change: with the keys held as map values, check:i18n-dead-keys listed aiApprovals.statusPending and aiApprovals.statusApproved as needs-review — precisely the two whose only appearance was that map, while the three that also had a literal call site elsewhere were seen. That is #7149's lesson ("a key visible only as a map value has no call site the i18n scanners can resolve"). After the change the namespace is absent from the report entirely. The Badge variant stays a map, because a variant is not copy.

Evidence

An en-only test cannot discriminate here — every key's en value is byte-identical to the literal it replaced. So the load-bearing assertions are zh and ar.

Pre-fix red, measured on the unconverted file:Tests 8 failed | 4 passed (12). The four survivors were exactly the en / provider-less cases, which is the card's point restated as data. Sample failure: Unable to find an element with the text: 刚刚.

Ablation, after commit — hardcoded English put back into all four branches, then restored:

HEAD blob: cc690304a545194774da9a945c3d41cb42b90c79
injected 'ABLATION-7173': 4
deleted "t('detail.justNow')": 0
mutated blob: 77514218602baef1ba50b06d9bb74a0ed4ee63c4
× 8 of 14 cases red (all the zh/ar ones)
Tests 8 failed | 6 passed (14)
RESTORE OK: on-disk blob cc690304a545194774da9a945c3d41cb42b90c79 == HEAD blob cc690304a545194774da9a945c3d41cb42b90c79

No rebuild leg is needed for it: both suites import the component by relative source path (../AiPendingActionsInbox) and the root vitest config aliases @object-ui/i18n to packages/i18n/src, so nothing on the tested path resolves through dist/. The mutation was proven on disk by grep counts on both the injected and the deleted text plus a blob-hash change; the restore by blob-hash equality with the HEAD blob, under an EXIT INT TERM trap using absolute paths.

Why the provider-less leg is in its own file.createI18n installs itself as react-i18next's module-level global, so a provider-less render in a file that has already mounted an I18nProvider reads that pack instead of the defaults map. Measured while writing this: two mounts in one test produced two zh trees, and the "provider-less" assertion was quietly measuring the previous test's pack. Vitest isolates module state per file, so AiPendingActionsInbox.noProviderFallback.test.tsx never mounts a provider.

Runs, all at 5b4a3074d:

pnpm exec vitest run packages/plugin-chatbot/ packages/i18n/
→ Test Files 89 passed (89) · Tests 1336 passed (1336)
pnpm --filter @object-ui/plugin-chatbot run type-check → EXIT=0
pnpm --filter @object-ui/i18n run type-check → EXIT=0
(tsc -p tsconfig.test.json --listFiles confirms both new suites are IN the program —
3 hits for AiPendingActionsInbox, so this is not a "typecheck excludes tests" green)
pnpm check:i18n-keys → EXIT=0 "Every in-scope call-site key resolves against the en pack (2906 keys) …"
pnpm check:i18n-drift → EXIT=0 "0 en value(s) changed (38 key(s) added, 0 removed)"
pnpm check:i18n-dead-keys → EXIT=0 report-only; aiApprovals absent from the findings
pnpm check:control-bytes → EXIT=0 "scanned 6019 tracked text file(s)"
pnpm check:phantom-deps / check:self-import / check:side-effects-array / check:esm-specifiers → EXIT=0
node scripts/check-changeset-presence.mjs → EXIT=0 "15 source file(s) of 2 released package(s) … declares 1 changeset(s)"
node scripts/check-changeset-no-major.mjs → EXIT=0
node scripts/check-governed-queue-guard.mjs --test → "NOT GOVERNED — 16 path(s) checked"

Every exit code was captured by redirecting first (cmd > out 2>&1; EXIT=$?), never read across a pipe.

Repo-wide pnpm lint was narrowed, and the narrowing is declared. Ran eslint --no-inline-config --format json over the 16 changed files: 16 files linted, 0 errors, 0 warnings, the count read from the JSON output rather than assumed. The narrowing excludes nothing relevant: this repo's eslint.config.js enables no type-aware linting, so a diff cannot move the verdict on a file it does not touch. CI runs the full farm regardless.

One ratchet moved

packages/i18n/src/__tests__/de-quote-pairing-3876.test.ts: 58 → 59 matched German „…“ pairs. The de aiApprovals.rejectPlaceholder quotes the example rejection reason it suggests. Both numeric pins were updated together with a comment naming the key, and rdq stays at 0 — the load-bearing half, since a stray closer would make close === open true for the wrong reason.


Draft on purpose: the dispatching PM lands this.

Generated by Claude Code


Generated by Claude Code

)
`AiPendingActionsInbox` held its own relative-time helper returning hardcoded
English, and had no translation wiring of its own — the unwired-component shape.
Swept whole rather than converting only its timestamps: #7142 wired one string
into an otherwise untranslated component and shipped something visibly
half-done, and the triage ruling on this card carried that forward.
The four relative-time branches borrow `detail.justNow` / `minutesAgo` /
`hoursAgo` / `daysAgo`, already translated in all ten packs; cross-package
`detail.*` borrowing is settled convention here (ObjectGrid, ObjectKanban,
ObjectTree, ListView, ObjectView, NavigationOverlay, RecordDetailView). The rest
of the sweep needed copy no pack had, so `@object-ui/i18n` gains an
`aiApprovals` namespace — 38 keys, translated in all ten packs, deliberately
separate from `approvalsInbox` (the human approval-process inbox).
The five relative-time helpers are NOT unified: they differ in real behaviour
(`Math.round` vs `Math.floor`, 45s/30d vs 60s/7d, different tails). This copy's
arithmetic is untouched and three test rows exist only to pin it.
Assertions are in zh and ar — each key's `en` value is byte-identical to the
literal it replaced, so an `en`-only test is green before the fix too. The
provider-less leg lives in its own file because `createI18n` installs itself as
react-i18next's module-level global. No inline `defaultValue` (#3517).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NRRumy89BYdW9ogbcdHTho
@github-actions

Copy link
Copy Markdown
Contributor

❌ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3165.4 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-BeDRt1bU.js
StatusFAIL

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.

Which half objected:

Eager-closure halfVerdict
Aggregate closure ceiling✅ pass
Per-chunk ceilings❌ over its ceiling
Ceiling sensitivity (headroom)✅ pass
Ceiling freshness (checkout vs. base branch)✅ pass

📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)514.48KB117.36KB
core (index.js)5.55KB2.23KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.25KB61.73KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)70.02KB19.44KB
plugin-chatbot (index.js)193.26KB45.80KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)250.63KB63.90KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.78KB32.58KB
plugin-gantt (index.js)166.77KB40.76KB
plugin-grid (index.js)208.88KB56.59KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.51KB27.67KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

…mework chunk
Two consequences of translating AiPendingActionsInbox, both this PR's own.
`ConversationsSidebar.test.tsx` froze its `vi.mock('@object-ui/i18n', ...)`
factory to a hand-written object. Its import graph reaches `plugin-chatbot`,
which resolves `createSafeTranslation` at module scope, so the frozen surface
made that read `undefined` and the file died during COLLECTION — objectui#6849's
shape, which does not look like a test failure. It now spreads
`importOriginal()` and overrides only `useObjectTranslation`. Measured: of the
41 frozen `@object-ui/i18n` factories in the repo, running every one of them
showed this to be the only file whose graph reaches the package.
The ten pack blocks are locale DATA and land in the console's eager `framework`
chunk. Its ceiling moves 512,000 -> 524,000 gzipped bytes and its baseline is
re-pinned onto a fresh measurement, 502,405 -> 514,863. Attributed by three
console builds: the merge parent reads 510,192, this branch with the ten
`aiApprovals` blocks cut reads 510,192 again, and this branch reads 514,863, so
the whole 4,671-byte delta is the pack data and nothing else. Headroom is kept
at the line's own convention (9,137 bytes, 0.10x the regression the gate must
catch), not widened; the merge parent was already at 510,192 of the 512,000
allowed, so most of the overage was pre-existing drift.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NRRumy89BYdW9ogbcdHTho
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3165.5 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-IXjpYdG_.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)514.48KB117.36KB
core (index.js)5.55KB2.23KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.25KB61.73KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)70.02KB19.44KB
plugin-chatbot (index.js)193.26KB45.80KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)250.63KB63.90KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.78KB32.58KB
plugin-gantt (index.js)166.77KB40.76KB
plugin-grid (index.js)208.88KB56.59KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.51KB27.67KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-litant
os-litant marked this pull request as ready for review September 2, 2026 08:37
@os-litant
os-litant added this pull request to the merge queueSep 2, 2026
Merged via the queue into main with commit 01c9023Sep 2, 2026
32 checks passed
@os-litant
os-litant deleted the claude/issue-7173-ai-inbox-relative-time-i18n branch September 2, 2026 08:51
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

AiPendingActionsInbox.formatRelative is a fifth untranslated copy of the same relative-time helper — the keys it needs are already in all ten packs

2 participants

@os-litant@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(plugin-chatbot): translate the whole AI pending-actions inbox - #7330

Merged
os-litant merged 3 commits into
mainfrom
claude/issue-7173-ai-inbox-relative-time-i18n
Sep 2, 2026
Merged

fix(plugin-chatbot): translate the whole AI pending-actions inbox#7330
os-litant merged 3 commits into
mainfrom
claude/issue-7173-ai-inbox-relative-time-i18n

Conversation

@os-litant

Copy link
Copy Markdown
Collaborator

Fixes#7173

AiPendingActionsInbox held its own relative-time helper returning hardcoded English, so a zh / ja / ar session read English relative times on every row. The file also had no translation wiring of its own — the unwired-component shape, not the lookup-swap shape — so it is swept whole rather than having only its timestamps converted. That is the triage ruling of 2026-09-01 ("Sweep the file whole or leave it"), carrying forward what #7142 cost and #7149 had to pay back.

Verified on 5b4a3074d.

The namespace question was a measurement, and it came back non-empty

The triage ruled: establish whether cross-package key borrowing already has precedent in this repo, and follow it if it does. It does, overwhelmingly. Packages resolving detail.* from outside plugin-detail today:

packageexample call site
app-shellRecordDetailView.tsx (14 keys), RecordAttachmentsPanel.tsx (15), RecordApprovalsPanel.tsx, writeWarningToast.ts
plugin-listListView.tsx (9)
plugin-gridObjectGrid.tsxdetail.recordDetail sits in its own defaults map, the exact shape used here
plugin-viewObjectView.tsx (7)
plugin-kanbanObjectKanban.tsx (6)
plugin-treeObjectTree.tsx (5)
componentsnavigation-overlay.tsx (7), containers.tsx, RecordTitleChip.tsx, empty.tsx, action-menu.tsx
collaboration, fields, apps/consolefurther call sites

So the four relative-time branches borrow detail.justNow / minutesAgo / hoursAgo / daysAgo and no new pack rows are needed for them. Key presence re-verified by reading the pack OBJECTS, not a dotted grep (the packs are nested): 10/10 for each of the four, positive controls detail.back / detail.noActivity 10/10, negative control detail.zzzAbsentControl7163 0/10.

This does not escalate. No open question comes back.

The file census, and what each literal became

Every user-visible English literal in the 526-line file, in one table. Nothing is left half-wired.

surfaceliteralsresolution
card headerAI Approvals, the description sentenceaiApprovals.title / .description
tabsPending, Decided, AllaiApprovals.tabPending / .tabDecided / .tabAll
toolbarRefreshreused common.refresh
status badgesPending, Approved, Executed, Failed, RejectedaiApprovals.status*
outcome bannerApprove … for …: …, Reject …, OK, Failed, Executed, Rejected, Action failed during executionaiApprovals.outcomeApprove / .outcomeReject / .outcomeExecuteFailed, reused common.ok
empty stateNo actions waiting + its sentenceaiApprovals.emptyTitle / .emptyDescription
table headingsTool, Action, Object, Status, Proposed, DecisionaiApprovals.col*
row buttonsView, Approve, Reject, Working…aiApprovals.view / .approve / .reject / .working
drawer headerPending action, Loading…, Tool … on …aiApprovals.drawerFallbackTitle / .drawerSubtitle, reused common.loading
drawer fieldsStatus, Proposed, Proposed by, Decided by, Conversation, Tool input, Result, Error, Rejection reasonaiApprovals.colStatus / .colProposed / .field*
drawer buttonsApprove & Execute, RejectaiApprovals.approveAndExecute / .reject
reject dialogtitle, body, placeholder, Cancel, RejectaiApprovals.rejectTitle / .rejectBody / .rejectPlaceholder, reused common.cancel
relative timesjust now, {count}m ago, {count}h ago, {count}d agoborrowed detail.*

Deliberately not keyed: the placeholder for a null field, and the toLocaleDateString() tail past 30 days. Neither is English copy — the first is punctuation, the second is already locale-formatted, and the sibling helpers in plugin-detail leave their own tails unkeyed for the same reason.

38 new keys, translated in all ten packs (aiApprovals). It is deliberately separate from approvalsInbox, which is the human approval-process inbox: different surface, different feature, so no rows are shared with it. Four generic verbs are reused rather than forked.

The five helpers are NOT unified

Per the ruling. Math.round here against Math.floor in plugin-detail; thresholds 45s / 60min / 24h / 30d against 60s / 7d; different tails. The arithmetic is byte-for-byte unchanged and three test rows exist only to pin it, so a later "unification" cannot change it silently:

  • 50 s renders 1m ago — a 60 s threshold would still say "just now".
  • 90 s renders 2m agoMath.floor gives 1m ago.
  • 20 d renders 20d ago — a 7 d threshold would already have fallen through to a date.

Two assembled sentences became single interpolated keys

The outcome banner was {verb} for <code>{id}</code>: {message} and the drawer subtitle was Tool <code>{tool}</code> on <code>{object}</code> — English word order assembled from JSX fragments, which several locales cannot reorder. They are now aiApprovals.outcomeApprove (Approve for {{id}}: {{message}}) and aiApprovals.drawerSubtitle (Tool {{tool}} on {{object}}), following the repo's own precedent (approvalsInbox.inlineApproved).

The visible cost, stated rather than hidden: the three identifiers lose their <code> monospace styling. The id keeps its 8-character truncation. Restoring monospace would require splitting the sentence back into fragments, which is the defect.

Two smaller decisions, both forced by measurement

createSafeTranslation, not useSafeTranslate. The card requires the provider-less path to yield English from the defaults map and to interpolate {{count}}. useSafeTranslate(key, fallback) has no options argument, so {{count}} would reach the DOM raw; it is also the per-call-site inline-English shape #3517 rules out. createSafeTranslation is what plugin-detail, plugin-grid, plugin-kanban and components all use. No inline defaultValue anywhere.

Status labels are a switch over literal keys, not a map of labelKey values. Measured on this change: with the keys held as map values, check:i18n-dead-keys listed aiApprovals.statusPending and aiApprovals.statusApproved as needs-review — precisely the two whose only appearance was that map, while the three that also had a literal call site elsewhere were seen. That is #7149's lesson ("a key visible only as a map value has no call site the i18n scanners can resolve"). After the change the namespace is absent from the report entirely. The Badge variant stays a map, because a variant is not copy.

Evidence

An en-only test cannot discriminate here — every key's en value is byte-identical to the literal it replaced. So the load-bearing assertions are zh and ar.

Pre-fix red, measured on the unconverted file:Tests 8 failed | 4 passed (12). The four survivors were exactly the en / provider-less cases, which is the card's point restated as data. Sample failure: Unable to find an element with the text: 刚刚.

Ablation, after commit — hardcoded English put back into all four branches, then restored:

HEAD blob: cc690304a545194774da9a945c3d41cb42b90c79
injected 'ABLATION-7173': 4
deleted "t('detail.justNow')": 0
mutated blob: 77514218602baef1ba50b06d9bb74a0ed4ee63c4
× 8 of 14 cases red (all the zh/ar ones)
Tests 8 failed | 6 passed (14)
RESTORE OK: on-disk blob cc690304a545194774da9a945c3d41cb42b90c79 == HEAD blob cc690304a545194774da9a945c3d41cb42b90c79

No rebuild leg is needed for it: both suites import the component by relative source path (../AiPendingActionsInbox) and the root vitest config aliases @object-ui/i18n to packages/i18n/src, so nothing on the tested path resolves through dist/. The mutation was proven on disk by grep counts on both the injected and the deleted text plus a blob-hash change; the restore by blob-hash equality with the HEAD blob, under an EXIT INT TERM trap using absolute paths.

Why the provider-less leg is in its own file.createI18n installs itself as react-i18next's module-level global, so a provider-less render in a file that has already mounted an I18nProvider reads that pack instead of the defaults map. Measured while writing this: two mounts in one test produced two zh trees, and the "provider-less" assertion was quietly measuring the previous test's pack. Vitest isolates module state per file, so AiPendingActionsInbox.noProviderFallback.test.tsx never mounts a provider.

Runs, all at 5b4a3074d:

pnpm exec vitest run packages/plugin-chatbot/ packages/i18n/
→ Test Files 89 passed (89) · Tests 1336 passed (1336)
pnpm --filter @object-ui/plugin-chatbot run type-check → EXIT=0
pnpm --filter @object-ui/i18n run type-check → EXIT=0
(tsc -p tsconfig.test.json --listFiles confirms both new suites are IN the program —
3 hits for AiPendingActionsInbox, so this is not a "typecheck excludes tests" green)
pnpm check:i18n-keys → EXIT=0 "Every in-scope call-site key resolves against the en pack (2906 keys) …"
pnpm check:i18n-drift → EXIT=0 "0 en value(s) changed (38 key(s) added, 0 removed)"
pnpm check:i18n-dead-keys → EXIT=0 report-only; aiApprovals absent from the findings
pnpm check:control-bytes → EXIT=0 "scanned 6019 tracked text file(s)"
pnpm check:phantom-deps / check:self-import / check:side-effects-array / check:esm-specifiers → EXIT=0
node scripts/check-changeset-presence.mjs → EXIT=0 "15 source file(s) of 2 released package(s) … declares 1 changeset(s)"
node scripts/check-changeset-no-major.mjs → EXIT=0
node scripts/check-governed-queue-guard.mjs --test → "NOT GOVERNED — 16 path(s) checked"

Every exit code was captured by redirecting first (cmd > out 2>&1; EXIT=$?), never read across a pipe.

Repo-wide pnpm lint was narrowed, and the narrowing is declared. Ran eslint --no-inline-config --format json over the 16 changed files: 16 files linted, 0 errors, 0 warnings, the count read from the JSON output rather than assumed. The narrowing excludes nothing relevant: this repo's eslint.config.js enables no type-aware linting, so a diff cannot move the verdict on a file it does not touch. CI runs the full farm regardless.

One ratchet moved

packages/i18n/src/__tests__/de-quote-pairing-3876.test.ts: 58 → 59 matched German „…“ pairs. The de aiApprovals.rejectPlaceholder quotes the example rejection reason it suggests. Both numeric pins were updated together with a comment naming the key, and rdq stays at 0 — the load-bearing half, since a stray closer would make close === open true for the wrong reason.


Draft on purpose: the dispatching PM lands this.

Generated by Claude Code


Generated by Claude Code

)
`AiPendingActionsInbox` held its own relative-time helper returning hardcoded
English, and had no translation wiring of its own — the unwired-component shape.
Swept whole rather than converting only its timestamps: #7142 wired one string
into an otherwise untranslated component and shipped something visibly
half-done, and the triage ruling on this card carried that forward.
The four relative-time branches borrow `detail.justNow` / `minutesAgo` /
`hoursAgo` / `daysAgo`, already translated in all ten packs; cross-package
`detail.*` borrowing is settled convention here (ObjectGrid, ObjectKanban,
ObjectTree, ListView, ObjectView, NavigationOverlay, RecordDetailView). The rest
of the sweep needed copy no pack had, so `@object-ui/i18n` gains an
`aiApprovals` namespace — 38 keys, translated in all ten packs, deliberately
separate from `approvalsInbox` (the human approval-process inbox).
The five relative-time helpers are NOT unified: they differ in real behaviour
(`Math.round` vs `Math.floor`, 45s/30d vs 60s/7d, different tails). This copy's
arithmetic is untouched and three test rows exist only to pin it.
Assertions are in zh and ar — each key's `en` value is byte-identical to the
literal it replaced, so an `en`-only test is green before the fix too. The
provider-less leg lives in its own file because `createI18n` installs itself as
react-i18next's module-level global. No inline `defaultValue` (#3517).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NRRumy89BYdW9ogbcdHTho
@github-actions

Copy link
Copy Markdown
Contributor

❌ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3165.4 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-BeDRt1bU.js
StatusFAIL

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.

Which half objected:

Eager-closure halfVerdict
Aggregate closure ceiling✅ pass
Per-chunk ceilings❌ over its ceiling
Ceiling sensitivity (headroom)✅ pass
Ceiling freshness (checkout vs. base branch)✅ pass

📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)514.48KB117.36KB
core (index.js)5.55KB2.23KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.25KB61.73KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)70.02KB19.44KB
plugin-chatbot (index.js)193.26KB45.80KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)250.63KB63.90KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.78KB32.58KB
plugin-gantt (index.js)166.77KB40.76KB
plugin-grid (index.js)208.88KB56.59KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.51KB27.67KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

…mework chunk
Two consequences of translating AiPendingActionsInbox, both this PR's own.
`ConversationsSidebar.test.tsx` froze its `vi.mock('@object-ui/i18n', ...)`
factory to a hand-written object. Its import graph reaches `plugin-chatbot`,
which resolves `createSafeTranslation` at module scope, so the frozen surface
made that read `undefined` and the file died during COLLECTION — objectui#6849's
shape, which does not look like a test failure. It now spreads
`importOriginal()` and overrides only `useObjectTranslation`. Measured: of the
41 frozen `@object-ui/i18n` factories in the repo, running every one of them
showed this to be the only file whose graph reaches the package.
The ten pack blocks are locale DATA and land in the console's eager `framework`
chunk. Its ceiling moves 512,000 -> 524,000 gzipped bytes and its baseline is
re-pinned onto a fresh measurement, 502,405 -> 514,863. Attributed by three
console builds: the merge parent reads 510,192, this branch with the ten
`aiApprovals` blocks cut reads 510,192 again, and this branch reads 514,863, so
the whole 4,671-byte delta is the pack data and nothing else. Headroom is kept
at the line's own convention (9,137 bytes, 0.10x the regression the gate must
catch), not widened; the merge parent was already at 510,192 of the 512,000
allowed, so most of the overage was pre-existing drift.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NRRumy89BYdW9ogbcdHTho
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3165.5 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-IXjpYdG_.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)514.48KB117.36KB
core (index.js)5.55KB2.23KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.25KB61.73KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)70.02KB19.44KB
plugin-chatbot (index.js)193.26KB45.80KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)250.63KB63.90KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.78KB32.58KB
plugin-gantt (index.js)166.77KB40.76KB
plugin-grid (index.js)208.88KB56.59KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.51KB27.67KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-litant
os-litant marked this pull request as ready for review September 2, 2026 08:37
@os-litant
os-litant added this pull request to the merge queueSep 2, 2026
Merged via the queue into main with commit 01c9023Sep 2, 2026
32 checks passed
@os-litant
os-litant deleted the claude/issue-7173-ai-inbox-relative-time-i18n branch September 2, 2026 08:51
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

AiPendingActionsInbox.formatRelative is a fifth untranslated copy of the same relative-time helper — the keys it needs are already in all ten packs

2 participants

@os-litant@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(plugin-chatbot): translate the whole AI pending-actions inbox - #7330

Merged
os-litant merged 3 commits into
mainfrom
claude/issue-7173-ai-inbox-relative-time-i18n
Sep 2, 2026
Merged

fix(plugin-chatbot): translate the whole AI pending-actions inbox#7330
os-litant merged 3 commits into
mainfrom
claude/issue-7173-ai-inbox-relative-time-i18n

Conversation

@os-litant

Copy link
Copy Markdown
Collaborator

Fixes#7173

AiPendingActionsInbox held its own relative-time helper returning hardcoded English, so a zh / ja / ar session read English relative times on every row. The file also had no translation wiring of its own — the unwired-component shape, not the lookup-swap shape — so it is swept whole rather than having only its timestamps converted. That is the triage ruling of 2026-09-01 ("Sweep the file whole or leave it"), carrying forward what #7142 cost and #7149 had to pay back.

Verified on 5b4a3074d.

The namespace question was a measurement, and it came back non-empty

The triage ruled: establish whether cross-package key borrowing already has precedent in this repo, and follow it if it does. It does, overwhelmingly. Packages resolving detail.* from outside plugin-detail today:

packageexample call site
app-shellRecordDetailView.tsx (14 keys), RecordAttachmentsPanel.tsx (15), RecordApprovalsPanel.tsx, writeWarningToast.ts
plugin-listListView.tsx (9)
plugin-gridObjectGrid.tsxdetail.recordDetail sits in its own defaults map, the exact shape used here
plugin-viewObjectView.tsx (7)
plugin-kanbanObjectKanban.tsx (6)
plugin-treeObjectTree.tsx (5)
componentsnavigation-overlay.tsx (7), containers.tsx, RecordTitleChip.tsx, empty.tsx, action-menu.tsx
collaboration, fields, apps/consolefurther call sites

So the four relative-time branches borrow detail.justNow / minutesAgo / hoursAgo / daysAgo and no new pack rows are needed for them. Key presence re-verified by reading the pack OBJECTS, not a dotted grep (the packs are nested): 10/10 for each of the four, positive controls detail.back / detail.noActivity 10/10, negative control detail.zzzAbsentControl7163 0/10.

This does not escalate. No open question comes back.

The file census, and what each literal became

Every user-visible English literal in the 526-line file, in one table. Nothing is left half-wired.

surfaceliteralsresolution
card headerAI Approvals, the description sentenceaiApprovals.title / .description
tabsPending, Decided, AllaiApprovals.tabPending / .tabDecided / .tabAll
toolbarRefreshreused common.refresh
status badgesPending, Approved, Executed, Failed, RejectedaiApprovals.status*
outcome bannerApprove … for …: …, Reject …, OK, Failed, Executed, Rejected, Action failed during executionaiApprovals.outcomeApprove / .outcomeReject / .outcomeExecuteFailed, reused common.ok
empty stateNo actions waiting + its sentenceaiApprovals.emptyTitle / .emptyDescription
table headingsTool, Action, Object, Status, Proposed, DecisionaiApprovals.col*
row buttonsView, Approve, Reject, Working…aiApprovals.view / .approve / .reject / .working
drawer headerPending action, Loading…, Tool … on …aiApprovals.drawerFallbackTitle / .drawerSubtitle, reused common.loading
drawer fieldsStatus, Proposed, Proposed by, Decided by, Conversation, Tool input, Result, Error, Rejection reasonaiApprovals.colStatus / .colProposed / .field*
drawer buttonsApprove & Execute, RejectaiApprovals.approveAndExecute / .reject
reject dialogtitle, body, placeholder, Cancel, RejectaiApprovals.rejectTitle / .rejectBody / .rejectPlaceholder, reused common.cancel
relative timesjust now, {count}m ago, {count}h ago, {count}d agoborrowed detail.*

Deliberately not keyed: the placeholder for a null field, and the toLocaleDateString() tail past 30 days. Neither is English copy — the first is punctuation, the second is already locale-formatted, and the sibling helpers in plugin-detail leave their own tails unkeyed for the same reason.

38 new keys, translated in all ten packs (aiApprovals). It is deliberately separate from approvalsInbox, which is the human approval-process inbox: different surface, different feature, so no rows are shared with it. Four generic verbs are reused rather than forked.

The five helpers are NOT unified

Per the ruling. Math.round here against Math.floor in plugin-detail; thresholds 45s / 60min / 24h / 30d against 60s / 7d; different tails. The arithmetic is byte-for-byte unchanged and three test rows exist only to pin it, so a later "unification" cannot change it silently:

  • 50 s renders 1m ago — a 60 s threshold would still say "just now".
  • 90 s renders 2m agoMath.floor gives 1m ago.
  • 20 d renders 20d ago — a 7 d threshold would already have fallen through to a date.

Two assembled sentences became single interpolated keys

The outcome banner was {verb} for <code>{id}</code>: {message} and the drawer subtitle was Tool <code>{tool}</code> on <code>{object}</code> — English word order assembled from JSX fragments, which several locales cannot reorder. They are now aiApprovals.outcomeApprove (Approve for {{id}}: {{message}}) and aiApprovals.drawerSubtitle (Tool {{tool}} on {{object}}), following the repo's own precedent (approvalsInbox.inlineApproved).

The visible cost, stated rather than hidden: the three identifiers lose their <code> monospace styling. The id keeps its 8-character truncation. Restoring monospace would require splitting the sentence back into fragments, which is the defect.

Two smaller decisions, both forced by measurement

createSafeTranslation, not useSafeTranslate. The card requires the provider-less path to yield English from the defaults map and to interpolate {{count}}. useSafeTranslate(key, fallback) has no options argument, so {{count}} would reach the DOM raw; it is also the per-call-site inline-English shape #3517 rules out. createSafeTranslation is what plugin-detail, plugin-grid, plugin-kanban and components all use. No inline defaultValue anywhere.

Status labels are a switch over literal keys, not a map of labelKey values. Measured on this change: with the keys held as map values, check:i18n-dead-keys listed aiApprovals.statusPending and aiApprovals.statusApproved as needs-review — precisely the two whose only appearance was that map, while the three that also had a literal call site elsewhere were seen. That is #7149's lesson ("a key visible only as a map value has no call site the i18n scanners can resolve"). After the change the namespace is absent from the report entirely. The Badge variant stays a map, because a variant is not copy.

Evidence

An en-only test cannot discriminate here — every key's en value is byte-identical to the literal it replaced. So the load-bearing assertions are zh and ar.

Pre-fix red, measured on the unconverted file:Tests 8 failed | 4 passed (12). The four survivors were exactly the en / provider-less cases, which is the card's point restated as data. Sample failure: Unable to find an element with the text: 刚刚.

Ablation, after commit — hardcoded English put back into all four branches, then restored:

HEAD blob: cc690304a545194774da9a945c3d41cb42b90c79
injected 'ABLATION-7173': 4
deleted "t('detail.justNow')": 0
mutated blob: 77514218602baef1ba50b06d9bb74a0ed4ee63c4
× 8 of 14 cases red (all the zh/ar ones)
Tests 8 failed | 6 passed (14)
RESTORE OK: on-disk blob cc690304a545194774da9a945c3d41cb42b90c79 == HEAD blob cc690304a545194774da9a945c3d41cb42b90c79

No rebuild leg is needed for it: both suites import the component by relative source path (../AiPendingActionsInbox) and the root vitest config aliases @object-ui/i18n to packages/i18n/src, so nothing on the tested path resolves through dist/. The mutation was proven on disk by grep counts on both the injected and the deleted text plus a blob-hash change; the restore by blob-hash equality with the HEAD blob, under an EXIT INT TERM trap using absolute paths.

Why the provider-less leg is in its own file.createI18n installs itself as react-i18next's module-level global, so a provider-less render in a file that has already mounted an I18nProvider reads that pack instead of the defaults map. Measured while writing this: two mounts in one test produced two zh trees, and the "provider-less" assertion was quietly measuring the previous test's pack. Vitest isolates module state per file, so AiPendingActionsInbox.noProviderFallback.test.tsx never mounts a provider.

Runs, all at 5b4a3074d:

pnpm exec vitest run packages/plugin-chatbot/ packages/i18n/
→ Test Files 89 passed (89) · Tests 1336 passed (1336)
pnpm --filter @object-ui/plugin-chatbot run type-check → EXIT=0
pnpm --filter @object-ui/i18n run type-check → EXIT=0
(tsc -p tsconfig.test.json --listFiles confirms both new suites are IN the program —
3 hits for AiPendingActionsInbox, so this is not a "typecheck excludes tests" green)
pnpm check:i18n-keys → EXIT=0 "Every in-scope call-site key resolves against the en pack (2906 keys) …"
pnpm check:i18n-drift → EXIT=0 "0 en value(s) changed (38 key(s) added, 0 removed)"
pnpm check:i18n-dead-keys → EXIT=0 report-only; aiApprovals absent from the findings
pnpm check:control-bytes → EXIT=0 "scanned 6019 tracked text file(s)"
pnpm check:phantom-deps / check:self-import / check:side-effects-array / check:esm-specifiers → EXIT=0
node scripts/check-changeset-presence.mjs → EXIT=0 "15 source file(s) of 2 released package(s) … declares 1 changeset(s)"
node scripts/check-changeset-no-major.mjs → EXIT=0
node scripts/check-governed-queue-guard.mjs --test → "NOT GOVERNED — 16 path(s) checked"

Every exit code was captured by redirecting first (cmd > out 2>&1; EXIT=$?), never read across a pipe.

Repo-wide pnpm lint was narrowed, and the narrowing is declared. Ran eslint --no-inline-config --format json over the 16 changed files: 16 files linted, 0 errors, 0 warnings, the count read from the JSON output rather than assumed. The narrowing excludes nothing relevant: this repo's eslint.config.js enables no type-aware linting, so a diff cannot move the verdict on a file it does not touch. CI runs the full farm regardless.

One ratchet moved

packages/i18n/src/__tests__/de-quote-pairing-3876.test.ts: 58 → 59 matched German „…“ pairs. The de aiApprovals.rejectPlaceholder quotes the example rejection reason it suggests. Both numeric pins were updated together with a comment naming the key, and rdq stays at 0 — the load-bearing half, since a stray closer would make close === open true for the wrong reason.


Draft on purpose: the dispatching PM lands this.

Generated by Claude Code


Generated by Claude Code

)
`AiPendingActionsInbox` held its own relative-time helper returning hardcoded
English, and had no translation wiring of its own — the unwired-component shape.
Swept whole rather than converting only its timestamps: #7142 wired one string
into an otherwise untranslated component and shipped something visibly
half-done, and the triage ruling on this card carried that forward.
The four relative-time branches borrow `detail.justNow` / `minutesAgo` /
`hoursAgo` / `daysAgo`, already translated in all ten packs; cross-package
`detail.*` borrowing is settled convention here (ObjectGrid, ObjectKanban,
ObjectTree, ListView, ObjectView, NavigationOverlay, RecordDetailView). The rest
of the sweep needed copy no pack had, so `@object-ui/i18n` gains an
`aiApprovals` namespace — 38 keys, translated in all ten packs, deliberately
separate from `approvalsInbox` (the human approval-process inbox).
The five relative-time helpers are NOT unified: they differ in real behaviour
(`Math.round` vs `Math.floor`, 45s/30d vs 60s/7d, different tails). This copy's
arithmetic is untouched and three test rows exist only to pin it.
Assertions are in zh and ar — each key's `en` value is byte-identical to the
literal it replaced, so an `en`-only test is green before the fix too. The
provider-less leg lives in its own file because `createI18n` installs itself as
react-i18next's module-level global. No inline `defaultValue` (#3517).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NRRumy89BYdW9ogbcdHTho
@github-actions

Copy link
Copy Markdown
Contributor

❌ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3165.4 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-BeDRt1bU.js
StatusFAIL

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.

Which half objected:

Eager-closure halfVerdict
Aggregate closure ceiling✅ pass
Per-chunk ceilings❌ over its ceiling
Ceiling sensitivity (headroom)✅ pass
Ceiling freshness (checkout vs. base branch)✅ pass

📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)514.48KB117.36KB
core (index.js)5.55KB2.23KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.25KB61.73KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)70.02KB19.44KB
plugin-chatbot (index.js)193.26KB45.80KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)250.63KB63.90KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.78KB32.58KB
plugin-gantt (index.js)166.77KB40.76KB
plugin-grid (index.js)208.88KB56.59KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.51KB27.67KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

…mework chunk
Two consequences of translating AiPendingActionsInbox, both this PR's own.
`ConversationsSidebar.test.tsx` froze its `vi.mock('@object-ui/i18n', ...)`
factory to a hand-written object. Its import graph reaches `plugin-chatbot`,
which resolves `createSafeTranslation` at module scope, so the frozen surface
made that read `undefined` and the file died during COLLECTION — objectui#6849's
shape, which does not look like a test failure. It now spreads
`importOriginal()` and overrides only `useObjectTranslation`. Measured: of the
41 frozen `@object-ui/i18n` factories in the repo, running every one of them
showed this to be the only file whose graph reaches the package.
The ten pack blocks are locale DATA and land in the console's eager `framework`
chunk. Its ceiling moves 512,000 -> 524,000 gzipped bytes and its baseline is
re-pinned onto a fresh measurement, 502,405 -> 514,863. Attributed by three
console builds: the merge parent reads 510,192, this branch with the ten
`aiApprovals` blocks cut reads 510,192 again, and this branch reads 514,863, so
the whole 4,671-byte delta is the pack data and nothing else. Headroom is kept
at the line's own convention (9,137 bytes, 0.10x the regression the gate must
catch), not widened; the merge parent was already at 510,192 of the 512,000
allowed, so most of the overage was pre-existing drift.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NRRumy89BYdW9ogbcdHTho
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3165.5 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-IXjpYdG_.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)514.48KB117.36KB
core (index.js)5.55KB2.23KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.25KB61.73KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)70.02KB19.44KB
plugin-chatbot (index.js)193.26KB45.80KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)250.63KB63.90KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.78KB32.58KB
plugin-gantt (index.js)166.77KB40.76KB
plugin-grid (index.js)208.88KB56.59KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.51KB27.67KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-litant
os-litant marked this pull request as ready for review September 2, 2026 08:37
@os-litant
os-litant added this pull request to the merge queueSep 2, 2026
Merged via the queue into main with commit 01c9023Sep 2, 2026
32 checks passed
@os-litant
os-litant deleted the claude/issue-7173-ai-inbox-relative-time-i18n branch September 2, 2026 08:51
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

AiPendingActionsInbox.formatRelative is a fifth untranslated copy of the same relative-time helper — the keys it needs are already in all ten packs

2 participants

@os-litant@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

fix(plugin-chatbot): translate the whole AI pending-actions inbox - #7330

Merged
os-litant merged 3 commits into
mainfrom
claude/issue-7173-ai-inbox-relative-time-i18n
Sep 2, 2026
Merged

fix(plugin-chatbot): translate the whole AI pending-actions inbox#7330
os-litant merged 3 commits into
mainfrom
claude/issue-7173-ai-inbox-relative-time-i18n

Conversation

@os-litant

Copy link
Copy Markdown
Collaborator

Fixes#7173

AiPendingActionsInbox held its own relative-time helper returning hardcoded English, so a zh / ja / ar session read English relative times on every row. The file also had no translation wiring of its own — the unwired-component shape, not the lookup-swap shape — so it is swept whole rather than having only its timestamps converted. That is the triage ruling of 2026-09-01 ("Sweep the file whole or leave it"), carrying forward what #7142 cost and #7149 had to pay back.

Verified on 5b4a3074d.

The namespace question was a measurement, and it came back non-empty

The triage ruled: establish whether cross-package key borrowing already has precedent in this repo, and follow it if it does. It does, overwhelmingly. Packages resolving detail.* from outside plugin-detail today:

packageexample call site
app-shellRecordDetailView.tsx (14 keys), RecordAttachmentsPanel.tsx (15), RecordApprovalsPanel.tsx, writeWarningToast.ts
plugin-listListView.tsx (9)
plugin-gridObjectGrid.tsxdetail.recordDetail sits in its own defaults map, the exact shape used here
plugin-viewObjectView.tsx (7)
plugin-kanbanObjectKanban.tsx (6)
plugin-treeObjectTree.tsx (5)
componentsnavigation-overlay.tsx (7), containers.tsx, RecordTitleChip.tsx, empty.tsx, action-menu.tsx
collaboration, fields, apps/consolefurther call sites

So the four relative-time branches borrow detail.justNow / minutesAgo / hoursAgo / daysAgo and no new pack rows are needed for them. Key presence re-verified by reading the pack OBJECTS, not a dotted grep (the packs are nested): 10/10 for each of the four, positive controls detail.back / detail.noActivity 10/10, negative control detail.zzzAbsentControl7163 0/10.

This does not escalate. No open question comes back.

The file census, and what each literal became

Every user-visible English literal in the 526-line file, in one table. Nothing is left half-wired.

surfaceliteralsresolution
card headerAI Approvals, the description sentenceaiApprovals.title / .description
tabsPending, Decided, AllaiApprovals.tabPending / .tabDecided / .tabAll
toolbarRefreshreused common.refresh
status badgesPending, Approved, Executed, Failed, RejectedaiApprovals.status*
outcome bannerApprove … for …: …, Reject …, OK, Failed, Executed, Rejected, Action failed during executionaiApprovals.outcomeApprove / .outcomeReject / .outcomeExecuteFailed, reused common.ok
empty stateNo actions waiting + its sentenceaiApprovals.emptyTitle / .emptyDescription
table headingsTool, Action, Object, Status, Proposed, DecisionaiApprovals.col*
row buttonsView, Approve, Reject, Working…aiApprovals.view / .approve / .reject / .working
drawer headerPending action, Loading…, Tool … on …aiApprovals.drawerFallbackTitle / .drawerSubtitle, reused common.loading
drawer fieldsStatus, Proposed, Proposed by, Decided by, Conversation, Tool input, Result, Error, Rejection reasonaiApprovals.colStatus / .colProposed / .field*
drawer buttonsApprove & Execute, RejectaiApprovals.approveAndExecute / .reject
reject dialogtitle, body, placeholder, Cancel, RejectaiApprovals.rejectTitle / .rejectBody / .rejectPlaceholder, reused common.cancel
relative timesjust now, {count}m ago, {count}h ago, {count}d agoborrowed detail.*

Deliberately not keyed: the placeholder for a null field, and the toLocaleDateString() tail past 30 days. Neither is English copy — the first is punctuation, the second is already locale-formatted, and the sibling helpers in plugin-detail leave their own tails unkeyed for the same reason.

38 new keys, translated in all ten packs (aiApprovals). It is deliberately separate from approvalsInbox, which is the human approval-process inbox: different surface, different feature, so no rows are shared with it. Four generic verbs are reused rather than forked.

The five helpers are NOT unified

Per the ruling. Math.round here against Math.floor in plugin-detail; thresholds 45s / 60min / 24h / 30d against 60s / 7d; different tails. The arithmetic is byte-for-byte unchanged and three test rows exist only to pin it, so a later "unification" cannot change it silently:

  • 50 s renders 1m ago — a 60 s threshold would still say "just now".
  • 90 s renders 2m agoMath.floor gives 1m ago.
  • 20 d renders 20d ago — a 7 d threshold would already have fallen through to a date.

Two assembled sentences became single interpolated keys

The outcome banner was {verb} for <code>{id}</code>: {message} and the drawer subtitle was Tool <code>{tool}</code> on <code>{object}</code> — English word order assembled from JSX fragments, which several locales cannot reorder. They are now aiApprovals.outcomeApprove (Approve for {{id}}: {{message}}) and aiApprovals.drawerSubtitle (Tool {{tool}} on {{object}}), following the repo's own precedent (approvalsInbox.inlineApproved).

The visible cost, stated rather than hidden: the three identifiers lose their <code> monospace styling. The id keeps its 8-character truncation. Restoring monospace would require splitting the sentence back into fragments, which is the defect.

Two smaller decisions, both forced by measurement

createSafeTranslation, not useSafeTranslate. The card requires the provider-less path to yield English from the defaults map and to interpolate {{count}}. useSafeTranslate(key, fallback) has no options argument, so {{count}} would reach the DOM raw; it is also the per-call-site inline-English shape #3517 rules out. createSafeTranslation is what plugin-detail, plugin-grid, plugin-kanban and components all use. No inline defaultValue anywhere.

Status labels are a switch over literal keys, not a map of labelKey values. Measured on this change: with the keys held as map values, check:i18n-dead-keys listed aiApprovals.statusPending and aiApprovals.statusApproved as needs-review — precisely the two whose only appearance was that map, while the three that also had a literal call site elsewhere were seen. That is #7149's lesson ("a key visible only as a map value has no call site the i18n scanners can resolve"). After the change the namespace is absent from the report entirely. The Badge variant stays a map, because a variant is not copy.

Evidence

An en-only test cannot discriminate here — every key's en value is byte-identical to the literal it replaced. So the load-bearing assertions are zh and ar.

Pre-fix red, measured on the unconverted file:Tests 8 failed | 4 passed (12). The four survivors were exactly the en / provider-less cases, which is the card's point restated as data. Sample failure: Unable to find an element with the text: 刚刚.

Ablation, after commit — hardcoded English put back into all four branches, then restored:

HEAD blob: cc690304a545194774da9a945c3d41cb42b90c79
injected 'ABLATION-7173': 4
deleted "t('detail.justNow')": 0
mutated blob: 77514218602baef1ba50b06d9bb74a0ed4ee63c4
× 8 of 14 cases red (all the zh/ar ones)
Tests 8 failed | 6 passed (14)
RESTORE OK: on-disk blob cc690304a545194774da9a945c3d41cb42b90c79 == HEAD blob cc690304a545194774da9a945c3d41cb42b90c79

No rebuild leg is needed for it: both suites import the component by relative source path (../AiPendingActionsInbox) and the root vitest config aliases @object-ui/i18n to packages/i18n/src, so nothing on the tested path resolves through dist/. The mutation was proven on disk by grep counts on both the injected and the deleted text plus a blob-hash change; the restore by blob-hash equality with the HEAD blob, under an EXIT INT TERM trap using absolute paths.

Why the provider-less leg is in its own file.createI18n installs itself as react-i18next's module-level global, so a provider-less render in a file that has already mounted an I18nProvider reads that pack instead of the defaults map. Measured while writing this: two mounts in one test produced two zh trees, and the "provider-less" assertion was quietly measuring the previous test's pack. Vitest isolates module state per file, so AiPendingActionsInbox.noProviderFallback.test.tsx never mounts a provider.

Runs, all at 5b4a3074d:

pnpm exec vitest run packages/plugin-chatbot/ packages/i18n/
→ Test Files 89 passed (89) · Tests 1336 passed (1336)
pnpm --filter @object-ui/plugin-chatbot run type-check → EXIT=0
pnpm --filter @object-ui/i18n run type-check → EXIT=0
(tsc -p tsconfig.test.json --listFiles confirms both new suites are IN the program —
3 hits for AiPendingActionsInbox, so this is not a "typecheck excludes tests" green)
pnpm check:i18n-keys → EXIT=0 "Every in-scope call-site key resolves against the en pack (2906 keys) …"
pnpm check:i18n-drift → EXIT=0 "0 en value(s) changed (38 key(s) added, 0 removed)"
pnpm check:i18n-dead-keys → EXIT=0 report-only; aiApprovals absent from the findings
pnpm check:control-bytes → EXIT=0 "scanned 6019 tracked text file(s)"
pnpm check:phantom-deps / check:self-import / check:side-effects-array / check:esm-specifiers → EXIT=0
node scripts/check-changeset-presence.mjs → EXIT=0 "15 source file(s) of 2 released package(s) … declares 1 changeset(s)"
node scripts/check-changeset-no-major.mjs → EXIT=0
node scripts/check-governed-queue-guard.mjs --test → "NOT GOVERNED — 16 path(s) checked"

Every exit code was captured by redirecting first (cmd > out 2>&1; EXIT=$?), never read across a pipe.

Repo-wide pnpm lint was narrowed, and the narrowing is declared. Ran eslint --no-inline-config --format json over the 16 changed files: 16 files linted, 0 errors, 0 warnings, the count read from the JSON output rather than assumed. The narrowing excludes nothing relevant: this repo's eslint.config.js enables no type-aware linting, so a diff cannot move the verdict on a file it does not touch. CI runs the full farm regardless.

One ratchet moved

packages/i18n/src/__tests__/de-quote-pairing-3876.test.ts: 58 → 59 matched German „…“ pairs. The de aiApprovals.rejectPlaceholder quotes the example rejection reason it suggests. Both numeric pins were updated together with a comment naming the key, and rdq stays at 0 — the load-bearing half, since a stray closer would make close === open true for the wrong reason.


Draft on purpose: the dispatching PM lands this.

Generated by Claude Code


Generated by Claude Code

)
`AiPendingActionsInbox` held its own relative-time helper returning hardcoded
English, and had no translation wiring of its own — the unwired-component shape.
Swept whole rather than converting only its timestamps: #7142 wired one string
into an otherwise untranslated component and shipped something visibly
half-done, and the triage ruling on this card carried that forward.
The four relative-time branches borrow `detail.justNow` / `minutesAgo` /
`hoursAgo` / `daysAgo`, already translated in all ten packs; cross-package
`detail.*` borrowing is settled convention here (ObjectGrid, ObjectKanban,
ObjectTree, ListView, ObjectView, NavigationOverlay, RecordDetailView). The rest
of the sweep needed copy no pack had, so `@object-ui/i18n` gains an
`aiApprovals` namespace — 38 keys, translated in all ten packs, deliberately
separate from `approvalsInbox` (the human approval-process inbox).
The five relative-time helpers are NOT unified: they differ in real behaviour
(`Math.round` vs `Math.floor`, 45s/30d vs 60s/7d, different tails). This copy's
arithmetic is untouched and three test rows exist only to pin it.
Assertions are in zh and ar — each key's `en` value is byte-identical to the
literal it replaced, so an `en`-only test is green before the fix too. The
provider-less leg lives in its own file because `createI18n` installs itself as
react-i18next's module-level global. No inline `defaultValue` (#3517).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NRRumy89BYdW9ogbcdHTho
@github-actions

Copy link
Copy Markdown
Contributor

❌ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3165.4 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-BeDRt1bU.js
StatusFAIL

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.

Which half objected:

Eager-closure halfVerdict
Aggregate closure ceiling✅ pass
Per-chunk ceilings❌ over its ceiling
Ceiling sensitivity (headroom)✅ pass
Ceiling freshness (checkout vs. base branch)✅ pass

📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)514.48KB117.36KB
core (index.js)5.55KB2.23KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.25KB61.73KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)70.02KB19.44KB
plugin-chatbot (index.js)193.26KB45.80KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)250.63KB63.90KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.78KB32.58KB
plugin-gantt (index.js)166.77KB40.76KB
plugin-grid (index.js)208.88KB56.59KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.51KB27.67KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

…mework chunk
Two consequences of translating AiPendingActionsInbox, both this PR's own.
`ConversationsSidebar.test.tsx` froze its `vi.mock('@object-ui/i18n', ...)`
factory to a hand-written object. Its import graph reaches `plugin-chatbot`,
which resolves `createSafeTranslation` at module scope, so the frozen surface
made that read `undefined` and the file died during COLLECTION — objectui#6849's
shape, which does not look like a test failure. It now spreads
`importOriginal()` and overrides only `useObjectTranslation`. Measured: of the
41 frozen `@object-ui/i18n` factories in the repo, running every one of them
showed this to be the only file whose graph reaches the package.
The ten pack blocks are locale DATA and land in the console's eager `framework`
chunk. Its ceiling moves 512,000 -> 524,000 gzipped bytes and its baseline is
re-pinned onto a fresh measurement, 502,405 -> 514,863. Attributed by three
console builds: the merge parent reads 510,192, this branch with the ten
`aiApprovals` blocks cut reads 510,192 again, and this branch reads 514,863, so
the whole 4,671-byte delta is the pack data and nothing else. Headroom is kept
at the line's own convention (9,137 bytes, 0.10x the regression the gate must
catch), not widened; the merge parent was already at 510,192 of the 512,000
allowed, so most of the overage was pre-existing drift.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NRRumy89BYdW9ogbcdHTho
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3165.5 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-IXjpYdG_.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)514.48KB117.36KB
core (index.js)5.55KB2.23KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.25KB61.73KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)70.02KB19.44KB
plugin-chatbot (index.js)193.26KB45.80KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)250.63KB63.90KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.78KB32.58KB
plugin-gantt (index.js)166.77KB40.76KB
plugin-grid (index.js)208.88KB56.59KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.51KB27.67KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-litant
os-litant marked this pull request as ready for review September 2, 2026 08:37
@os-litant
os-litant added this pull request to the merge queueSep 2, 2026
Merged via the queue into main with commit 01c9023Sep 2, 2026
32 checks passed
@os-litant
os-litant deleted the claude/issue-7173-ai-inbox-relative-time-i18n branch September 2, 2026 08:51
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

AiPendingActionsInbox.formatRelative is a fifth untranslated copy of the same relative-time helper — the keys it needs are already in all ten packs

2 participants

@os-litant@claude