Uh oh!
There was an error while loading. Please reload this page.
fix(plugin-chatbot): translate the whole AI pending-actions inbox - #7330
Merged
Conversation
) `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
Contributor
❌ Console Performance Budget
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:
📦 Bundle Size Report
Size Limits
|
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NRRumy89BYdW9ogbcdHTho
…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_01NRRumy89BYdW9ogbcdHThoContributor
✅ Console Performance Budget
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
Size Limits
|
os-litant
marked this pull request as ready for review
September 2, 2026 08:37
Uh oh!
There was an error while loading. Please reload this page.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes#7173
AiPendingActionsInboxheld 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 outsideplugin-detailtoday:app-shellRecordDetailView.tsx(14 keys),RecordAttachmentsPanel.tsx(15),RecordApprovalsPanel.tsx,writeWarningToast.tsplugin-listListView.tsx(9)plugin-gridObjectGrid.tsx—detail.recordDetailsits in its own defaults map, the exact shape used hereplugin-viewObjectView.tsx(7)plugin-kanbanObjectKanban.tsx(6)plugin-treeObjectTree.tsx(5)componentsnavigation-overlay.tsx(7),containers.tsx,RecordTitleChip.tsx,empty.tsx,action-menu.tsxcollaboration,fields,apps/consoleSo the four relative-time branches borrow
detail.justNow/minutesAgo/hoursAgo/daysAgoand 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 controlsdetail.back/detail.noActivity10/10, negative controldetail.zzzAbsentControl71630/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.
AI Approvals, the description sentenceaiApprovals.title/.descriptionPending,Decided,AllaiApprovals.tabPending/.tabDecided/.tabAllRefreshcommon.refreshPending,Approved,Executed,Failed,RejectedaiApprovals.status*Approve … for …: …,Reject …,OK,Failed,Executed,Rejected,Action failed during executionaiApprovals.outcomeApprove/.outcomeReject/.outcomeExecuteFailed, reusedcommon.okNo actions waiting+ its sentenceaiApprovals.emptyTitle/.emptyDescriptionTool,Action,Object,Status,Proposed,DecisionaiApprovals.col*View,Approve,Reject,Working…aiApprovals.view/.approve/.reject/.workingPending action,Loading…,Tool … on …aiApprovals.drawerFallbackTitle/.drawerSubtitle, reusedcommon.loadingStatus,Proposed,Proposed by,Decided by,Conversation,Tool input,Result,Error,Rejection reasonaiApprovals.colStatus/.colProposed/.field*Approve & Execute,RejectaiApprovals.approveAndExecute/.rejectCancel,RejectaiApprovals.rejectTitle/.rejectBody/.rejectPlaceholder, reusedcommon.canceljust now,{count}m ago,{count}h ago,{count}d agodetail.*Deliberately not keyed: the
—placeholder for a null field, and thetoLocaleDateString()tail past 30 days. Neither is English copy — the first is punctuation, the second is already locale-formatted, and the sibling helpers inplugin-detailleave their own tails unkeyed for the same reason.38 new keys, translated in all ten packs (
aiApprovals). It is deliberately separate fromapprovalsInbox, 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.roundhere againstMath.floorinplugin-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:1m ago— a 60 s threshold would still say "just now".2m ago—Math.floorgives1m ago.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 wasTool <code>{tool}</code> on <code>{object}</code>— English word order assembled from JSX fragments, which several locales cannot reorder. They are nowaiApprovals.outcomeApprove(Approve for {{id}}: {{message}}) andaiApprovals.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, notuseSafeTranslate. 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.createSafeTranslationis whatplugin-detail,plugin-grid,plugin-kanbanandcomponentsall use. No inlinedefaultValueanywhere.Status labels are a
switchover literal keys, not a map oflabelKeyvalues. Measured on this change: with the keys held as map values,check:i18n-dead-keyslistedaiApprovals.statusPendingandaiApprovals.statusApprovedas 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'senvalue 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 theen/ 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:
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/i18ntopackages/i18n/src, so nothing on the tested path resolves throughdist/. 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 theHEADblob, under anEXIT INT TERMtrap using absolute paths.Why the provider-less leg is in its own file.
createI18ninstalls itself as react-i18next's module-level global, so a provider-less render in a file that has already mounted anI18nProviderreads 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, soAiPendingActionsInbox.noProviderFallback.test.tsxnever mounts a provider.Runs, all at
5b4a3074d:Every exit code was captured by redirecting first (
cmd > out 2>&1; EXIT=$?), never read across a pipe.Repo-wide
pnpm lintwas narrowed, and the narrowing is declared. Raneslint --no-inline-config --format jsonover 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'seslint.config.jsenables 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 deaiApprovals.rejectPlaceholderquotes the example rejection reason it suggests. Both numeric pins were updated together with a comment naming the key, andrdqstays at 0 — the load-bearing half, since a stray closer would makeclose === opentrue for the wrong reason.Draft on purpose: the dispatching PM lands this.
Generated by Claude Code
Generated by Claude Code