fix(plugin-detail,i18n): ActivityTimeline's remaining 18 literals resolve from the packs - #7162

Merged
os-warren merged 2 commits into
mainfrom
claude/issue-7149-activity-timeline-remaining-literals
Sep 1, 2026
Merged

fix(plugin-detail,i18n): ActivityTimeline's remaining 18 literals resolve from the packs#7162
os-warren merged 2 commits into
mainfrom
claude/issue-7149-activity-timeline-remaining-literals

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#7149

ActivityTimeline had 18 hardcoded English literals left after #7142 fixed one
of them. All 18 now resolve from the ten packs.

What this finishes, honestly

#7142 gave this component its firstt() call — the empty-state title — and
filed the sweep that found the rest. The state it left behind is what this PR
repairs: a zh activity tab read "Activity(0)暂无活动记录", the empty state
correct and the card title beside it still English. One translated string in a
component that was otherwise entirely untranslated, next to a sibling
(RecordActivityTimeline) that renders the same surface fully in Chinese.

Re-derived on origin/main head 85b5077d7 — the card's line numbers still
land exactly, and the re-sweep confirms the count.

The re-sweep: 18 literals, 20 occurrences

The card's 18 is right; (empty) is one literal at three sites (L66, L67, L74),
which is where the other two occurrences go. No nineteenth: the only other
locale-sensitive thing in the file is date.toLocaleDateString() on the
past-a-week branch, which already localizes and is byte-identical to the
sibling's own tail — not a literal, deliberately untouched.

Three groups, split by reachability rather than by defect

groupliteralsreachable fromkeys
Relative timestamps + card title5every activity tab5 existing, exact
formatFieldChange sentences6every activity tab (entries without description)6 new
Filter chips + group aria-label7published export only2 existing exact, 4 new, 1 copy change

All three shipped. The measurement that decided it: the card grouped the
formatFieldChange sentences apart because they need interpolation holes, but
that is a key-shape difference, not a reachability one. description is
optional on ActivityEntry, and formatFieldChange runs for every rendered
entry — so those six sentences render on exactly the same surface as the
timestamps. Shipping the timestamps without them would have reproduced #7142's
half-done shape one level down: "Ada Changed Status from "open" to "closed" 5分钟前".

The chips are genuinely less reachable — DetailView mounts this component at
two sites (DetailView.tsx:1586, :1713) and neither passes filterable,
so no host in this repo renders them. They are included rather than deferred
because filterable is already published API on an already-shipped export, and
3 of those 7 need no new key at all; the marginal cost was 4 keys on a PR
already touching all ten packs. Value accrues to outside consumers only, and
that is stated rather than implied.

Pack verification — read out of the pack objects, never grepped

The packs are nested, so a dotted-key grep returns a false zero for keys
that exist. Every mapping was re-verified by importing the ten pack objects and
resolving dotted paths, with both controls live:

  • positivedetail.back, detail.noActivity, detail.edit: 10/10 each
  • negativedetail.zzzAbsentControl7149, detail.noSuchKeyAtAll: 0/10

All 7 inherited "existing key" mappings are exact (en value byte-identical
to the literal), each present in 10/10 packs:

detail.justNow'just now' · detail.minutesAgo'{{count}}m ago' ·
detail.hoursAgo · detail.daysAgo · detail.activity'Activity' ·
detail.fieldChangesFilter'Field Changes' · detail.comments'Comments'

A second probe over all 2845 en leaf keys confirmed the new copy has no
existing home — Creates, Deletes, Status Changes, (empty),
Created this record, Deleted this record, Updated record,
Activity type filter all return 0 exact-value hits, with All, Create
and Delete returning non-zero on the same instrument as the live control.

One deliberate English copy change

The chip group's aria-label was "Activity type filter". It now resolves
detail.filterActivity ('Filter activity') — not an exact match, and the
card flagged it as such. Adopted anyway, and reported rather than quietly
swapped: RecordActivityTimeline.tsx:345 already uses that exact key for the
accessible name of its activity filter. Minting a near-synonym would fork
one control's name across two components — the same argument
useDetailTranslation.ts already records for common.resizeDrawer. The English
accessible name changes; no host in this repo renders the control today.

Ten new keys, all ten packs

detail.allFilter · createsFilter · deletesFilter · statusChangesFilter ·
activityEmptyValue · activityFieldChanged · activityCreated ·
activityDeleted · activityStatusChanged · activityUpdated

No inline defaultValue anywhere (#3517) — every key resolves from the packs or
from DETAIL_DEFAULT_TRANSLATIONS, which mirrors the ten new en rows
byte-for-byte (verified against the pack object; defaults-maps-mirror-en-pack
green).

Quotes live inside each pack's value so every locale punctuates its own way,
following each pack's measured convention rather than a guess: de „…“, zh
“…”, ja 「…」, fr/ru «…», en/ko/es/pt/ar ASCII. This moves the de quote
census, and de-quote-pairing-3876.test.ts is updated with it: okSpans and
{open, close, rdq} go 55 → 58 (three interpolated spans; rdq stays 0, so
each addition is a matched pair), with the comment naming why, in the file's
established style.

Evidence

Ablation — prediction stated before running: reverting only
ActivityTimeline.tsx to origin/main (packs left in place) turns the new
locale assertions red. Mutation proved on disk by blob hash
(50c29a43…6bb6ce32…, equal to origin/main: that path) and marker
count ('just now' literal 0 → 1). Restore proved by state — git diff HEAD,
git diff --cached and git status --short all empty, blob back to
50c29a43…. No rebuild leg needed and it is stated why: the test imports
./ActivityTimeline relatively, so vitest loads the source, not dist.

Result 10 failed | 10 passed (20). The ten reds are every locale-dependent
assertion. The ten greens are the discriminating half and are green by
construction
on the unfixed component — the six that came with #7142, the two
no-provider English cases (each en pack value is byte-identical to the literal
it replaces, so English cannot tell a lookup from a literal), the
author-description short-circuit, and the no-raw-key sweep.

Non-en render — the actual defect is "stays English in a zh session", so
zh/ja/ar/de carry the load:

5分钟前 · 3小时前 · 2天前 · 刚刚 (zh, {{count}} interpolated)
5分前 (ja) · منذ 5 دقيقة (ar)
将 Status code 从“open”改为“closed” (zh, three holes filled)
将 Owner 从“(空)”改为“Ada” (localized (empty), inside the sentence)
Stage von „A“ zu „B“ geändert (de, German quotes not the ASCII pair)
全部 · 字段变更 · 创建 · 删除 · 评论 · 状态变更 (six chips)
role=group name="筛选活动" / "تصفية النشاط"

Plus the exact repair, asserted whole: the zh card's textContent is now
'活动(0)暂无活动记录' — the string the card recorded as half-done, minus the
English half.

Gates — all run at ea5e7c052, exit codes captured before any pipe:

  • vitest run packages/plugin-detail packages/i18n defaults-maps-mirror-en-pack
    Test Files 181 passed (181) · Tests 2066 passed (2066)
  • type-check (both packages, script name echoed, not a zero-match) → Done
  • eslint both packages → 0 errors; ActivityTimeline.tsx contributes no
    findings, so the unused Filter import warning the card noted is gone
  • check:i18n-keys → "Every in-scope call-site key resolves against the en pack
    (2855 keys) … every call site passes exactly the arguments that value has
    holes for"
  • check:i18n-drift → "0 en value(s) changed (10 key(s) added, 0 removed)" —
    additive, no translation follow-ups owed
  • all-locales-key-parity, untranslated-identity-4376,
    fallback-placeholder-spelling-3512, de-quote-pairing-3876 — all inside the
    959-test packages/i18n run
  • changeset:check → "✅ All workspace packages are in the changeset fixed
    group. ✅ No changeset declares a major bump."

Also in this PR

The second commit resolves the chip labels with statict() calls behind a
filterLabel(type, t) switch instead of t(KEYS[type]). A key that only
appears as a map value has no call site the scanners can resolve: all four new
chip keys rendered correctly while reading as unreferenced. Measured
detail needs-review 21 → 17 and dynamic call sites with no static head
36 → 35. Same shape as the sibling's getFilterOptions.

RecordActivityTimeline.tsx was read-only reference throughout and is
unchanged.


Generated by Claude Code

…olve from the packs
objectui#7142 gave this component its first `t()` call (the empty-state title)
and filed the sweep that found the other 18. A zh activity tab read
"Activity(0)暂无活动记录" — one translated string in an otherwise English
component, beside a sibling (`RecordActivityTimeline`) that renders the same
surface fully translated.
Three groups, split by reachability rather than by defect:
- Relative timestamps + the card title render on EVERY activity tab. All five
were a pure lookup swap — the `en` pack value was already byte-identical to
the literal, and the sibling already used the same keys.
- The `formatFieldChange` sentences are assembled in code, so they needed new
keys WITH interpolation holes. Same reachability as the timestamps: they
render for any entry whose optional `description` is absent.
- The six filter chips + the chip group's accessible name need `filterable`,
which no host in this repo passes — reachable through the published export.
Ten new `detail.*` keys in all ten packs, mirrored byte-for-byte into
DETAIL_DEFAULT_TRANSLATIONS; no inline `defaultValue` anywhere (objectui#3517).
The quoted spans are inside each pack's value so every locale punctuates its
own way, which moves the de quote census 55 -> 58.
The chip group's aria-label is a deliberate English copy change: "Activity type
filter" now resolves `detail.filterActivity` ("Filter activity"), the key
`RecordActivityTimeline` already uses for the same control.
Also drops the unused `Filter` import (pre-existing eslint warning).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
A `type -> key` map read as `t(KEYS[type])` gives a key no call site the i18n
scanners can resolve: `detail.allFilter`, `createsFilter`, `deletesFilter` and
`statusChangesFilter` each rendered correctly but read as unreferenced —
`check-i18n-dead-keys` listed all four as needs-review in the `detail`
namespace, and `check:i18n-keys` counted the site among its dynamic-head
bucket.
Static `t()` calls behind a `filterLabel(type, t)` switch instead — the shape
the sibling `RecordActivityTimeline`'s `getFilterOptions` already uses. Chip
order moves to an explicit `FILTER_ORDER` array, which is what
`Object.keys(FILTER_LABELS)` was supplying before.
Measured: `detail` needs-review 21 -> 17, dynamic call sites with no static
head 36 -> 35, both gates still exit 0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3154.9 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-BqBOec6X.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)512.32KB116.52KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)177.67KB49.45KB
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)65.94KB18.64KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)249.91KB63.82KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)205.53KB55.50KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.21KB27.60KB
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-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

✅ Reviewed — will arm on green

CI still settling; auto-merge SQUASH goes on once all 30 read green. Posting now because the scope correction here is mine to record.

⚠️ My tiering was wrong, and the correction is the right one

I split the 18 literals into three tiers and ruled Tier A ships while B and C are include-or-defer. Tier C was mis-grouped.

I separated the six formatFieldChange sentences because they need interpolation holes — a key-shape difference. You measured that description is optional on ActivityEntry and formatFieldChange runs for every rendered entry, so those sentences render on exactly the same surface as Tier A. They are not less reachable; they are just a different key shape.

Deferring them on my grouping would have shipped a component whose timestamps and title are translated and whose activity descriptions are still English — objectui#7142's half-done shape reproduced one level down, which is the specific thing I flagged as the visible cost of my earlier scoping. Shipping all three tiers was correct, and the reason you give is a measurement rather than a preference.

The judgement call you surfaced instead of burying

detail.filterActivity ('Filter activity') is not identical to the aria-label 'Activity type filter' — the card flagged it and you confirmed it. You adopted the key anyway and reported it as a copy change rather than quietly swapping it, with the reason: RecordActivityTimeline.tsx:345 already uses that exact key for the accessible name of its own activity filter, so minting a near-synonym would fork one control's name across two components. And no host in this repo renders the control today.

That is the right disposal — a small deliberate copy change, named, with the alternative and its cost stated. A silent adoption would have been indistinguishable in the diff and much worse.

Four instrument checks that each could have gone wrong

A3.3 done on the pack objects, never a dotted-key grep — positive control (detail.back / noActivity / edit = 10/10) and negative control (two invented keys = 0/10). All 7 backing keys byte-identical and present 10/10.

A3.4's near-miss is worth more than the result. Repo-wide greps for 'just now' and 'm ago' return 18 and 21 hits — but every test assertion among them belongs to packages/collaboration against collaboration.* keys. A different package and namespace. Reading those hits as "something pins these literals" would have been a false positive, and the sweep only survived because you attributed each hit rather than counting them.

A3.5 verified at the mechanism, not assumed.{{name}} with no inner spaces is the one spelling both paths resolve (i18next on the provider path, interpolateFallback on the provider-less one). Both legs asserted — including a separate assertion that '{{count}}m ago' does not appear on the no-provider path, which is exactly where a wrong spelling renders raw and silently.

And you caught your own change degrading a repo gate. Resolving chip labels via t(KEYS[type]) leaves keys appearing only as map values, which the scanners cannot resolve — all four new chip keys read as unreferenced. The second commit converts them to static t() calls, measured: detail needs-review21 → 17, dynamic call sites 36 → 35. Noticing that a correct-looking change would make a gate less able to see is the hard version of this work.

Evidence

Ablation 10 failed | 10 passed (20), direction predicted before running — and the part that makes it discriminating is stated: the ten greens are green by construction on pre-fix code (objectui#7142's own six, the two no-provider English cases where the pack value is byte-identical to the literal, the author-description short-circuit, and the no-raw-key sweep). A red half without that explanation would prove much less.

Non-en render evidence across zh / ja / ar / de, including German quotes rather than the ASCII pair, and the de quote-pairing census moved 55 → 58 in de-quote-pairing-3876.test.ts — updated in that file's established style rather than left to break.

One NOT MEASURED discarded rather than reported: the first mirror-verification died ERR_MODULE_NOT_FOUND on an unbuilt closure. Built and re-ran; all ten keys mirror exactly, with an absent-key control reading undefined on both sides.

Clause ② confirmed disengaged — FILTER_LABELS was module-private and is not exported; check:i18n-drift confirms 0 en values changed, purely additive.

The loop closed

The zh card now reads 活动(0)暂无活动记录 — precisely the string objectui#7149 recorded as half-done, minus its English half. Asserting the exact repair as a whole, rather than only the parts you touched, is what makes it verifiable that the defect is gone rather than merely reduced.

objectui#7163 filed from the same sweep, pm:blocked on this PR (the ten pack files are the gate, not the components).


Generated by Claude Code

@os-warren
os-warren marked this pull request as ready for review September 1, 2026 08:23
@os-warren
os-warren added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit d8ec8d6Sep 1, 2026
32 checks passed
@os-warren
os-warren deleted the claude/issue-7149-activity-timeline-remaining-literals branch September 1, 2026 08:37
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.

ActivityTimeline has 18 more hardcoded English literals — the empty state was one of a set, and 12 of them already have pack keys sitting unused

2 participants

@os-warren@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-detail,i18n): ActivityTimeline's remaining 18 literals resolve from the packs - #7162

Merged
os-warren merged 2 commits into
mainfrom
claude/issue-7149-activity-timeline-remaining-literals
Sep 1, 2026
Merged

fix(plugin-detail,i18n): ActivityTimeline's remaining 18 literals resolve from the packs#7162
os-warren merged 2 commits into
mainfrom
claude/issue-7149-activity-timeline-remaining-literals

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#7149

ActivityTimeline had 18 hardcoded English literals left after #7142 fixed one
of them. All 18 now resolve from the ten packs.

What this finishes, honestly

#7142 gave this component its firstt() call — the empty-state title — and
filed the sweep that found the rest. The state it left behind is what this PR
repairs: a zh activity tab read "Activity(0)暂无活动记录", the empty state
correct and the card title beside it still English. One translated string in a
component that was otherwise entirely untranslated, next to a sibling
(RecordActivityTimeline) that renders the same surface fully in Chinese.

Re-derived on origin/main head 85b5077d7 — the card's line numbers still
land exactly, and the re-sweep confirms the count.

The re-sweep: 18 literals, 20 occurrences

The card's 18 is right; (empty) is one literal at three sites (L66, L67, L74),
which is where the other two occurrences go. No nineteenth: the only other
locale-sensitive thing in the file is date.toLocaleDateString() on the
past-a-week branch, which already localizes and is byte-identical to the
sibling's own tail — not a literal, deliberately untouched.

Three groups, split by reachability rather than by defect

groupliteralsreachable fromkeys
Relative timestamps + card title5every activity tab5 existing, exact
formatFieldChange sentences6every activity tab (entries without description)6 new
Filter chips + group aria-label7published export only2 existing exact, 4 new, 1 copy change

All three shipped. The measurement that decided it: the card grouped the
formatFieldChange sentences apart because they need interpolation holes, but
that is a key-shape difference, not a reachability one. description is
optional on ActivityEntry, and formatFieldChange runs for every rendered
entry — so those six sentences render on exactly the same surface as the
timestamps. Shipping the timestamps without them would have reproduced #7142's
half-done shape one level down: "Ada Changed Status from "open" to "closed" 5分钟前".

The chips are genuinely less reachable — DetailView mounts this component at
two sites (DetailView.tsx:1586, :1713) and neither passes filterable,
so no host in this repo renders them. They are included rather than deferred
because filterable is already published API on an already-shipped export, and
3 of those 7 need no new key at all; the marginal cost was 4 keys on a PR
already touching all ten packs. Value accrues to outside consumers only, and
that is stated rather than implied.

Pack verification — read out of the pack objects, never grepped

The packs are nested, so a dotted-key grep returns a false zero for keys
that exist. Every mapping was re-verified by importing the ten pack objects and
resolving dotted paths, with both controls live:

  • positivedetail.back, detail.noActivity, detail.edit: 10/10 each
  • negativedetail.zzzAbsentControl7149, detail.noSuchKeyAtAll: 0/10

All 7 inherited "existing key" mappings are exact (en value byte-identical
to the literal), each present in 10/10 packs:

detail.justNow'just now' · detail.minutesAgo'{{count}}m ago' ·
detail.hoursAgo · detail.daysAgo · detail.activity'Activity' ·
detail.fieldChangesFilter'Field Changes' · detail.comments'Comments'

A second probe over all 2845 en leaf keys confirmed the new copy has no
existing home — Creates, Deletes, Status Changes, (empty),
Created this record, Deleted this record, Updated record,
Activity type filter all return 0 exact-value hits, with All, Create
and Delete returning non-zero on the same instrument as the live control.

One deliberate English copy change

The chip group's aria-label was "Activity type filter". It now resolves
detail.filterActivity ('Filter activity') — not an exact match, and the
card flagged it as such. Adopted anyway, and reported rather than quietly
swapped: RecordActivityTimeline.tsx:345 already uses that exact key for the
accessible name of its activity filter. Minting a near-synonym would fork
one control's name across two components — the same argument
useDetailTranslation.ts already records for common.resizeDrawer. The English
accessible name changes; no host in this repo renders the control today.

Ten new keys, all ten packs

detail.allFilter · createsFilter · deletesFilter · statusChangesFilter ·
activityEmptyValue · activityFieldChanged · activityCreated ·
activityDeleted · activityStatusChanged · activityUpdated

No inline defaultValue anywhere (#3517) — every key resolves from the packs or
from DETAIL_DEFAULT_TRANSLATIONS, which mirrors the ten new en rows
byte-for-byte (verified against the pack object; defaults-maps-mirror-en-pack
green).

Quotes live inside each pack's value so every locale punctuates its own way,
following each pack's measured convention rather than a guess: de „…“, zh
“…”, ja 「…」, fr/ru «…», en/ko/es/pt/ar ASCII. This moves the de quote
census, and de-quote-pairing-3876.test.ts is updated with it: okSpans and
{open, close, rdq} go 55 → 58 (three interpolated spans; rdq stays 0, so
each addition is a matched pair), with the comment naming why, in the file's
established style.

Evidence

Ablation — prediction stated before running: reverting only
ActivityTimeline.tsx to origin/main (packs left in place) turns the new
locale assertions red. Mutation proved on disk by blob hash
(50c29a43…6bb6ce32…, equal to origin/main: that path) and marker
count ('just now' literal 0 → 1). Restore proved by state — git diff HEAD,
git diff --cached and git status --short all empty, blob back to
50c29a43…. No rebuild leg needed and it is stated why: the test imports
./ActivityTimeline relatively, so vitest loads the source, not dist.

Result 10 failed | 10 passed (20). The ten reds are every locale-dependent
assertion. The ten greens are the discriminating half and are green by
construction
on the unfixed component — the six that came with #7142, the two
no-provider English cases (each en pack value is byte-identical to the literal
it replaces, so English cannot tell a lookup from a literal), the
author-description short-circuit, and the no-raw-key sweep.

Non-en render — the actual defect is "stays English in a zh session", so
zh/ja/ar/de carry the load:

5分钟前 · 3小时前 · 2天前 · 刚刚 (zh, {{count}} interpolated)
5分前 (ja) · منذ 5 دقيقة (ar)
将 Status code 从“open”改为“closed” (zh, three holes filled)
将 Owner 从“(空)”改为“Ada” (localized (empty), inside the sentence)
Stage von „A“ zu „B“ geändert (de, German quotes not the ASCII pair)
全部 · 字段变更 · 创建 · 删除 · 评论 · 状态变更 (six chips)
role=group name="筛选活动" / "تصفية النشاط"

Plus the exact repair, asserted whole: the zh card's textContent is now
'活动(0)暂无活动记录' — the string the card recorded as half-done, minus the
English half.

Gates — all run at ea5e7c052, exit codes captured before any pipe:

  • vitest run packages/plugin-detail packages/i18n defaults-maps-mirror-en-pack
    Test Files 181 passed (181) · Tests 2066 passed (2066)
  • type-check (both packages, script name echoed, not a zero-match) → Done
  • eslint both packages → 0 errors; ActivityTimeline.tsx contributes no
    findings, so the unused Filter import warning the card noted is gone
  • check:i18n-keys → "Every in-scope call-site key resolves against the en pack
    (2855 keys) … every call site passes exactly the arguments that value has
    holes for"
  • check:i18n-drift → "0 en value(s) changed (10 key(s) added, 0 removed)" —
    additive, no translation follow-ups owed
  • all-locales-key-parity, untranslated-identity-4376,
    fallback-placeholder-spelling-3512, de-quote-pairing-3876 — all inside the
    959-test packages/i18n run
  • changeset:check → "✅ All workspace packages are in the changeset fixed
    group. ✅ No changeset declares a major bump."

Also in this PR

The second commit resolves the chip labels with statict() calls behind a
filterLabel(type, t) switch instead of t(KEYS[type]). A key that only
appears as a map value has no call site the scanners can resolve: all four new
chip keys rendered correctly while reading as unreferenced. Measured
detail needs-review 21 → 17 and dynamic call sites with no static head
36 → 35. Same shape as the sibling's getFilterOptions.

RecordActivityTimeline.tsx was read-only reference throughout and is
unchanged.


Generated by Claude Code

…olve from the packs
objectui#7142 gave this component its first `t()` call (the empty-state title)
and filed the sweep that found the other 18. A zh activity tab read
"Activity(0)暂无活动记录" — one translated string in an otherwise English
component, beside a sibling (`RecordActivityTimeline`) that renders the same
surface fully translated.
Three groups, split by reachability rather than by defect:
- Relative timestamps + the card title render on EVERY activity tab. All five
were a pure lookup swap — the `en` pack value was already byte-identical to
the literal, and the sibling already used the same keys.
- The `formatFieldChange` sentences are assembled in code, so they needed new
keys WITH interpolation holes. Same reachability as the timestamps: they
render for any entry whose optional `description` is absent.
- The six filter chips + the chip group's accessible name need `filterable`,
which no host in this repo passes — reachable through the published export.
Ten new `detail.*` keys in all ten packs, mirrored byte-for-byte into
DETAIL_DEFAULT_TRANSLATIONS; no inline `defaultValue` anywhere (objectui#3517).
The quoted spans are inside each pack's value so every locale punctuates its
own way, which moves the de quote census 55 -> 58.
The chip group's aria-label is a deliberate English copy change: "Activity type
filter" now resolves `detail.filterActivity` ("Filter activity"), the key
`RecordActivityTimeline` already uses for the same control.
Also drops the unused `Filter` import (pre-existing eslint warning).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
A `type -> key` map read as `t(KEYS[type])` gives a key no call site the i18n
scanners can resolve: `detail.allFilter`, `createsFilter`, `deletesFilter` and
`statusChangesFilter` each rendered correctly but read as unreferenced —
`check-i18n-dead-keys` listed all four as needs-review in the `detail`
namespace, and `check:i18n-keys` counted the site among its dynamic-head
bucket.
Static `t()` calls behind a `filterLabel(type, t)` switch instead — the shape
the sibling `RecordActivityTimeline`'s `getFilterOptions` already uses. Chip
order moves to an explicit `FILTER_ORDER` array, which is what
`Object.keys(FILTER_LABELS)` was supplying before.
Measured: `detail` needs-review 21 -> 17, dynamic call sites with no static
head 36 -> 35, both gates still exit 0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3154.9 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-BqBOec6X.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)512.32KB116.52KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)177.67KB49.45KB
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)65.94KB18.64KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)249.91KB63.82KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)205.53KB55.50KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.21KB27.60KB
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-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

✅ Reviewed — will arm on green

CI still settling; auto-merge SQUASH goes on once all 30 read green. Posting now because the scope correction here is mine to record.

⚠️ My tiering was wrong, and the correction is the right one

I split the 18 literals into three tiers and ruled Tier A ships while B and C are include-or-defer. Tier C was mis-grouped.

I separated the six formatFieldChange sentences because they need interpolation holes — a key-shape difference. You measured that description is optional on ActivityEntry and formatFieldChange runs for every rendered entry, so those sentences render on exactly the same surface as Tier A. They are not less reachable; they are just a different key shape.

Deferring them on my grouping would have shipped a component whose timestamps and title are translated and whose activity descriptions are still English — objectui#7142's half-done shape reproduced one level down, which is the specific thing I flagged as the visible cost of my earlier scoping. Shipping all three tiers was correct, and the reason you give is a measurement rather than a preference.

The judgement call you surfaced instead of burying

detail.filterActivity ('Filter activity') is not identical to the aria-label 'Activity type filter' — the card flagged it and you confirmed it. You adopted the key anyway and reported it as a copy change rather than quietly swapping it, with the reason: RecordActivityTimeline.tsx:345 already uses that exact key for the accessible name of its own activity filter, so minting a near-synonym would fork one control's name across two components. And no host in this repo renders the control today.

That is the right disposal — a small deliberate copy change, named, with the alternative and its cost stated. A silent adoption would have been indistinguishable in the diff and much worse.

Four instrument checks that each could have gone wrong

A3.3 done on the pack objects, never a dotted-key grep — positive control (detail.back / noActivity / edit = 10/10) and negative control (two invented keys = 0/10). All 7 backing keys byte-identical and present 10/10.

A3.4's near-miss is worth more than the result. Repo-wide greps for 'just now' and 'm ago' return 18 and 21 hits — but every test assertion among them belongs to packages/collaboration against collaboration.* keys. A different package and namespace. Reading those hits as "something pins these literals" would have been a false positive, and the sweep only survived because you attributed each hit rather than counting them.

A3.5 verified at the mechanism, not assumed.{{name}} with no inner spaces is the one spelling both paths resolve (i18next on the provider path, interpolateFallback on the provider-less one). Both legs asserted — including a separate assertion that '{{count}}m ago' does not appear on the no-provider path, which is exactly where a wrong spelling renders raw and silently.

And you caught your own change degrading a repo gate. Resolving chip labels via t(KEYS[type]) leaves keys appearing only as map values, which the scanners cannot resolve — all four new chip keys read as unreferenced. The second commit converts them to static t() calls, measured: detail needs-review21 → 17, dynamic call sites 36 → 35. Noticing that a correct-looking change would make a gate less able to see is the hard version of this work.

Evidence

Ablation 10 failed | 10 passed (20), direction predicted before running — and the part that makes it discriminating is stated: the ten greens are green by construction on pre-fix code (objectui#7142's own six, the two no-provider English cases where the pack value is byte-identical to the literal, the author-description short-circuit, and the no-raw-key sweep). A red half without that explanation would prove much less.

Non-en render evidence across zh / ja / ar / de, including German quotes rather than the ASCII pair, and the de quote-pairing census moved 55 → 58 in de-quote-pairing-3876.test.ts — updated in that file's established style rather than left to break.

One NOT MEASURED discarded rather than reported: the first mirror-verification died ERR_MODULE_NOT_FOUND on an unbuilt closure. Built and re-ran; all ten keys mirror exactly, with an absent-key control reading undefined on both sides.

Clause ② confirmed disengaged — FILTER_LABELS was module-private and is not exported; check:i18n-drift confirms 0 en values changed, purely additive.

The loop closed

The zh card now reads 活动(0)暂无活动记录 — precisely the string objectui#7149 recorded as half-done, minus its English half. Asserting the exact repair as a whole, rather than only the parts you touched, is what makes it verifiable that the defect is gone rather than merely reduced.

objectui#7163 filed from the same sweep, pm:blocked on this PR (the ten pack files are the gate, not the components).


Generated by Claude Code

@os-warren
os-warren marked this pull request as ready for review September 1, 2026 08:23
@os-warren
os-warren added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit d8ec8d6Sep 1, 2026
32 checks passed
@os-warren
os-warren deleted the claude/issue-7149-activity-timeline-remaining-literals branch September 1, 2026 08:37
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.

ActivityTimeline has 18 more hardcoded English literals — the empty state was one of a set, and 12 of them already have pack keys sitting unused

2 participants

@os-warren@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-detail,i18n): ActivityTimeline's remaining 18 literals resolve from the packs - #7162

Merged
os-warren merged 2 commits into
mainfrom
claude/issue-7149-activity-timeline-remaining-literals
Sep 1, 2026
Merged

fix(plugin-detail,i18n): ActivityTimeline's remaining 18 literals resolve from the packs#7162
os-warren merged 2 commits into
mainfrom
claude/issue-7149-activity-timeline-remaining-literals

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#7149

ActivityTimeline had 18 hardcoded English literals left after #7142 fixed one
of them. All 18 now resolve from the ten packs.

What this finishes, honestly

#7142 gave this component its firstt() call — the empty-state title — and
filed the sweep that found the rest. The state it left behind is what this PR
repairs: a zh activity tab read "Activity(0)暂无活动记录", the empty state
correct and the card title beside it still English. One translated string in a
component that was otherwise entirely untranslated, next to a sibling
(RecordActivityTimeline) that renders the same surface fully in Chinese.

Re-derived on origin/main head 85b5077d7 — the card's line numbers still
land exactly, and the re-sweep confirms the count.

The re-sweep: 18 literals, 20 occurrences

The card's 18 is right; (empty) is one literal at three sites (L66, L67, L74),
which is where the other two occurrences go. No nineteenth: the only other
locale-sensitive thing in the file is date.toLocaleDateString() on the
past-a-week branch, which already localizes and is byte-identical to the
sibling's own tail — not a literal, deliberately untouched.

Three groups, split by reachability rather than by defect

groupliteralsreachable fromkeys
Relative timestamps + card title5every activity tab5 existing, exact
formatFieldChange sentences6every activity tab (entries without description)6 new
Filter chips + group aria-label7published export only2 existing exact, 4 new, 1 copy change

All three shipped. The measurement that decided it: the card grouped the
formatFieldChange sentences apart because they need interpolation holes, but
that is a key-shape difference, not a reachability one. description is
optional on ActivityEntry, and formatFieldChange runs for every rendered
entry — so those six sentences render on exactly the same surface as the
timestamps. Shipping the timestamps without them would have reproduced #7142's
half-done shape one level down: "Ada Changed Status from "open" to "closed" 5分钟前".

The chips are genuinely less reachable — DetailView mounts this component at
two sites (DetailView.tsx:1586, :1713) and neither passes filterable,
so no host in this repo renders them. They are included rather than deferred
because filterable is already published API on an already-shipped export, and
3 of those 7 need no new key at all; the marginal cost was 4 keys on a PR
already touching all ten packs. Value accrues to outside consumers only, and
that is stated rather than implied.

Pack verification — read out of the pack objects, never grepped

The packs are nested, so a dotted-key grep returns a false zero for keys
that exist. Every mapping was re-verified by importing the ten pack objects and
resolving dotted paths, with both controls live:

  • positivedetail.back, detail.noActivity, detail.edit: 10/10 each
  • negativedetail.zzzAbsentControl7149, detail.noSuchKeyAtAll: 0/10

All 7 inherited "existing key" mappings are exact (en value byte-identical
to the literal), each present in 10/10 packs:

detail.justNow'just now' · detail.minutesAgo'{{count}}m ago' ·
detail.hoursAgo · detail.daysAgo · detail.activity'Activity' ·
detail.fieldChangesFilter'Field Changes' · detail.comments'Comments'

A second probe over all 2845 en leaf keys confirmed the new copy has no
existing home — Creates, Deletes, Status Changes, (empty),
Created this record, Deleted this record, Updated record,
Activity type filter all return 0 exact-value hits, with All, Create
and Delete returning non-zero on the same instrument as the live control.

One deliberate English copy change

The chip group's aria-label was "Activity type filter". It now resolves
detail.filterActivity ('Filter activity') — not an exact match, and the
card flagged it as such. Adopted anyway, and reported rather than quietly
swapped: RecordActivityTimeline.tsx:345 already uses that exact key for the
accessible name of its activity filter. Minting a near-synonym would fork
one control's name across two components — the same argument
useDetailTranslation.ts already records for common.resizeDrawer. The English
accessible name changes; no host in this repo renders the control today.

Ten new keys, all ten packs

detail.allFilter · createsFilter · deletesFilter · statusChangesFilter ·
activityEmptyValue · activityFieldChanged · activityCreated ·
activityDeleted · activityStatusChanged · activityUpdated

No inline defaultValue anywhere (#3517) — every key resolves from the packs or
from DETAIL_DEFAULT_TRANSLATIONS, which mirrors the ten new en rows
byte-for-byte (verified against the pack object; defaults-maps-mirror-en-pack
green).

Quotes live inside each pack's value so every locale punctuates its own way,
following each pack's measured convention rather than a guess: de „…“, zh
“…”, ja 「…」, fr/ru «…», en/ko/es/pt/ar ASCII. This moves the de quote
census, and de-quote-pairing-3876.test.ts is updated with it: okSpans and
{open, close, rdq} go 55 → 58 (three interpolated spans; rdq stays 0, so
each addition is a matched pair), with the comment naming why, in the file's
established style.

Evidence

Ablation — prediction stated before running: reverting only
ActivityTimeline.tsx to origin/main (packs left in place) turns the new
locale assertions red. Mutation proved on disk by blob hash
(50c29a43…6bb6ce32…, equal to origin/main: that path) and marker
count ('just now' literal 0 → 1). Restore proved by state — git diff HEAD,
git diff --cached and git status --short all empty, blob back to
50c29a43…. No rebuild leg needed and it is stated why: the test imports
./ActivityTimeline relatively, so vitest loads the source, not dist.

Result 10 failed | 10 passed (20). The ten reds are every locale-dependent
assertion. The ten greens are the discriminating half and are green by
construction
on the unfixed component — the six that came with #7142, the two
no-provider English cases (each en pack value is byte-identical to the literal
it replaces, so English cannot tell a lookup from a literal), the
author-description short-circuit, and the no-raw-key sweep.

Non-en render — the actual defect is "stays English in a zh session", so
zh/ja/ar/de carry the load:

5分钟前 · 3小时前 · 2天前 · 刚刚 (zh, {{count}} interpolated)
5分前 (ja) · منذ 5 دقيقة (ar)
将 Status code 从“open”改为“closed” (zh, three holes filled)
将 Owner 从“(空)”改为“Ada” (localized (empty), inside the sentence)
Stage von „A“ zu „B“ geändert (de, German quotes not the ASCII pair)
全部 · 字段变更 · 创建 · 删除 · 评论 · 状态变更 (six chips)
role=group name="筛选活动" / "تصفية النشاط"

Plus the exact repair, asserted whole: the zh card's textContent is now
'活动(0)暂无活动记录' — the string the card recorded as half-done, minus the
English half.

Gates — all run at ea5e7c052, exit codes captured before any pipe:

  • vitest run packages/plugin-detail packages/i18n defaults-maps-mirror-en-pack
    Test Files 181 passed (181) · Tests 2066 passed (2066)
  • type-check (both packages, script name echoed, not a zero-match) → Done
  • eslint both packages → 0 errors; ActivityTimeline.tsx contributes no
    findings, so the unused Filter import warning the card noted is gone
  • check:i18n-keys → "Every in-scope call-site key resolves against the en pack
    (2855 keys) … every call site passes exactly the arguments that value has
    holes for"
  • check:i18n-drift → "0 en value(s) changed (10 key(s) added, 0 removed)" —
    additive, no translation follow-ups owed
  • all-locales-key-parity, untranslated-identity-4376,
    fallback-placeholder-spelling-3512, de-quote-pairing-3876 — all inside the
    959-test packages/i18n run
  • changeset:check → "✅ All workspace packages are in the changeset fixed
    group. ✅ No changeset declares a major bump."

Also in this PR

The second commit resolves the chip labels with statict() calls behind a
filterLabel(type, t) switch instead of t(KEYS[type]). A key that only
appears as a map value has no call site the scanners can resolve: all four new
chip keys rendered correctly while reading as unreferenced. Measured
detail needs-review 21 → 17 and dynamic call sites with no static head
36 → 35. Same shape as the sibling's getFilterOptions.

RecordActivityTimeline.tsx was read-only reference throughout and is
unchanged.


Generated by Claude Code

…olve from the packs
objectui#7142 gave this component its first `t()` call (the empty-state title)
and filed the sweep that found the other 18. A zh activity tab read
"Activity(0)暂无活动记录" — one translated string in an otherwise English
component, beside a sibling (`RecordActivityTimeline`) that renders the same
surface fully translated.
Three groups, split by reachability rather than by defect:
- Relative timestamps + the card title render on EVERY activity tab. All five
were a pure lookup swap — the `en` pack value was already byte-identical to
the literal, and the sibling already used the same keys.
- The `formatFieldChange` sentences are assembled in code, so they needed new
keys WITH interpolation holes. Same reachability as the timestamps: they
render for any entry whose optional `description` is absent.
- The six filter chips + the chip group's accessible name need `filterable`,
which no host in this repo passes — reachable through the published export.
Ten new `detail.*` keys in all ten packs, mirrored byte-for-byte into
DETAIL_DEFAULT_TRANSLATIONS; no inline `defaultValue` anywhere (objectui#3517).
The quoted spans are inside each pack's value so every locale punctuates its
own way, which moves the de quote census 55 -> 58.
The chip group's aria-label is a deliberate English copy change: "Activity type
filter" now resolves `detail.filterActivity` ("Filter activity"), the key
`RecordActivityTimeline` already uses for the same control.
Also drops the unused `Filter` import (pre-existing eslint warning).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
A `type -> key` map read as `t(KEYS[type])` gives a key no call site the i18n
scanners can resolve: `detail.allFilter`, `createsFilter`, `deletesFilter` and
`statusChangesFilter` each rendered correctly but read as unreferenced —
`check-i18n-dead-keys` listed all four as needs-review in the `detail`
namespace, and `check:i18n-keys` counted the site among its dynamic-head
bucket.
Static `t()` calls behind a `filterLabel(type, t)` switch instead — the shape
the sibling `RecordActivityTimeline`'s `getFilterOptions` already uses. Chip
order moves to an explicit `FILTER_ORDER` array, which is what
`Object.keys(FILTER_LABELS)` was supplying before.
Measured: `detail` needs-review 21 -> 17, dynamic call sites with no static
head 36 -> 35, both gates still exit 0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3154.9 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-BqBOec6X.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)512.32KB116.52KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)177.67KB49.45KB
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)65.94KB18.64KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)249.91KB63.82KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)205.53KB55.50KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.21KB27.60KB
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-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

✅ Reviewed — will arm on green

CI still settling; auto-merge SQUASH goes on once all 30 read green. Posting now because the scope correction here is mine to record.

⚠️ My tiering was wrong, and the correction is the right one

I split the 18 literals into three tiers and ruled Tier A ships while B and C are include-or-defer. Tier C was mis-grouped.

I separated the six formatFieldChange sentences because they need interpolation holes — a key-shape difference. You measured that description is optional on ActivityEntry and formatFieldChange runs for every rendered entry, so those sentences render on exactly the same surface as Tier A. They are not less reachable; they are just a different key shape.

Deferring them on my grouping would have shipped a component whose timestamps and title are translated and whose activity descriptions are still English — objectui#7142's half-done shape reproduced one level down, which is the specific thing I flagged as the visible cost of my earlier scoping. Shipping all three tiers was correct, and the reason you give is a measurement rather than a preference.

The judgement call you surfaced instead of burying

detail.filterActivity ('Filter activity') is not identical to the aria-label 'Activity type filter' — the card flagged it and you confirmed it. You adopted the key anyway and reported it as a copy change rather than quietly swapping it, with the reason: RecordActivityTimeline.tsx:345 already uses that exact key for the accessible name of its own activity filter, so minting a near-synonym would fork one control's name across two components. And no host in this repo renders the control today.

That is the right disposal — a small deliberate copy change, named, with the alternative and its cost stated. A silent adoption would have been indistinguishable in the diff and much worse.

Four instrument checks that each could have gone wrong

A3.3 done on the pack objects, never a dotted-key grep — positive control (detail.back / noActivity / edit = 10/10) and negative control (two invented keys = 0/10). All 7 backing keys byte-identical and present 10/10.

A3.4's near-miss is worth more than the result. Repo-wide greps for 'just now' and 'm ago' return 18 and 21 hits — but every test assertion among them belongs to packages/collaboration against collaboration.* keys. A different package and namespace. Reading those hits as "something pins these literals" would have been a false positive, and the sweep only survived because you attributed each hit rather than counting them.

A3.5 verified at the mechanism, not assumed.{{name}} with no inner spaces is the one spelling both paths resolve (i18next on the provider path, interpolateFallback on the provider-less one). Both legs asserted — including a separate assertion that '{{count}}m ago' does not appear on the no-provider path, which is exactly where a wrong spelling renders raw and silently.

And you caught your own change degrading a repo gate. Resolving chip labels via t(KEYS[type]) leaves keys appearing only as map values, which the scanners cannot resolve — all four new chip keys read as unreferenced. The second commit converts them to static t() calls, measured: detail needs-review21 → 17, dynamic call sites 36 → 35. Noticing that a correct-looking change would make a gate less able to see is the hard version of this work.

Evidence

Ablation 10 failed | 10 passed (20), direction predicted before running — and the part that makes it discriminating is stated: the ten greens are green by construction on pre-fix code (objectui#7142's own six, the two no-provider English cases where the pack value is byte-identical to the literal, the author-description short-circuit, and the no-raw-key sweep). A red half without that explanation would prove much less.

Non-en render evidence across zh / ja / ar / de, including German quotes rather than the ASCII pair, and the de quote-pairing census moved 55 → 58 in de-quote-pairing-3876.test.ts — updated in that file's established style rather than left to break.

One NOT MEASURED discarded rather than reported: the first mirror-verification died ERR_MODULE_NOT_FOUND on an unbuilt closure. Built and re-ran; all ten keys mirror exactly, with an absent-key control reading undefined on both sides.

Clause ② confirmed disengaged — FILTER_LABELS was module-private and is not exported; check:i18n-drift confirms 0 en values changed, purely additive.

The loop closed

The zh card now reads 活动(0)暂无活动记录 — precisely the string objectui#7149 recorded as half-done, minus its English half. Asserting the exact repair as a whole, rather than only the parts you touched, is what makes it verifiable that the defect is gone rather than merely reduced.

objectui#7163 filed from the same sweep, pm:blocked on this PR (the ten pack files are the gate, not the components).


Generated by Claude Code

@os-warren
os-warren marked this pull request as ready for review September 1, 2026 08:23
@os-warren
os-warren added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit d8ec8d6Sep 1, 2026
32 checks passed
@os-warren
os-warren deleted the claude/issue-7149-activity-timeline-remaining-literals branch September 1, 2026 08:37
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.

ActivityTimeline has 18 more hardcoded English literals — the empty state was one of a set, and 12 of them already have pack keys sitting unused

2 participants

@os-warren@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-detail,i18n): ActivityTimeline's remaining 18 literals resolve from the packs - #7162

Merged
os-warren merged 2 commits into
mainfrom
claude/issue-7149-activity-timeline-remaining-literals
Sep 1, 2026
Merged

fix(plugin-detail,i18n): ActivityTimeline's remaining 18 literals resolve from the packs#7162
os-warren merged 2 commits into
mainfrom
claude/issue-7149-activity-timeline-remaining-literals

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#7149

ActivityTimeline had 18 hardcoded English literals left after #7142 fixed one
of them. All 18 now resolve from the ten packs.

What this finishes, honestly

#7142 gave this component its firstt() call — the empty-state title — and
filed the sweep that found the rest. The state it left behind is what this PR
repairs: a zh activity tab read "Activity(0)暂无活动记录", the empty state
correct and the card title beside it still English. One translated string in a
component that was otherwise entirely untranslated, next to a sibling
(RecordActivityTimeline) that renders the same surface fully in Chinese.

Re-derived on origin/main head 85b5077d7 — the card's line numbers still
land exactly, and the re-sweep confirms the count.

The re-sweep: 18 literals, 20 occurrences

The card's 18 is right; (empty) is one literal at three sites (L66, L67, L74),
which is where the other two occurrences go. No nineteenth: the only other
locale-sensitive thing in the file is date.toLocaleDateString() on the
past-a-week branch, which already localizes and is byte-identical to the
sibling's own tail — not a literal, deliberately untouched.

Three groups, split by reachability rather than by defect

groupliteralsreachable fromkeys
Relative timestamps + card title5every activity tab5 existing, exact
formatFieldChange sentences6every activity tab (entries without description)6 new
Filter chips + group aria-label7published export only2 existing exact, 4 new, 1 copy change

All three shipped. The measurement that decided it: the card grouped the
formatFieldChange sentences apart because they need interpolation holes, but
that is a key-shape difference, not a reachability one. description is
optional on ActivityEntry, and formatFieldChange runs for every rendered
entry — so those six sentences render on exactly the same surface as the
timestamps. Shipping the timestamps without them would have reproduced #7142's
half-done shape one level down: "Ada Changed Status from "open" to "closed" 5分钟前".

The chips are genuinely less reachable — DetailView mounts this component at
two sites (DetailView.tsx:1586, :1713) and neither passes filterable,
so no host in this repo renders them. They are included rather than deferred
because filterable is already published API on an already-shipped export, and
3 of those 7 need no new key at all; the marginal cost was 4 keys on a PR
already touching all ten packs. Value accrues to outside consumers only, and
that is stated rather than implied.

Pack verification — read out of the pack objects, never grepped

The packs are nested, so a dotted-key grep returns a false zero for keys
that exist. Every mapping was re-verified by importing the ten pack objects and
resolving dotted paths, with both controls live:

  • positivedetail.back, detail.noActivity, detail.edit: 10/10 each
  • negativedetail.zzzAbsentControl7149, detail.noSuchKeyAtAll: 0/10

All 7 inherited "existing key" mappings are exact (en value byte-identical
to the literal), each present in 10/10 packs:

detail.justNow'just now' · detail.minutesAgo'{{count}}m ago' ·
detail.hoursAgo · detail.daysAgo · detail.activity'Activity' ·
detail.fieldChangesFilter'Field Changes' · detail.comments'Comments'

A second probe over all 2845 en leaf keys confirmed the new copy has no
existing home — Creates, Deletes, Status Changes, (empty),
Created this record, Deleted this record, Updated record,
Activity type filter all return 0 exact-value hits, with All, Create
and Delete returning non-zero on the same instrument as the live control.

One deliberate English copy change

The chip group's aria-label was "Activity type filter". It now resolves
detail.filterActivity ('Filter activity') — not an exact match, and the
card flagged it as such. Adopted anyway, and reported rather than quietly
swapped: RecordActivityTimeline.tsx:345 already uses that exact key for the
accessible name of its activity filter. Minting a near-synonym would fork
one control's name across two components — the same argument
useDetailTranslation.ts already records for common.resizeDrawer. The English
accessible name changes; no host in this repo renders the control today.

Ten new keys, all ten packs

detail.allFilter · createsFilter · deletesFilter · statusChangesFilter ·
activityEmptyValue · activityFieldChanged · activityCreated ·
activityDeleted · activityStatusChanged · activityUpdated

No inline defaultValue anywhere (#3517) — every key resolves from the packs or
from DETAIL_DEFAULT_TRANSLATIONS, which mirrors the ten new en rows
byte-for-byte (verified against the pack object; defaults-maps-mirror-en-pack
green).

Quotes live inside each pack's value so every locale punctuates its own way,
following each pack's measured convention rather than a guess: de „…“, zh
“…”, ja 「…」, fr/ru «…», en/ko/es/pt/ar ASCII. This moves the de quote
census, and de-quote-pairing-3876.test.ts is updated with it: okSpans and
{open, close, rdq} go 55 → 58 (three interpolated spans; rdq stays 0, so
each addition is a matched pair), with the comment naming why, in the file's
established style.

Evidence

Ablation — prediction stated before running: reverting only
ActivityTimeline.tsx to origin/main (packs left in place) turns the new
locale assertions red. Mutation proved on disk by blob hash
(50c29a43…6bb6ce32…, equal to origin/main: that path) and marker
count ('just now' literal 0 → 1). Restore proved by state — git diff HEAD,
git diff --cached and git status --short all empty, blob back to
50c29a43…. No rebuild leg needed and it is stated why: the test imports
./ActivityTimeline relatively, so vitest loads the source, not dist.

Result 10 failed | 10 passed (20). The ten reds are every locale-dependent
assertion. The ten greens are the discriminating half and are green by
construction
on the unfixed component — the six that came with #7142, the two
no-provider English cases (each en pack value is byte-identical to the literal
it replaces, so English cannot tell a lookup from a literal), the
author-description short-circuit, and the no-raw-key sweep.

Non-en render — the actual defect is "stays English in a zh session", so
zh/ja/ar/de carry the load:

5分钟前 · 3小时前 · 2天前 · 刚刚 (zh, {{count}} interpolated)
5分前 (ja) · منذ 5 دقيقة (ar)
将 Status code 从“open”改为“closed” (zh, three holes filled)
将 Owner 从“(空)”改为“Ada” (localized (empty), inside the sentence)
Stage von „A“ zu „B“ geändert (de, German quotes not the ASCII pair)
全部 · 字段变更 · 创建 · 删除 · 评论 · 状态变更 (six chips)
role=group name="筛选活动" / "تصفية النشاط"

Plus the exact repair, asserted whole: the zh card's textContent is now
'活动(0)暂无活动记录' — the string the card recorded as half-done, minus the
English half.

Gates — all run at ea5e7c052, exit codes captured before any pipe:

  • vitest run packages/plugin-detail packages/i18n defaults-maps-mirror-en-pack
    Test Files 181 passed (181) · Tests 2066 passed (2066)
  • type-check (both packages, script name echoed, not a zero-match) → Done
  • eslint both packages → 0 errors; ActivityTimeline.tsx contributes no
    findings, so the unused Filter import warning the card noted is gone
  • check:i18n-keys → "Every in-scope call-site key resolves against the en pack
    (2855 keys) … every call site passes exactly the arguments that value has
    holes for"
  • check:i18n-drift → "0 en value(s) changed (10 key(s) added, 0 removed)" —
    additive, no translation follow-ups owed
  • all-locales-key-parity, untranslated-identity-4376,
    fallback-placeholder-spelling-3512, de-quote-pairing-3876 — all inside the
    959-test packages/i18n run
  • changeset:check → "✅ All workspace packages are in the changeset fixed
    group. ✅ No changeset declares a major bump."

Also in this PR

The second commit resolves the chip labels with statict() calls behind a
filterLabel(type, t) switch instead of t(KEYS[type]). A key that only
appears as a map value has no call site the scanners can resolve: all four new
chip keys rendered correctly while reading as unreferenced. Measured
detail needs-review 21 → 17 and dynamic call sites with no static head
36 → 35. Same shape as the sibling's getFilterOptions.

RecordActivityTimeline.tsx was read-only reference throughout and is
unchanged.


Generated by Claude Code

…olve from the packs
objectui#7142 gave this component its first `t()` call (the empty-state title)
and filed the sweep that found the other 18. A zh activity tab read
"Activity(0)暂无活动记录" — one translated string in an otherwise English
component, beside a sibling (`RecordActivityTimeline`) that renders the same
surface fully translated.
Three groups, split by reachability rather than by defect:
- Relative timestamps + the card title render on EVERY activity tab. All five
were a pure lookup swap — the `en` pack value was already byte-identical to
the literal, and the sibling already used the same keys.
- The `formatFieldChange` sentences are assembled in code, so they needed new
keys WITH interpolation holes. Same reachability as the timestamps: they
render for any entry whose optional `description` is absent.
- The six filter chips + the chip group's accessible name need `filterable`,
which no host in this repo passes — reachable through the published export.
Ten new `detail.*` keys in all ten packs, mirrored byte-for-byte into
DETAIL_DEFAULT_TRANSLATIONS; no inline `defaultValue` anywhere (objectui#3517).
The quoted spans are inside each pack's value so every locale punctuates its
own way, which moves the de quote census 55 -> 58.
The chip group's aria-label is a deliberate English copy change: "Activity type
filter" now resolves `detail.filterActivity` ("Filter activity"), the key
`RecordActivityTimeline` already uses for the same control.
Also drops the unused `Filter` import (pre-existing eslint warning).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
A `type -> key` map read as `t(KEYS[type])` gives a key no call site the i18n
scanners can resolve: `detail.allFilter`, `createsFilter`, `deletesFilter` and
`statusChangesFilter` each rendered correctly but read as unreferenced —
`check-i18n-dead-keys` listed all four as needs-review in the `detail`
namespace, and `check:i18n-keys` counted the site among its dynamic-head
bucket.
Static `t()` calls behind a `filterLabel(type, t)` switch instead — the shape
the sibling `RecordActivityTimeline`'s `getFilterOptions` already uses. Chip
order moves to an explicit `FILTER_ORDER` array, which is what
`Object.keys(FILTER_LABELS)` was supplying before.
Measured: `detail` needs-review 21 -> 17, dynamic call sites with no static
head 36 -> 35, both gates still exit 0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3154.9 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-BqBOec6X.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)512.32KB116.52KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)177.67KB49.45KB
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)65.94KB18.64KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)249.91KB63.82KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)205.53KB55.50KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.21KB27.60KB
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-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

✅ Reviewed — will arm on green

CI still settling; auto-merge SQUASH goes on once all 30 read green. Posting now because the scope correction here is mine to record.

⚠️ My tiering was wrong, and the correction is the right one

I split the 18 literals into three tiers and ruled Tier A ships while B and C are include-or-defer. Tier C was mis-grouped.

I separated the six formatFieldChange sentences because they need interpolation holes — a key-shape difference. You measured that description is optional on ActivityEntry and formatFieldChange runs for every rendered entry, so those sentences render on exactly the same surface as Tier A. They are not less reachable; they are just a different key shape.

Deferring them on my grouping would have shipped a component whose timestamps and title are translated and whose activity descriptions are still English — objectui#7142's half-done shape reproduced one level down, which is the specific thing I flagged as the visible cost of my earlier scoping. Shipping all three tiers was correct, and the reason you give is a measurement rather than a preference.

The judgement call you surfaced instead of burying

detail.filterActivity ('Filter activity') is not identical to the aria-label 'Activity type filter' — the card flagged it and you confirmed it. You adopted the key anyway and reported it as a copy change rather than quietly swapping it, with the reason: RecordActivityTimeline.tsx:345 already uses that exact key for the accessible name of its own activity filter, so minting a near-synonym would fork one control's name across two components. And no host in this repo renders the control today.

That is the right disposal — a small deliberate copy change, named, with the alternative and its cost stated. A silent adoption would have been indistinguishable in the diff and much worse.

Four instrument checks that each could have gone wrong

A3.3 done on the pack objects, never a dotted-key grep — positive control (detail.back / noActivity / edit = 10/10) and negative control (two invented keys = 0/10). All 7 backing keys byte-identical and present 10/10.

A3.4's near-miss is worth more than the result. Repo-wide greps for 'just now' and 'm ago' return 18 and 21 hits — but every test assertion among them belongs to packages/collaboration against collaboration.* keys. A different package and namespace. Reading those hits as "something pins these literals" would have been a false positive, and the sweep only survived because you attributed each hit rather than counting them.

A3.5 verified at the mechanism, not assumed.{{name}} with no inner spaces is the one spelling both paths resolve (i18next on the provider path, interpolateFallback on the provider-less one). Both legs asserted — including a separate assertion that '{{count}}m ago' does not appear on the no-provider path, which is exactly where a wrong spelling renders raw and silently.

And you caught your own change degrading a repo gate. Resolving chip labels via t(KEYS[type]) leaves keys appearing only as map values, which the scanners cannot resolve — all four new chip keys read as unreferenced. The second commit converts them to static t() calls, measured: detail needs-review21 → 17, dynamic call sites 36 → 35. Noticing that a correct-looking change would make a gate less able to see is the hard version of this work.

Evidence

Ablation 10 failed | 10 passed (20), direction predicted before running — and the part that makes it discriminating is stated: the ten greens are green by construction on pre-fix code (objectui#7142's own six, the two no-provider English cases where the pack value is byte-identical to the literal, the author-description short-circuit, and the no-raw-key sweep). A red half without that explanation would prove much less.

Non-en render evidence across zh / ja / ar / de, including German quotes rather than the ASCII pair, and the de quote-pairing census moved 55 → 58 in de-quote-pairing-3876.test.ts — updated in that file's established style rather than left to break.

One NOT MEASURED discarded rather than reported: the first mirror-verification died ERR_MODULE_NOT_FOUND on an unbuilt closure. Built and re-ran; all ten keys mirror exactly, with an absent-key control reading undefined on both sides.

Clause ② confirmed disengaged — FILTER_LABELS was module-private and is not exported; check:i18n-drift confirms 0 en values changed, purely additive.

The loop closed

The zh card now reads 活动(0)暂无活动记录 — precisely the string objectui#7149 recorded as half-done, minus its English half. Asserting the exact repair as a whole, rather than only the parts you touched, is what makes it verifiable that the defect is gone rather than merely reduced.

objectui#7163 filed from the same sweep, pm:blocked on this PR (the ten pack files are the gate, not the components).


Generated by Claude Code

@os-warren
os-warren marked this pull request as ready for review September 1, 2026 08:23
@os-warren
os-warren added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit d8ec8d6Sep 1, 2026
32 checks passed
@os-warren
os-warren deleted the claude/issue-7149-activity-timeline-remaining-literals branch September 1, 2026 08:37
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.

ActivityTimeline has 18 more hardcoded English literals — the empty state was one of a set, and 12 of them already have pack keys sitting unused

2 participants

@os-warren@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-detail,i18n): ActivityTimeline's remaining 18 literals resolve from the packs - #7162

Merged
os-warren merged 2 commits into
mainfrom
claude/issue-7149-activity-timeline-remaining-literals
Sep 1, 2026
Merged

fix(plugin-detail,i18n): ActivityTimeline's remaining 18 literals resolve from the packs#7162
os-warren merged 2 commits into
mainfrom
claude/issue-7149-activity-timeline-remaining-literals

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#7149

ActivityTimeline had 18 hardcoded English literals left after #7142 fixed one
of them. All 18 now resolve from the ten packs.

What this finishes, honestly

#7142 gave this component its firstt() call — the empty-state title — and
filed the sweep that found the rest. The state it left behind is what this PR
repairs: a zh activity tab read "Activity(0)暂无活动记录", the empty state
correct and the card title beside it still English. One translated string in a
component that was otherwise entirely untranslated, next to a sibling
(RecordActivityTimeline) that renders the same surface fully in Chinese.

Re-derived on origin/main head 85b5077d7 — the card's line numbers still
land exactly, and the re-sweep confirms the count.

The re-sweep: 18 literals, 20 occurrences

The card's 18 is right; (empty) is one literal at three sites (L66, L67, L74),
which is where the other two occurrences go. No nineteenth: the only other
locale-sensitive thing in the file is date.toLocaleDateString() on the
past-a-week branch, which already localizes and is byte-identical to the
sibling's own tail — not a literal, deliberately untouched.

Three groups, split by reachability rather than by defect

groupliteralsreachable fromkeys
Relative timestamps + card title5every activity tab5 existing, exact
formatFieldChange sentences6every activity tab (entries without description)6 new
Filter chips + group aria-label7published export only2 existing exact, 4 new, 1 copy change

All three shipped. The measurement that decided it: the card grouped the
formatFieldChange sentences apart because they need interpolation holes, but
that is a key-shape difference, not a reachability one. description is
optional on ActivityEntry, and formatFieldChange runs for every rendered
entry — so those six sentences render on exactly the same surface as the
timestamps. Shipping the timestamps without them would have reproduced #7142's
half-done shape one level down: "Ada Changed Status from "open" to "closed" 5分钟前".

The chips are genuinely less reachable — DetailView mounts this component at
two sites (DetailView.tsx:1586, :1713) and neither passes filterable,
so no host in this repo renders them. They are included rather than deferred
because filterable is already published API on an already-shipped export, and
3 of those 7 need no new key at all; the marginal cost was 4 keys on a PR
already touching all ten packs. Value accrues to outside consumers only, and
that is stated rather than implied.

Pack verification — read out of the pack objects, never grepped

The packs are nested, so a dotted-key grep returns a false zero for keys
that exist. Every mapping was re-verified by importing the ten pack objects and
resolving dotted paths, with both controls live:

  • positivedetail.back, detail.noActivity, detail.edit: 10/10 each
  • negativedetail.zzzAbsentControl7149, detail.noSuchKeyAtAll: 0/10

All 7 inherited "existing key" mappings are exact (en value byte-identical
to the literal), each present in 10/10 packs:

detail.justNow'just now' · detail.minutesAgo'{{count}}m ago' ·
detail.hoursAgo · detail.daysAgo · detail.activity'Activity' ·
detail.fieldChangesFilter'Field Changes' · detail.comments'Comments'

A second probe over all 2845 en leaf keys confirmed the new copy has no
existing home — Creates, Deletes, Status Changes, (empty),
Created this record, Deleted this record, Updated record,
Activity type filter all return 0 exact-value hits, with All, Create
and Delete returning non-zero on the same instrument as the live control.

One deliberate English copy change

The chip group's aria-label was "Activity type filter". It now resolves
detail.filterActivity ('Filter activity') — not an exact match, and the
card flagged it as such. Adopted anyway, and reported rather than quietly
swapped: RecordActivityTimeline.tsx:345 already uses that exact key for the
accessible name of its activity filter. Minting a near-synonym would fork
one control's name across two components — the same argument
useDetailTranslation.ts already records for common.resizeDrawer. The English
accessible name changes; no host in this repo renders the control today.

Ten new keys, all ten packs

detail.allFilter · createsFilter · deletesFilter · statusChangesFilter ·
activityEmptyValue · activityFieldChanged · activityCreated ·
activityDeleted · activityStatusChanged · activityUpdated

No inline defaultValue anywhere (#3517) — every key resolves from the packs or
from DETAIL_DEFAULT_TRANSLATIONS, which mirrors the ten new en rows
byte-for-byte (verified against the pack object; defaults-maps-mirror-en-pack
green).

Quotes live inside each pack's value so every locale punctuates its own way,
following each pack's measured convention rather than a guess: de „…“, zh
“…”, ja 「…」, fr/ru «…», en/ko/es/pt/ar ASCII. This moves the de quote
census, and de-quote-pairing-3876.test.ts is updated with it: okSpans and
{open, close, rdq} go 55 → 58 (three interpolated spans; rdq stays 0, so
each addition is a matched pair), with the comment naming why, in the file's
established style.

Evidence

Ablation — prediction stated before running: reverting only
ActivityTimeline.tsx to origin/main (packs left in place) turns the new
locale assertions red. Mutation proved on disk by blob hash
(50c29a43…6bb6ce32…, equal to origin/main: that path) and marker
count ('just now' literal 0 → 1). Restore proved by state — git diff HEAD,
git diff --cached and git status --short all empty, blob back to
50c29a43…. No rebuild leg needed and it is stated why: the test imports
./ActivityTimeline relatively, so vitest loads the source, not dist.

Result 10 failed | 10 passed (20). The ten reds are every locale-dependent
assertion. The ten greens are the discriminating half and are green by
construction
on the unfixed component — the six that came with #7142, the two
no-provider English cases (each en pack value is byte-identical to the literal
it replaces, so English cannot tell a lookup from a literal), the
author-description short-circuit, and the no-raw-key sweep.

Non-en render — the actual defect is "stays English in a zh session", so
zh/ja/ar/de carry the load:

5分钟前 · 3小时前 · 2天前 · 刚刚 (zh, {{count}} interpolated)
5分前 (ja) · منذ 5 دقيقة (ar)
将 Status code 从“open”改为“closed” (zh, three holes filled)
将 Owner 从“(空)”改为“Ada” (localized (empty), inside the sentence)
Stage von „A“ zu „B“ geändert (de, German quotes not the ASCII pair)
全部 · 字段变更 · 创建 · 删除 · 评论 · 状态变更 (six chips)
role=group name="筛选活动" / "تصفية النشاط"

Plus the exact repair, asserted whole: the zh card's textContent is now
'活动(0)暂无活动记录' — the string the card recorded as half-done, minus the
English half.

Gates — all run at ea5e7c052, exit codes captured before any pipe:

  • vitest run packages/plugin-detail packages/i18n defaults-maps-mirror-en-pack
    Test Files 181 passed (181) · Tests 2066 passed (2066)
  • type-check (both packages, script name echoed, not a zero-match) → Done
  • eslint both packages → 0 errors; ActivityTimeline.tsx contributes no
    findings, so the unused Filter import warning the card noted is gone
  • check:i18n-keys → "Every in-scope call-site key resolves against the en pack
    (2855 keys) … every call site passes exactly the arguments that value has
    holes for"
  • check:i18n-drift → "0 en value(s) changed (10 key(s) added, 0 removed)" —
    additive, no translation follow-ups owed
  • all-locales-key-parity, untranslated-identity-4376,
    fallback-placeholder-spelling-3512, de-quote-pairing-3876 — all inside the
    959-test packages/i18n run
  • changeset:check → "✅ All workspace packages are in the changeset fixed
    group. ✅ No changeset declares a major bump."

Also in this PR

The second commit resolves the chip labels with statict() calls behind a
filterLabel(type, t) switch instead of t(KEYS[type]). A key that only
appears as a map value has no call site the scanners can resolve: all four new
chip keys rendered correctly while reading as unreferenced. Measured
detail needs-review 21 → 17 and dynamic call sites with no static head
36 → 35. Same shape as the sibling's getFilterOptions.

RecordActivityTimeline.tsx was read-only reference throughout and is
unchanged.


Generated by Claude Code

…olve from the packs
objectui#7142 gave this component its first `t()` call (the empty-state title)
and filed the sweep that found the other 18. A zh activity tab read
"Activity(0)暂无活动记录" — one translated string in an otherwise English
component, beside a sibling (`RecordActivityTimeline`) that renders the same
surface fully translated.
Three groups, split by reachability rather than by defect:
- Relative timestamps + the card title render on EVERY activity tab. All five
were a pure lookup swap — the `en` pack value was already byte-identical to
the literal, and the sibling already used the same keys.
- The `formatFieldChange` sentences are assembled in code, so they needed new
keys WITH interpolation holes. Same reachability as the timestamps: they
render for any entry whose optional `description` is absent.
- The six filter chips + the chip group's accessible name need `filterable`,
which no host in this repo passes — reachable through the published export.
Ten new `detail.*` keys in all ten packs, mirrored byte-for-byte into
DETAIL_DEFAULT_TRANSLATIONS; no inline `defaultValue` anywhere (objectui#3517).
The quoted spans are inside each pack's value so every locale punctuates its
own way, which moves the de quote census 55 -> 58.
The chip group's aria-label is a deliberate English copy change: "Activity type
filter" now resolves `detail.filterActivity` ("Filter activity"), the key
`RecordActivityTimeline` already uses for the same control.
Also drops the unused `Filter` import (pre-existing eslint warning).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
A `type -> key` map read as `t(KEYS[type])` gives a key no call site the i18n
scanners can resolve: `detail.allFilter`, `createsFilter`, `deletesFilter` and
`statusChangesFilter` each rendered correctly but read as unreferenced —
`check-i18n-dead-keys` listed all four as needs-review in the `detail`
namespace, and `check:i18n-keys` counted the site among its dynamic-head
bucket.
Static `t()` calls behind a `filterLabel(type, t)` switch instead — the shape
the sibling `RecordActivityTimeline`'s `getFilterOptions` already uses. Chip
order moves to an explicit `FILTER_ORDER` array, which is what
`Object.keys(FILTER_LABELS)` was supplying before.
Measured: `detail` needs-review 21 -> 17, dynamic call sites with no static
head 36 -> 35, both gates still exit 0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3154.9 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-BqBOec6X.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)512.32KB116.52KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)177.67KB49.45KB
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)65.94KB18.64KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)249.91KB63.82KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)205.53KB55.50KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.21KB27.60KB
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-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

✅ Reviewed — will arm on green

CI still settling; auto-merge SQUASH goes on once all 30 read green. Posting now because the scope correction here is mine to record.

⚠️ My tiering was wrong, and the correction is the right one

I split the 18 literals into three tiers and ruled Tier A ships while B and C are include-or-defer. Tier C was mis-grouped.

I separated the six formatFieldChange sentences because they need interpolation holes — a key-shape difference. You measured that description is optional on ActivityEntry and formatFieldChange runs for every rendered entry, so those sentences render on exactly the same surface as Tier A. They are not less reachable; they are just a different key shape.

Deferring them on my grouping would have shipped a component whose timestamps and title are translated and whose activity descriptions are still English — objectui#7142's half-done shape reproduced one level down, which is the specific thing I flagged as the visible cost of my earlier scoping. Shipping all three tiers was correct, and the reason you give is a measurement rather than a preference.

The judgement call you surfaced instead of burying

detail.filterActivity ('Filter activity') is not identical to the aria-label 'Activity type filter' — the card flagged it and you confirmed it. You adopted the key anyway and reported it as a copy change rather than quietly swapping it, with the reason: RecordActivityTimeline.tsx:345 already uses that exact key for the accessible name of its own activity filter, so minting a near-synonym would fork one control's name across two components. And no host in this repo renders the control today.

That is the right disposal — a small deliberate copy change, named, with the alternative and its cost stated. A silent adoption would have been indistinguishable in the diff and much worse.

Four instrument checks that each could have gone wrong

A3.3 done on the pack objects, never a dotted-key grep — positive control (detail.back / noActivity / edit = 10/10) and negative control (two invented keys = 0/10). All 7 backing keys byte-identical and present 10/10.

A3.4's near-miss is worth more than the result. Repo-wide greps for 'just now' and 'm ago' return 18 and 21 hits — but every test assertion among them belongs to packages/collaboration against collaboration.* keys. A different package and namespace. Reading those hits as "something pins these literals" would have been a false positive, and the sweep only survived because you attributed each hit rather than counting them.

A3.5 verified at the mechanism, not assumed.{{name}} with no inner spaces is the one spelling both paths resolve (i18next on the provider path, interpolateFallback on the provider-less one). Both legs asserted — including a separate assertion that '{{count}}m ago' does not appear on the no-provider path, which is exactly where a wrong spelling renders raw and silently.

And you caught your own change degrading a repo gate. Resolving chip labels via t(KEYS[type]) leaves keys appearing only as map values, which the scanners cannot resolve — all four new chip keys read as unreferenced. The second commit converts them to static t() calls, measured: detail needs-review21 → 17, dynamic call sites 36 → 35. Noticing that a correct-looking change would make a gate less able to see is the hard version of this work.

Evidence

Ablation 10 failed | 10 passed (20), direction predicted before running — and the part that makes it discriminating is stated: the ten greens are green by construction on pre-fix code (objectui#7142's own six, the two no-provider English cases where the pack value is byte-identical to the literal, the author-description short-circuit, and the no-raw-key sweep). A red half without that explanation would prove much less.

Non-en render evidence across zh / ja / ar / de, including German quotes rather than the ASCII pair, and the de quote-pairing census moved 55 → 58 in de-quote-pairing-3876.test.ts — updated in that file's established style rather than left to break.

One NOT MEASURED discarded rather than reported: the first mirror-verification died ERR_MODULE_NOT_FOUND on an unbuilt closure. Built and re-ran; all ten keys mirror exactly, with an absent-key control reading undefined on both sides.

Clause ② confirmed disengaged — FILTER_LABELS was module-private and is not exported; check:i18n-drift confirms 0 en values changed, purely additive.

The loop closed

The zh card now reads 活动(0)暂无活动记录 — precisely the string objectui#7149 recorded as half-done, minus its English half. Asserting the exact repair as a whole, rather than only the parts you touched, is what makes it verifiable that the defect is gone rather than merely reduced.

objectui#7163 filed from the same sweep, pm:blocked on this PR (the ten pack files are the gate, not the components).


Generated by Claude Code

@os-warren
os-warren marked this pull request as ready for review September 1, 2026 08:23
@os-warren
os-warren added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit d8ec8d6Sep 1, 2026
32 checks passed
@os-warren
os-warren deleted the claude/issue-7149-activity-timeline-remaining-literals branch September 1, 2026 08:37
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.

ActivityTimeline has 18 more hardcoded English literals — the empty state was one of a set, and 12 of them already have pack keys sitting unused

2 participants

@os-warren@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-detail,i18n): ActivityTimeline's remaining 18 literals resolve from the packs - #7162

Merged
os-warren merged 2 commits into
mainfrom
claude/issue-7149-activity-timeline-remaining-literals
Sep 1, 2026
Merged

fix(plugin-detail,i18n): ActivityTimeline's remaining 18 literals resolve from the packs#7162
os-warren merged 2 commits into
mainfrom
claude/issue-7149-activity-timeline-remaining-literals

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#7149

ActivityTimeline had 18 hardcoded English literals left after #7142 fixed one
of them. All 18 now resolve from the ten packs.

What this finishes, honestly

#7142 gave this component its firstt() call — the empty-state title — and
filed the sweep that found the rest. The state it left behind is what this PR
repairs: a zh activity tab read "Activity(0)暂无活动记录", the empty state
correct and the card title beside it still English. One translated string in a
component that was otherwise entirely untranslated, next to a sibling
(RecordActivityTimeline) that renders the same surface fully in Chinese.

Re-derived on origin/main head 85b5077d7 — the card's line numbers still
land exactly, and the re-sweep confirms the count.

The re-sweep: 18 literals, 20 occurrences

The card's 18 is right; (empty) is one literal at three sites (L66, L67, L74),
which is where the other two occurrences go. No nineteenth: the only other
locale-sensitive thing in the file is date.toLocaleDateString() on the
past-a-week branch, which already localizes and is byte-identical to the
sibling's own tail — not a literal, deliberately untouched.

Three groups, split by reachability rather than by defect

groupliteralsreachable fromkeys
Relative timestamps + card title5every activity tab5 existing, exact
formatFieldChange sentences6every activity tab (entries without description)6 new
Filter chips + group aria-label7published export only2 existing exact, 4 new, 1 copy change

All three shipped. The measurement that decided it: the card grouped the
formatFieldChange sentences apart because they need interpolation holes, but
that is a key-shape difference, not a reachability one. description is
optional on ActivityEntry, and formatFieldChange runs for every rendered
entry — so those six sentences render on exactly the same surface as the
timestamps. Shipping the timestamps without them would have reproduced #7142's
half-done shape one level down: "Ada Changed Status from "open" to "closed" 5分钟前".

The chips are genuinely less reachable — DetailView mounts this component at
two sites (DetailView.tsx:1586, :1713) and neither passes filterable,
so no host in this repo renders them. They are included rather than deferred
because filterable is already published API on an already-shipped export, and
3 of those 7 need no new key at all; the marginal cost was 4 keys on a PR
already touching all ten packs. Value accrues to outside consumers only, and
that is stated rather than implied.

Pack verification — read out of the pack objects, never grepped

The packs are nested, so a dotted-key grep returns a false zero for keys
that exist. Every mapping was re-verified by importing the ten pack objects and
resolving dotted paths, with both controls live:

  • positivedetail.back, detail.noActivity, detail.edit: 10/10 each
  • negativedetail.zzzAbsentControl7149, detail.noSuchKeyAtAll: 0/10

All 7 inherited "existing key" mappings are exact (en value byte-identical
to the literal), each present in 10/10 packs:

detail.justNow'just now' · detail.minutesAgo'{{count}}m ago' ·
detail.hoursAgo · detail.daysAgo · detail.activity'Activity' ·
detail.fieldChangesFilter'Field Changes' · detail.comments'Comments'

A second probe over all 2845 en leaf keys confirmed the new copy has no
existing home — Creates, Deletes, Status Changes, (empty),
Created this record, Deleted this record, Updated record,
Activity type filter all return 0 exact-value hits, with All, Create
and Delete returning non-zero on the same instrument as the live control.

One deliberate English copy change

The chip group's aria-label was "Activity type filter". It now resolves
detail.filterActivity ('Filter activity') — not an exact match, and the
card flagged it as such. Adopted anyway, and reported rather than quietly
swapped: RecordActivityTimeline.tsx:345 already uses that exact key for the
accessible name of its activity filter. Minting a near-synonym would fork
one control's name across two components — the same argument
useDetailTranslation.ts already records for common.resizeDrawer. The English
accessible name changes; no host in this repo renders the control today.

Ten new keys, all ten packs

detail.allFilter · createsFilter · deletesFilter · statusChangesFilter ·
activityEmptyValue · activityFieldChanged · activityCreated ·
activityDeleted · activityStatusChanged · activityUpdated

No inline defaultValue anywhere (#3517) — every key resolves from the packs or
from DETAIL_DEFAULT_TRANSLATIONS, which mirrors the ten new en rows
byte-for-byte (verified against the pack object; defaults-maps-mirror-en-pack
green).

Quotes live inside each pack's value so every locale punctuates its own way,
following each pack's measured convention rather than a guess: de „…“, zh
“…”, ja 「…」, fr/ru «…», en/ko/es/pt/ar ASCII. This moves the de quote
census, and de-quote-pairing-3876.test.ts is updated with it: okSpans and
{open, close, rdq} go 55 → 58 (three interpolated spans; rdq stays 0, so
each addition is a matched pair), with the comment naming why, in the file's
established style.

Evidence

Ablation — prediction stated before running: reverting only
ActivityTimeline.tsx to origin/main (packs left in place) turns the new
locale assertions red. Mutation proved on disk by blob hash
(50c29a43…6bb6ce32…, equal to origin/main: that path) and marker
count ('just now' literal 0 → 1). Restore proved by state — git diff HEAD,
git diff --cached and git status --short all empty, blob back to
50c29a43…. No rebuild leg needed and it is stated why: the test imports
./ActivityTimeline relatively, so vitest loads the source, not dist.

Result 10 failed | 10 passed (20). The ten reds are every locale-dependent
assertion. The ten greens are the discriminating half and are green by
construction
on the unfixed component — the six that came with #7142, the two
no-provider English cases (each en pack value is byte-identical to the literal
it replaces, so English cannot tell a lookup from a literal), the
author-description short-circuit, and the no-raw-key sweep.

Non-en render — the actual defect is "stays English in a zh session", so
zh/ja/ar/de carry the load:

5分钟前 · 3小时前 · 2天前 · 刚刚 (zh, {{count}} interpolated)
5分前 (ja) · منذ 5 دقيقة (ar)
将 Status code 从“open”改为“closed” (zh, three holes filled)
将 Owner 从“(空)”改为“Ada” (localized (empty), inside the sentence)
Stage von „A“ zu „B“ geändert (de, German quotes not the ASCII pair)
全部 · 字段变更 · 创建 · 删除 · 评论 · 状态变更 (six chips)
role=group name="筛选活动" / "تصفية النشاط"

Plus the exact repair, asserted whole: the zh card's textContent is now
'活动(0)暂无活动记录' — the string the card recorded as half-done, minus the
English half.

Gates — all run at ea5e7c052, exit codes captured before any pipe:

  • vitest run packages/plugin-detail packages/i18n defaults-maps-mirror-en-pack
    Test Files 181 passed (181) · Tests 2066 passed (2066)
  • type-check (both packages, script name echoed, not a zero-match) → Done
  • eslint both packages → 0 errors; ActivityTimeline.tsx contributes no
    findings, so the unused Filter import warning the card noted is gone
  • check:i18n-keys → "Every in-scope call-site key resolves against the en pack
    (2855 keys) … every call site passes exactly the arguments that value has
    holes for"
  • check:i18n-drift → "0 en value(s) changed (10 key(s) added, 0 removed)" —
    additive, no translation follow-ups owed
  • all-locales-key-parity, untranslated-identity-4376,
    fallback-placeholder-spelling-3512, de-quote-pairing-3876 — all inside the
    959-test packages/i18n run
  • changeset:check → "✅ All workspace packages are in the changeset fixed
    group. ✅ No changeset declares a major bump."

Also in this PR

The second commit resolves the chip labels with statict() calls behind a
filterLabel(type, t) switch instead of t(KEYS[type]). A key that only
appears as a map value has no call site the scanners can resolve: all four new
chip keys rendered correctly while reading as unreferenced. Measured
detail needs-review 21 → 17 and dynamic call sites with no static head
36 → 35. Same shape as the sibling's getFilterOptions.

RecordActivityTimeline.tsx was read-only reference throughout and is
unchanged.


Generated by Claude Code

…olve from the packs
objectui#7142 gave this component its first `t()` call (the empty-state title)
and filed the sweep that found the other 18. A zh activity tab read
"Activity(0)暂无活动记录" — one translated string in an otherwise English
component, beside a sibling (`RecordActivityTimeline`) that renders the same
surface fully translated.
Three groups, split by reachability rather than by defect:
- Relative timestamps + the card title render on EVERY activity tab. All five
were a pure lookup swap — the `en` pack value was already byte-identical to
the literal, and the sibling already used the same keys.
- The `formatFieldChange` sentences are assembled in code, so they needed new
keys WITH interpolation holes. Same reachability as the timestamps: they
render for any entry whose optional `description` is absent.
- The six filter chips + the chip group's accessible name need `filterable`,
which no host in this repo passes — reachable through the published export.
Ten new `detail.*` keys in all ten packs, mirrored byte-for-byte into
DETAIL_DEFAULT_TRANSLATIONS; no inline `defaultValue` anywhere (objectui#3517).
The quoted spans are inside each pack's value so every locale punctuates its
own way, which moves the de quote census 55 -> 58.
The chip group's aria-label is a deliberate English copy change: "Activity type
filter" now resolves `detail.filterActivity` ("Filter activity"), the key
`RecordActivityTimeline` already uses for the same control.
Also drops the unused `Filter` import (pre-existing eslint warning).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
A `type -> key` map read as `t(KEYS[type])` gives a key no call site the i18n
scanners can resolve: `detail.allFilter`, `createsFilter`, `deletesFilter` and
`statusChangesFilter` each rendered correctly but read as unreferenced —
`check-i18n-dead-keys` listed all four as needs-review in the `detail`
namespace, and `check:i18n-keys` counted the site among its dynamic-head
bucket.
Static `t()` calls behind a `filterLabel(type, t)` switch instead — the shape
the sibling `RecordActivityTimeline`'s `getFilterOptions` already uses. Chip
order moves to an explicit `FILTER_ORDER` array, which is what
`Object.keys(FILTER_LABELS)` was supplying before.
Measured: `detail` needs-review 21 -> 17, dynamic call sites with no static
head 36 -> 35, both gates still exit 0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3154.9 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-BqBOec6X.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)512.32KB116.52KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)177.67KB49.45KB
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)65.94KB18.64KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)249.91KB63.82KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)205.53KB55.50KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.21KB27.60KB
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-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

✅ Reviewed — will arm on green

CI still settling; auto-merge SQUASH goes on once all 30 read green. Posting now because the scope correction here is mine to record.

⚠️ My tiering was wrong, and the correction is the right one

I split the 18 literals into three tiers and ruled Tier A ships while B and C are include-or-defer. Tier C was mis-grouped.

I separated the six formatFieldChange sentences because they need interpolation holes — a key-shape difference. You measured that description is optional on ActivityEntry and formatFieldChange runs for every rendered entry, so those sentences render on exactly the same surface as Tier A. They are not less reachable; they are just a different key shape.

Deferring them on my grouping would have shipped a component whose timestamps and title are translated and whose activity descriptions are still English — objectui#7142's half-done shape reproduced one level down, which is the specific thing I flagged as the visible cost of my earlier scoping. Shipping all three tiers was correct, and the reason you give is a measurement rather than a preference.

The judgement call you surfaced instead of burying

detail.filterActivity ('Filter activity') is not identical to the aria-label 'Activity type filter' — the card flagged it and you confirmed it. You adopted the key anyway and reported it as a copy change rather than quietly swapping it, with the reason: RecordActivityTimeline.tsx:345 already uses that exact key for the accessible name of its own activity filter, so minting a near-synonym would fork one control's name across two components. And no host in this repo renders the control today.

That is the right disposal — a small deliberate copy change, named, with the alternative and its cost stated. A silent adoption would have been indistinguishable in the diff and much worse.

Four instrument checks that each could have gone wrong

A3.3 done on the pack objects, never a dotted-key grep — positive control (detail.back / noActivity / edit = 10/10) and negative control (two invented keys = 0/10). All 7 backing keys byte-identical and present 10/10.

A3.4's near-miss is worth more than the result. Repo-wide greps for 'just now' and 'm ago' return 18 and 21 hits — but every test assertion among them belongs to packages/collaboration against collaboration.* keys. A different package and namespace. Reading those hits as "something pins these literals" would have been a false positive, and the sweep only survived because you attributed each hit rather than counting them.

A3.5 verified at the mechanism, not assumed.{{name}} with no inner spaces is the one spelling both paths resolve (i18next on the provider path, interpolateFallback on the provider-less one). Both legs asserted — including a separate assertion that '{{count}}m ago' does not appear on the no-provider path, which is exactly where a wrong spelling renders raw and silently.

And you caught your own change degrading a repo gate. Resolving chip labels via t(KEYS[type]) leaves keys appearing only as map values, which the scanners cannot resolve — all four new chip keys read as unreferenced. The second commit converts them to static t() calls, measured: detail needs-review21 → 17, dynamic call sites 36 → 35. Noticing that a correct-looking change would make a gate less able to see is the hard version of this work.

Evidence

Ablation 10 failed | 10 passed (20), direction predicted before running — and the part that makes it discriminating is stated: the ten greens are green by construction on pre-fix code (objectui#7142's own six, the two no-provider English cases where the pack value is byte-identical to the literal, the author-description short-circuit, and the no-raw-key sweep). A red half without that explanation would prove much less.

Non-en render evidence across zh / ja / ar / de, including German quotes rather than the ASCII pair, and the de quote-pairing census moved 55 → 58 in de-quote-pairing-3876.test.ts — updated in that file's established style rather than left to break.

One NOT MEASURED discarded rather than reported: the first mirror-verification died ERR_MODULE_NOT_FOUND on an unbuilt closure. Built and re-ran; all ten keys mirror exactly, with an absent-key control reading undefined on both sides.

Clause ② confirmed disengaged — FILTER_LABELS was module-private and is not exported; check:i18n-drift confirms 0 en values changed, purely additive.

The loop closed

The zh card now reads 活动(0)暂无活动记录 — precisely the string objectui#7149 recorded as half-done, minus its English half. Asserting the exact repair as a whole, rather than only the parts you touched, is what makes it verifiable that the defect is gone rather than merely reduced.

objectui#7163 filed from the same sweep, pm:blocked on this PR (the ten pack files are the gate, not the components).


Generated by Claude Code

@os-warren
os-warren marked this pull request as ready for review September 1, 2026 08:23
@os-warren
os-warren added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit d8ec8d6Sep 1, 2026
32 checks passed
@os-warren
os-warren deleted the claude/issue-7149-activity-timeline-remaining-literals branch September 1, 2026 08:37
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.

ActivityTimeline has 18 more hardcoded English literals — the empty state was one of a set, and 12 of them already have pack keys sitting unused

2 participants

@os-warren@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-detail,i18n): ActivityTimeline's remaining 18 literals resolve from the packs - #7162

Merged
os-warren merged 2 commits into
mainfrom
claude/issue-7149-activity-timeline-remaining-literals
Sep 1, 2026
Merged

fix(plugin-detail,i18n): ActivityTimeline's remaining 18 literals resolve from the packs#7162
os-warren merged 2 commits into
mainfrom
claude/issue-7149-activity-timeline-remaining-literals

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#7149

ActivityTimeline had 18 hardcoded English literals left after #7142 fixed one
of them. All 18 now resolve from the ten packs.

What this finishes, honestly

#7142 gave this component its firstt() call — the empty-state title — and
filed the sweep that found the rest. The state it left behind is what this PR
repairs: a zh activity tab read "Activity(0)暂无活动记录", the empty state
correct and the card title beside it still English. One translated string in a
component that was otherwise entirely untranslated, next to a sibling
(RecordActivityTimeline) that renders the same surface fully in Chinese.

Re-derived on origin/main head 85b5077d7 — the card's line numbers still
land exactly, and the re-sweep confirms the count.

The re-sweep: 18 literals, 20 occurrences

The card's 18 is right; (empty) is one literal at three sites (L66, L67, L74),
which is where the other two occurrences go. No nineteenth: the only other
locale-sensitive thing in the file is date.toLocaleDateString() on the
past-a-week branch, which already localizes and is byte-identical to the
sibling's own tail — not a literal, deliberately untouched.

Three groups, split by reachability rather than by defect

groupliteralsreachable fromkeys
Relative timestamps + card title5every activity tab5 existing, exact
formatFieldChange sentences6every activity tab (entries without description)6 new
Filter chips + group aria-label7published export only2 existing exact, 4 new, 1 copy change

All three shipped. The measurement that decided it: the card grouped the
formatFieldChange sentences apart because they need interpolation holes, but
that is a key-shape difference, not a reachability one. description is
optional on ActivityEntry, and formatFieldChange runs for every rendered
entry — so those six sentences render on exactly the same surface as the
timestamps. Shipping the timestamps without them would have reproduced #7142's
half-done shape one level down: "Ada Changed Status from "open" to "closed" 5分钟前".

The chips are genuinely less reachable — DetailView mounts this component at
two sites (DetailView.tsx:1586, :1713) and neither passes filterable,
so no host in this repo renders them. They are included rather than deferred
because filterable is already published API on an already-shipped export, and
3 of those 7 need no new key at all; the marginal cost was 4 keys on a PR
already touching all ten packs. Value accrues to outside consumers only, and
that is stated rather than implied.

Pack verification — read out of the pack objects, never grepped

The packs are nested, so a dotted-key grep returns a false zero for keys
that exist. Every mapping was re-verified by importing the ten pack objects and
resolving dotted paths, with both controls live:

  • positivedetail.back, detail.noActivity, detail.edit: 10/10 each
  • negativedetail.zzzAbsentControl7149, detail.noSuchKeyAtAll: 0/10

All 7 inherited "existing key" mappings are exact (en value byte-identical
to the literal), each present in 10/10 packs:

detail.justNow'just now' · detail.minutesAgo'{{count}}m ago' ·
detail.hoursAgo · detail.daysAgo · detail.activity'Activity' ·
detail.fieldChangesFilter'Field Changes' · detail.comments'Comments'

A second probe over all 2845 en leaf keys confirmed the new copy has no
existing home — Creates, Deletes, Status Changes, (empty),
Created this record, Deleted this record, Updated record,
Activity type filter all return 0 exact-value hits, with All, Create
and Delete returning non-zero on the same instrument as the live control.

One deliberate English copy change

The chip group's aria-label was "Activity type filter". It now resolves
detail.filterActivity ('Filter activity') — not an exact match, and the
card flagged it as such. Adopted anyway, and reported rather than quietly
swapped: RecordActivityTimeline.tsx:345 already uses that exact key for the
accessible name of its activity filter. Minting a near-synonym would fork
one control's name across two components — the same argument
useDetailTranslation.ts already records for common.resizeDrawer. The English
accessible name changes; no host in this repo renders the control today.

Ten new keys, all ten packs

detail.allFilter · createsFilter · deletesFilter · statusChangesFilter ·
activityEmptyValue · activityFieldChanged · activityCreated ·
activityDeleted · activityStatusChanged · activityUpdated

No inline defaultValue anywhere (#3517) — every key resolves from the packs or
from DETAIL_DEFAULT_TRANSLATIONS, which mirrors the ten new en rows
byte-for-byte (verified against the pack object; defaults-maps-mirror-en-pack
green).

Quotes live inside each pack's value so every locale punctuates its own way,
following each pack's measured convention rather than a guess: de „…“, zh
“…”, ja 「…」, fr/ru «…», en/ko/es/pt/ar ASCII. This moves the de quote
census, and de-quote-pairing-3876.test.ts is updated with it: okSpans and
{open, close, rdq} go 55 → 58 (three interpolated spans; rdq stays 0, so
each addition is a matched pair), with the comment naming why, in the file's
established style.

Evidence

Ablation — prediction stated before running: reverting only
ActivityTimeline.tsx to origin/main (packs left in place) turns the new
locale assertions red. Mutation proved on disk by blob hash
(50c29a43…6bb6ce32…, equal to origin/main: that path) and marker
count ('just now' literal 0 → 1). Restore proved by state — git diff HEAD,
git diff --cached and git status --short all empty, blob back to
50c29a43…. No rebuild leg needed and it is stated why: the test imports
./ActivityTimeline relatively, so vitest loads the source, not dist.

Result 10 failed | 10 passed (20). The ten reds are every locale-dependent
assertion. The ten greens are the discriminating half and are green by
construction
on the unfixed component — the six that came with #7142, the two
no-provider English cases (each en pack value is byte-identical to the literal
it replaces, so English cannot tell a lookup from a literal), the
author-description short-circuit, and the no-raw-key sweep.

Non-en render — the actual defect is "stays English in a zh session", so
zh/ja/ar/de carry the load:

5分钟前 · 3小时前 · 2天前 · 刚刚 (zh, {{count}} interpolated)
5分前 (ja) · منذ 5 دقيقة (ar)
将 Status code 从“open”改为“closed” (zh, three holes filled)
将 Owner 从“(空)”改为“Ada” (localized (empty), inside the sentence)
Stage von „A“ zu „B“ geändert (de, German quotes not the ASCII pair)
全部 · 字段变更 · 创建 · 删除 · 评论 · 状态变更 (six chips)
role=group name="筛选活动" / "تصفية النشاط"

Plus the exact repair, asserted whole: the zh card's textContent is now
'活动(0)暂无活动记录' — the string the card recorded as half-done, minus the
English half.

Gates — all run at ea5e7c052, exit codes captured before any pipe:

  • vitest run packages/plugin-detail packages/i18n defaults-maps-mirror-en-pack
    Test Files 181 passed (181) · Tests 2066 passed (2066)
  • type-check (both packages, script name echoed, not a zero-match) → Done
  • eslint both packages → 0 errors; ActivityTimeline.tsx contributes no
    findings, so the unused Filter import warning the card noted is gone
  • check:i18n-keys → "Every in-scope call-site key resolves against the en pack
    (2855 keys) … every call site passes exactly the arguments that value has
    holes for"
  • check:i18n-drift → "0 en value(s) changed (10 key(s) added, 0 removed)" —
    additive, no translation follow-ups owed
  • all-locales-key-parity, untranslated-identity-4376,
    fallback-placeholder-spelling-3512, de-quote-pairing-3876 — all inside the
    959-test packages/i18n run
  • changeset:check → "✅ All workspace packages are in the changeset fixed
    group. ✅ No changeset declares a major bump."

Also in this PR

The second commit resolves the chip labels with statict() calls behind a
filterLabel(type, t) switch instead of t(KEYS[type]). A key that only
appears as a map value has no call site the scanners can resolve: all four new
chip keys rendered correctly while reading as unreferenced. Measured
detail needs-review 21 → 17 and dynamic call sites with no static head
36 → 35. Same shape as the sibling's getFilterOptions.

RecordActivityTimeline.tsx was read-only reference throughout and is
unchanged.


Generated by Claude Code

…olve from the packs
objectui#7142 gave this component its first `t()` call (the empty-state title)
and filed the sweep that found the other 18. A zh activity tab read
"Activity(0)暂无活动记录" — one translated string in an otherwise English
component, beside a sibling (`RecordActivityTimeline`) that renders the same
surface fully translated.
Three groups, split by reachability rather than by defect:
- Relative timestamps + the card title render on EVERY activity tab. All five
were a pure lookup swap — the `en` pack value was already byte-identical to
the literal, and the sibling already used the same keys.
- The `formatFieldChange` sentences are assembled in code, so they needed new
keys WITH interpolation holes. Same reachability as the timestamps: they
render for any entry whose optional `description` is absent.
- The six filter chips + the chip group's accessible name need `filterable`,
which no host in this repo passes — reachable through the published export.
Ten new `detail.*` keys in all ten packs, mirrored byte-for-byte into
DETAIL_DEFAULT_TRANSLATIONS; no inline `defaultValue` anywhere (objectui#3517).
The quoted spans are inside each pack's value so every locale punctuates its
own way, which moves the de quote census 55 -> 58.
The chip group's aria-label is a deliberate English copy change: "Activity type
filter" now resolves `detail.filterActivity` ("Filter activity"), the key
`RecordActivityTimeline` already uses for the same control.
Also drops the unused `Filter` import (pre-existing eslint warning).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
A `type -> key` map read as `t(KEYS[type])` gives a key no call site the i18n
scanners can resolve: `detail.allFilter`, `createsFilter`, `deletesFilter` and
`statusChangesFilter` each rendered correctly but read as unreferenced —
`check-i18n-dead-keys` listed all four as needs-review in the `detail`
namespace, and `check:i18n-keys` counted the site among its dynamic-head
bucket.
Static `t()` calls behind a `filterLabel(type, t)` switch instead — the shape
the sibling `RecordActivityTimeline`'s `getFilterOptions` already uses. Chip
order moves to an explicit `FILTER_ORDER` array, which is what
`Object.keys(FILTER_LABELS)` was supplying before.
Measured: `detail` needs-review 21 -> 17, dynamic call sites with no static
head 36 -> 35, both gates still exit 0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3154.9 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-BqBOec6X.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)512.32KB116.52KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)177.67KB49.45KB
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)65.94KB18.64KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)249.91KB63.82KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)205.53KB55.50KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.21KB27.60KB
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-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

✅ Reviewed — will arm on green

CI still settling; auto-merge SQUASH goes on once all 30 read green. Posting now because the scope correction here is mine to record.

⚠️ My tiering was wrong, and the correction is the right one

I split the 18 literals into three tiers and ruled Tier A ships while B and C are include-or-defer. Tier C was mis-grouped.

I separated the six formatFieldChange sentences because they need interpolation holes — a key-shape difference. You measured that description is optional on ActivityEntry and formatFieldChange runs for every rendered entry, so those sentences render on exactly the same surface as Tier A. They are not less reachable; they are just a different key shape.

Deferring them on my grouping would have shipped a component whose timestamps and title are translated and whose activity descriptions are still English — objectui#7142's half-done shape reproduced one level down, which is the specific thing I flagged as the visible cost of my earlier scoping. Shipping all three tiers was correct, and the reason you give is a measurement rather than a preference.

The judgement call you surfaced instead of burying

detail.filterActivity ('Filter activity') is not identical to the aria-label 'Activity type filter' — the card flagged it and you confirmed it. You adopted the key anyway and reported it as a copy change rather than quietly swapping it, with the reason: RecordActivityTimeline.tsx:345 already uses that exact key for the accessible name of its own activity filter, so minting a near-synonym would fork one control's name across two components. And no host in this repo renders the control today.

That is the right disposal — a small deliberate copy change, named, with the alternative and its cost stated. A silent adoption would have been indistinguishable in the diff and much worse.

Four instrument checks that each could have gone wrong

A3.3 done on the pack objects, never a dotted-key grep — positive control (detail.back / noActivity / edit = 10/10) and negative control (two invented keys = 0/10). All 7 backing keys byte-identical and present 10/10.

A3.4's near-miss is worth more than the result. Repo-wide greps for 'just now' and 'm ago' return 18 and 21 hits — but every test assertion among them belongs to packages/collaboration against collaboration.* keys. A different package and namespace. Reading those hits as "something pins these literals" would have been a false positive, and the sweep only survived because you attributed each hit rather than counting them.

A3.5 verified at the mechanism, not assumed.{{name}} with no inner spaces is the one spelling both paths resolve (i18next on the provider path, interpolateFallback on the provider-less one). Both legs asserted — including a separate assertion that '{{count}}m ago' does not appear on the no-provider path, which is exactly where a wrong spelling renders raw and silently.

And you caught your own change degrading a repo gate. Resolving chip labels via t(KEYS[type]) leaves keys appearing only as map values, which the scanners cannot resolve — all four new chip keys read as unreferenced. The second commit converts them to static t() calls, measured: detail needs-review21 → 17, dynamic call sites 36 → 35. Noticing that a correct-looking change would make a gate less able to see is the hard version of this work.

Evidence

Ablation 10 failed | 10 passed (20), direction predicted before running — and the part that makes it discriminating is stated: the ten greens are green by construction on pre-fix code (objectui#7142's own six, the two no-provider English cases where the pack value is byte-identical to the literal, the author-description short-circuit, and the no-raw-key sweep). A red half without that explanation would prove much less.

Non-en render evidence across zh / ja / ar / de, including German quotes rather than the ASCII pair, and the de quote-pairing census moved 55 → 58 in de-quote-pairing-3876.test.ts — updated in that file's established style rather than left to break.

One NOT MEASURED discarded rather than reported: the first mirror-verification died ERR_MODULE_NOT_FOUND on an unbuilt closure. Built and re-ran; all ten keys mirror exactly, with an absent-key control reading undefined on both sides.

Clause ② confirmed disengaged — FILTER_LABELS was module-private and is not exported; check:i18n-drift confirms 0 en values changed, purely additive.

The loop closed

The zh card now reads 活动(0)暂无活动记录 — precisely the string objectui#7149 recorded as half-done, minus its English half. Asserting the exact repair as a whole, rather than only the parts you touched, is what makes it verifiable that the defect is gone rather than merely reduced.

objectui#7163 filed from the same sweep, pm:blocked on this PR (the ten pack files are the gate, not the components).


Generated by Claude Code

@os-warren
os-warren marked this pull request as ready for review September 1, 2026 08:23
@os-warren
os-warren added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit d8ec8d6Sep 1, 2026
32 checks passed
@os-warren
os-warren deleted the claude/issue-7149-activity-timeline-remaining-literals branch September 1, 2026 08:37
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.

ActivityTimeline has 18 more hardcoded English literals — the empty state was one of a set, and 12 of them already have pack keys sitting unused

2 participants

@os-warren@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-detail,i18n): ActivityTimeline's remaining 18 literals resolve from the packs - #7162

Merged
os-warren merged 2 commits into
mainfrom
claude/issue-7149-activity-timeline-remaining-literals
Sep 1, 2026
Merged

fix(plugin-detail,i18n): ActivityTimeline's remaining 18 literals resolve from the packs#7162
os-warren merged 2 commits into
mainfrom
claude/issue-7149-activity-timeline-remaining-literals

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#7149

ActivityTimeline had 18 hardcoded English literals left after #7142 fixed one
of them. All 18 now resolve from the ten packs.

What this finishes, honestly

#7142 gave this component its firstt() call — the empty-state title — and
filed the sweep that found the rest. The state it left behind is what this PR
repairs: a zh activity tab read "Activity(0)暂无活动记录", the empty state
correct and the card title beside it still English. One translated string in a
component that was otherwise entirely untranslated, next to a sibling
(RecordActivityTimeline) that renders the same surface fully in Chinese.

Re-derived on origin/main head 85b5077d7 — the card's line numbers still
land exactly, and the re-sweep confirms the count.

The re-sweep: 18 literals, 20 occurrences

The card's 18 is right; (empty) is one literal at three sites (L66, L67, L74),
which is where the other two occurrences go. No nineteenth: the only other
locale-sensitive thing in the file is date.toLocaleDateString() on the
past-a-week branch, which already localizes and is byte-identical to the
sibling's own tail — not a literal, deliberately untouched.

Three groups, split by reachability rather than by defect

groupliteralsreachable fromkeys
Relative timestamps + card title5every activity tab5 existing, exact
formatFieldChange sentences6every activity tab (entries without description)6 new
Filter chips + group aria-label7published export only2 existing exact, 4 new, 1 copy change

All three shipped. The measurement that decided it: the card grouped the
formatFieldChange sentences apart because they need interpolation holes, but
that is a key-shape difference, not a reachability one. description is
optional on ActivityEntry, and formatFieldChange runs for every rendered
entry — so those six sentences render on exactly the same surface as the
timestamps. Shipping the timestamps without them would have reproduced #7142's
half-done shape one level down: "Ada Changed Status from "open" to "closed" 5分钟前".

The chips are genuinely less reachable — DetailView mounts this component at
two sites (DetailView.tsx:1586, :1713) and neither passes filterable,
so no host in this repo renders them. They are included rather than deferred
because filterable is already published API on an already-shipped export, and
3 of those 7 need no new key at all; the marginal cost was 4 keys on a PR
already touching all ten packs. Value accrues to outside consumers only, and
that is stated rather than implied.

Pack verification — read out of the pack objects, never grepped

The packs are nested, so a dotted-key grep returns a false zero for keys
that exist. Every mapping was re-verified by importing the ten pack objects and
resolving dotted paths, with both controls live:

  • positivedetail.back, detail.noActivity, detail.edit: 10/10 each
  • negativedetail.zzzAbsentControl7149, detail.noSuchKeyAtAll: 0/10

All 7 inherited "existing key" mappings are exact (en value byte-identical
to the literal), each present in 10/10 packs:

detail.justNow'just now' · detail.minutesAgo'{{count}}m ago' ·
detail.hoursAgo · detail.daysAgo · detail.activity'Activity' ·
detail.fieldChangesFilter'Field Changes' · detail.comments'Comments'

A second probe over all 2845 en leaf keys confirmed the new copy has no
existing home — Creates, Deletes, Status Changes, (empty),
Created this record, Deleted this record, Updated record,
Activity type filter all return 0 exact-value hits, with All, Create
and Delete returning non-zero on the same instrument as the live control.

One deliberate English copy change

The chip group's aria-label was "Activity type filter". It now resolves
detail.filterActivity ('Filter activity') — not an exact match, and the
card flagged it as such. Adopted anyway, and reported rather than quietly
swapped: RecordActivityTimeline.tsx:345 already uses that exact key for the
accessible name of its activity filter. Minting a near-synonym would fork
one control's name across two components — the same argument
useDetailTranslation.ts already records for common.resizeDrawer. The English
accessible name changes; no host in this repo renders the control today.

Ten new keys, all ten packs

detail.allFilter · createsFilter · deletesFilter · statusChangesFilter ·
activityEmptyValue · activityFieldChanged · activityCreated ·
activityDeleted · activityStatusChanged · activityUpdated

No inline defaultValue anywhere (#3517) — every key resolves from the packs or
from DETAIL_DEFAULT_TRANSLATIONS, which mirrors the ten new en rows
byte-for-byte (verified against the pack object; defaults-maps-mirror-en-pack
green).

Quotes live inside each pack's value so every locale punctuates its own way,
following each pack's measured convention rather than a guess: de „…“, zh
“…”, ja 「…」, fr/ru «…», en/ko/es/pt/ar ASCII. This moves the de quote
census, and de-quote-pairing-3876.test.ts is updated with it: okSpans and
{open, close, rdq} go 55 → 58 (three interpolated spans; rdq stays 0, so
each addition is a matched pair), with the comment naming why, in the file's
established style.

Evidence

Ablation — prediction stated before running: reverting only
ActivityTimeline.tsx to origin/main (packs left in place) turns the new
locale assertions red. Mutation proved on disk by blob hash
(50c29a43…6bb6ce32…, equal to origin/main: that path) and marker
count ('just now' literal 0 → 1). Restore proved by state — git diff HEAD,
git diff --cached and git status --short all empty, blob back to
50c29a43…. No rebuild leg needed and it is stated why: the test imports
./ActivityTimeline relatively, so vitest loads the source, not dist.

Result 10 failed | 10 passed (20). The ten reds are every locale-dependent
assertion. The ten greens are the discriminating half and are green by
construction
on the unfixed component — the six that came with #7142, the two
no-provider English cases (each en pack value is byte-identical to the literal
it replaces, so English cannot tell a lookup from a literal), the
author-description short-circuit, and the no-raw-key sweep.

Non-en render — the actual defect is "stays English in a zh session", so
zh/ja/ar/de carry the load:

5分钟前 · 3小时前 · 2天前 · 刚刚 (zh, {{count}} interpolated)
5分前 (ja) · منذ 5 دقيقة (ar)
将 Status code 从“open”改为“closed” (zh, three holes filled)
将 Owner 从“(空)”改为“Ada” (localized (empty), inside the sentence)
Stage von „A“ zu „B“ geändert (de, German quotes not the ASCII pair)
全部 · 字段变更 · 创建 · 删除 · 评论 · 状态变更 (six chips)
role=group name="筛选活动" / "تصفية النشاط"

Plus the exact repair, asserted whole: the zh card's textContent is now
'活动(0)暂无活动记录' — the string the card recorded as half-done, minus the
English half.

Gates — all run at ea5e7c052, exit codes captured before any pipe:

  • vitest run packages/plugin-detail packages/i18n defaults-maps-mirror-en-pack
    Test Files 181 passed (181) · Tests 2066 passed (2066)
  • type-check (both packages, script name echoed, not a zero-match) → Done
  • eslint both packages → 0 errors; ActivityTimeline.tsx contributes no
    findings, so the unused Filter import warning the card noted is gone
  • check:i18n-keys → "Every in-scope call-site key resolves against the en pack
    (2855 keys) … every call site passes exactly the arguments that value has
    holes for"
  • check:i18n-drift → "0 en value(s) changed (10 key(s) added, 0 removed)" —
    additive, no translation follow-ups owed
  • all-locales-key-parity, untranslated-identity-4376,
    fallback-placeholder-spelling-3512, de-quote-pairing-3876 — all inside the
    959-test packages/i18n run
  • changeset:check → "✅ All workspace packages are in the changeset fixed
    group. ✅ No changeset declares a major bump."

Also in this PR

The second commit resolves the chip labels with statict() calls behind a
filterLabel(type, t) switch instead of t(KEYS[type]). A key that only
appears as a map value has no call site the scanners can resolve: all four new
chip keys rendered correctly while reading as unreferenced. Measured
detail needs-review 21 → 17 and dynamic call sites with no static head
36 → 35. Same shape as the sibling's getFilterOptions.

RecordActivityTimeline.tsx was read-only reference throughout and is
unchanged.


Generated by Claude Code

…olve from the packs
objectui#7142 gave this component its first `t()` call (the empty-state title)
and filed the sweep that found the other 18. A zh activity tab read
"Activity(0)暂无活动记录" — one translated string in an otherwise English
component, beside a sibling (`RecordActivityTimeline`) that renders the same
surface fully translated.
Three groups, split by reachability rather than by defect:
- Relative timestamps + the card title render on EVERY activity tab. All five
were a pure lookup swap — the `en` pack value was already byte-identical to
the literal, and the sibling already used the same keys.
- The `formatFieldChange` sentences are assembled in code, so they needed new
keys WITH interpolation holes. Same reachability as the timestamps: they
render for any entry whose optional `description` is absent.
- The six filter chips + the chip group's accessible name need `filterable`,
which no host in this repo passes — reachable through the published export.
Ten new `detail.*` keys in all ten packs, mirrored byte-for-byte into
DETAIL_DEFAULT_TRANSLATIONS; no inline `defaultValue` anywhere (objectui#3517).
The quoted spans are inside each pack's value so every locale punctuates its
own way, which moves the de quote census 55 -> 58.
The chip group's aria-label is a deliberate English copy change: "Activity type
filter" now resolves `detail.filterActivity` ("Filter activity"), the key
`RecordActivityTimeline` already uses for the same control.
Also drops the unused `Filter` import (pre-existing eslint warning).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
A `type -> key` map read as `t(KEYS[type])` gives a key no call site the i18n
scanners can resolve: `detail.allFilter`, `createsFilter`, `deletesFilter` and
`statusChangesFilter` each rendered correctly but read as unreferenced —
`check-i18n-dead-keys` listed all four as needs-review in the `detail`
namespace, and `check:i18n-keys` counted the site among its dynamic-head
bucket.
Static `t()` calls behind a `filterLabel(type, t)` switch instead — the shape
the sibling `RecordActivityTimeline`'s `getFilterOptions` already uses. Chip
order moves to an explicit `FILTER_ORDER` array, which is what
`Object.keys(FILTER_LABELS)` was supplying before.
Measured: `detail` needs-review 21 -> 17, dynamic call sites with no static
head 36 -> 35, both gates still exit 0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3154.9 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-BqBOec6X.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)512.32KB116.52KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)177.67KB49.45KB
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)65.94KB18.64KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)249.91KB63.82KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)205.53KB55.50KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.21KB27.60KB
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-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

✅ Reviewed — will arm on green

CI still settling; auto-merge SQUASH goes on once all 30 read green. Posting now because the scope correction here is mine to record.

⚠️ My tiering was wrong, and the correction is the right one

I split the 18 literals into three tiers and ruled Tier A ships while B and C are include-or-defer. Tier C was mis-grouped.

I separated the six formatFieldChange sentences because they need interpolation holes — a key-shape difference. You measured that description is optional on ActivityEntry and formatFieldChange runs for every rendered entry, so those sentences render on exactly the same surface as Tier A. They are not less reachable; they are just a different key shape.

Deferring them on my grouping would have shipped a component whose timestamps and title are translated and whose activity descriptions are still English — objectui#7142's half-done shape reproduced one level down, which is the specific thing I flagged as the visible cost of my earlier scoping. Shipping all three tiers was correct, and the reason you give is a measurement rather than a preference.

The judgement call you surfaced instead of burying

detail.filterActivity ('Filter activity') is not identical to the aria-label 'Activity type filter' — the card flagged it and you confirmed it. You adopted the key anyway and reported it as a copy change rather than quietly swapping it, with the reason: RecordActivityTimeline.tsx:345 already uses that exact key for the accessible name of its own activity filter, so minting a near-synonym would fork one control's name across two components. And no host in this repo renders the control today.

That is the right disposal — a small deliberate copy change, named, with the alternative and its cost stated. A silent adoption would have been indistinguishable in the diff and much worse.

Four instrument checks that each could have gone wrong

A3.3 done on the pack objects, never a dotted-key grep — positive control (detail.back / noActivity / edit = 10/10) and negative control (two invented keys = 0/10). All 7 backing keys byte-identical and present 10/10.

A3.4's near-miss is worth more than the result. Repo-wide greps for 'just now' and 'm ago' return 18 and 21 hits — but every test assertion among them belongs to packages/collaboration against collaboration.* keys. A different package and namespace. Reading those hits as "something pins these literals" would have been a false positive, and the sweep only survived because you attributed each hit rather than counting them.

A3.5 verified at the mechanism, not assumed.{{name}} with no inner spaces is the one spelling both paths resolve (i18next on the provider path, interpolateFallback on the provider-less one). Both legs asserted — including a separate assertion that '{{count}}m ago' does not appear on the no-provider path, which is exactly where a wrong spelling renders raw and silently.

And you caught your own change degrading a repo gate. Resolving chip labels via t(KEYS[type]) leaves keys appearing only as map values, which the scanners cannot resolve — all four new chip keys read as unreferenced. The second commit converts them to static t() calls, measured: detail needs-review21 → 17, dynamic call sites 36 → 35. Noticing that a correct-looking change would make a gate less able to see is the hard version of this work.

Evidence

Ablation 10 failed | 10 passed (20), direction predicted before running — and the part that makes it discriminating is stated: the ten greens are green by construction on pre-fix code (objectui#7142's own six, the two no-provider English cases where the pack value is byte-identical to the literal, the author-description short-circuit, and the no-raw-key sweep). A red half without that explanation would prove much less.

Non-en render evidence across zh / ja / ar / de, including German quotes rather than the ASCII pair, and the de quote-pairing census moved 55 → 58 in de-quote-pairing-3876.test.ts — updated in that file's established style rather than left to break.

One NOT MEASURED discarded rather than reported: the first mirror-verification died ERR_MODULE_NOT_FOUND on an unbuilt closure. Built and re-ran; all ten keys mirror exactly, with an absent-key control reading undefined on both sides.

Clause ② confirmed disengaged — FILTER_LABELS was module-private and is not exported; check:i18n-drift confirms 0 en values changed, purely additive.

The loop closed

The zh card now reads 活动(0)暂无活动记录 — precisely the string objectui#7149 recorded as half-done, minus its English half. Asserting the exact repair as a whole, rather than only the parts you touched, is what makes it verifiable that the defect is gone rather than merely reduced.

objectui#7163 filed from the same sweep, pm:blocked on this PR (the ten pack files are the gate, not the components).


Generated by Claude Code

@os-warren
os-warren marked this pull request as ready for review September 1, 2026 08:23
@os-warren
os-warren added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit d8ec8d6Sep 1, 2026
32 checks passed
@os-warren
os-warren deleted the claude/issue-7149-activity-timeline-remaining-literals branch September 1, 2026 08:37
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.

ActivityTimeline has 18 more hardcoded English literals — the empty state was one of a set, and 12 of them already have pack keys sitting unused

2 participants

@os-warren@claude