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

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 first t() 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

group literals reachable from keys
Relative timestamps + card title 5 every activity tab 5 existing, exact
formatFieldChange sentences 6 every activity tab (entries without description) 6 new
Filter chips + group aria-label 7 published export only 2 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:

  • positive — detail.back, detail.noActivity, detail.edit: 10/10 each
  • negative — detail.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 static t() 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

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 48 chunks) 3154.9 KB 3191.4 KB
Main entry chunk (gzip) 142.6 KB 350 KB
Entry file index-BqBOec6X.js —
Status PASS —

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 15.33KB 5.59KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 5.13KB 2.35KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 512.32KB 116.52KB
core (index.js) 5.30KB 2.13KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 177.67KB 49.45KB
fields (index.js) 244.25KB 61.73KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 4.28KB 1.75KB
i18n (index.js) 3.44KB 1.39KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 26.89KB 9.04KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 33.40KB 8.71KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.98KB 10.98KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.55KB 0.62KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useResponsiveConfig.js) 1.37KB 0.63KB
mobile (useSpecGesture.js) 4.32KB 1.64KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 11.71KB 4.29KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 5.12KB 1.74KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 15.75KB 3.80KB
plugin-calendar (index.js) 46.92KB 12.93KB
plugin-charts (index.js) 65.94KB 18.64KB
plugin-chatbot (index.js) 190.53KB 45.18KB
plugin-dashboard (index.js) 132.63KB 34.56KB
plugin-designer (index.js) 212.87KB 43.19KB
plugin-detail (index.js) 249.91KB 63.82KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 133.11KB 32.61KB
plugin-gantt (index.js) 165.21KB 40.37KB
plugin-grid (index.js) 205.53KB 55.50KB
plugin-kanban (index.js) 53.21KB 14.66KB
plugin-list (index.js) 113.21KB 27.60KB
plugin-map (index.js) 20.20KB 6.66KB
plugin-markdown (index.js) 13.72KB 4.69KB
plugin-report (index.js) 43.51KB 11.94KB
plugin-timeline (index.js) 29.34KB 8.47KB
plugin-tree (index.js) 8.98KB 3.08KB
plugin-view (index.js) 85.90KB 21.12KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 81.07KB 26.86KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 3.11KB 1.48KB
react (schema-input.js) 2.32KB 1.24KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 5.41KB 2.34KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 4.93KB 2.24KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 20.57KB 5.88KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 10.35KB 3.60KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 0.99KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.74KB 1.41KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.72KB 2.24KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 2.59KB 1.31KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 3.40KB 1.71KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

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

Copy link
Copy Markdown
Collaborator Author

✅ 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-review 21 → 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 queue Sep 1, 2026
Merged via the queue into main with commit d8ec8d6 Sep 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 free to join this conversation on GitHub. Already have an account? Sign in to comment

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