fix(plugin-detail): ActivityTimeline's empty state resolves from the locale packs - #7150

Merged
os-warren merged 1 commit into
mainfrom
claude/issue-7142-activity-timeline-empty-i18n
Sep 1, 2026
Merged

fix(plugin-detail): ActivityTimeline's empty state resolves from the locale packs#7150
os-warren merged 1 commit into
mainfrom
claude/issue-7142-activity-timeline-empty-i18n

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#7142

ActivityTimeline's empty-state title was a raw English JSX literal — title="No activity recorded", not a t() call and not an inline defaultValue — so it never reached the pack system and stayed English in all ten locales. It now reads detail.noActivity through useDetailTranslation, the hook its sibling already uses.

Three lines of production change: the hook import, the const { t } = useDetailTranslation(); call, and the title expression. No pack was edited.

The key-reuse decision was measured, not assumed

The card and the dispatch both flagged this as the thing to settle first: RecordActivityTimeline.tsx:415 already renders title={emptyLabel ?? t('detail.noActivity')}, but that proves a key exists, not that the two surfaces want the same words. So both were rendered empty and read.

[en] ActivityTimeline => "Activity(0)No activity recorded"
[en] RecordActivityTimeline => "Activity(0)All ActivityNo activity recorded"
[zh] ActivityTimeline => "Activity(0)No activity recorded" <- the defect
[zh] RecordActivityTimeline => "活动(0)全部动态暂无活动记录"

The en pack value for detail.noActivity is 'No activity recorded'byte-identical to the literal being replaced. The two surfaces were already saying exactly the same words in English, so reuse changes no copy in any locale, while a second key would fork one sentence across ten packs for zero copy difference. Reuse it is.

The key is genuinely translated in all ten packs

Read out of the pack objects directly rather than grepped — the packs are nested (detail: {noActivity:), so a dotted-key grep for detail.noActivity returns zero against a pack that defines it. Two controls ran on the same instrument: detail.noRelatedRecords and detail.loading returned present in all ten (positive), and detail.zzzNotAKeyEver returned absent in all ten (negative, proving the probe can report absence).

en "No activity recorded" zh "暂无活动记录"
ja "アクティビティの記録なし" ko "기록된 활동 없음"
de "Keine Aktivitäten aufgezeichnet" fr "Aucune activité enregistrée"
es "Sin actividad registrada" pt "Nenhuma atividade registrada"
ru "Нет зарегистрированной активности" ar "لا يوجد نشاط مسجل"

DETAIL_DEFAULT_TRANSLATIONS already carries the byte-identical English row, so the provider-less path is unchanged and defaults-maps-mirror-en-pack stays green. No inline defaultValue anywhere (#3517).

Test

packages/plugin-detail/src/ActivityTimeline.i18n.test.tsx, six cases: zh, ar, en, no-provider, never-the-raw-key, and the second route into the box — a populated timeline filtered down to a type with no entries.

zh and ar are the load-bearing half. An en-only assertion would have been green before the fix too, since the literal and the en pack value are the same string; only a non-Latin locale discriminates a pack lookup from a hardcoded literal.

Ablation

Predicted before running: restoring the literal turns exactly the three locale-discriminating cases red and leaves the other three green. Observed Tests 3 failed | 3 passed (6) — the passing count is quoted deliberately, since a suite collapsed to zero survivors also shows red and would look stronger.

Mutation proven on disk by marker count in both directions (t('detail.noActivity') 1 to 0, title="No activity recorded" 0 to 1) and by blob hash (6bb6ce32 to 678d2e03), never by an editor's exit code. Restore proven by state — git diff HEAD, git diff --cached and git status --short all empty, and the blob back to 6bb6ce32 — never by a trap firing. No rebuild leg applies: the test imports ./ActivityTimeline relatively and the root vitest config aliases @object-ui/i18n to packages/i18n/src, so nothing in the closure resolves through dist/.

Verification

All readings below are on 0e210de56, the final commit.

Green:

  • pnpm exec vitest run packages/plugin-detail/ packages/i18n/src/__tests__/all-locales-key-parity.test.ts packages/app-shell/src/__tests__/defaults-maps-mirror-en-pack.test.tsxTest Files 123 passed (123), Tests 1157 passed (1157)
  • check:i18n-keys — "Every in-scope call-site key resolves against the en pack (2845 keys) … no call site carries a literal fallback beside itself" (2604/2604 literal keys resolve)
  • check:i18n-drift — "No en value changed in this range."
  • check:i18n-dead-keys — report, not a gate; detail.noActivity gains a call site here rather than losing one
  • check-changeset-presence — "1 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)"
  • check-changeset-no-major — "No changeset declares a major bump." (quoted rather than predicted)
  • type-check (tsc --noEmit && tsc -p tsconfig.test.json) — exit 0. Confirmed it actually covers the diff: --listFiles puts ActivityTimeline.tsx in the main pass and ActivityTimeline.i18n.test.tsx in the tsconfig.test.json pass, with a present-file control and a nonexistent-path control both behaving.
  • eslint . in packages/plugin-detail — 0 errors. The one warning on ActivityTimeline.tsx (Filter imported unused, line 11) is pre-existing on origin/main and untouched here.
  • check:control-bytes, check:self-import, check:esm-specifiers, check:phantom-deps, check:vi-mock-specifiers, check:vi-mock-inherit, check:dist-completeness, check:side-effects-array

NOT MEASURED — recorded as the absence of a reading, not as passes:

  • check:sdui-registration-pins — exit 2, "a run with nothing to read has measured nothing. Build the console first". The console build needs 28 unbuilt packages; that full-monorepo build is CI's, and this diff registers no SDUI component.
  • check:eager-closure — exit 2, "a broken gauge, not a sensitive gate": no apps/console/dist/eager-closure.json locally. Same cause.
  • check:readme-exports — the gate declares its own run void ("the population COLLAPSED — this run proves nothing") while packages are unbuilt. Narrowed and declared: after building @object-ui/plugin-detail, its README dropped out of the unjudged list (1 hit to 0), so this package's slice is judged; the residual collapse is the other 24 unbuilt packages.

type-check first came back exit 2 with TS2307 across every @object-ui/* — an unbuilt dependency closure, which is a non-reading rather than a red. The closure was built (pnpm --filter '@object-ui/plugin-detail^...' build) and it was re-run to get the exit 0 above.

Scope

Only the empty-state title, per the dispatch ruling. Sweeping the file found the component uses no translation hook at all on origin/main — 18 further hardcoded English literals (relative timestamps, the six filter chips, the Activity card title, the filter aria-label, the six activity-description sentences), 12 of which already have pack keys sitting unused. That inventory is filed as #7149, unassigned, rather than folded in here.

No public export added, no accept/reject behaviour changed — contract clause 2 stays disengaged. No role was added, removed or aligned on the DataEmptyState call site; that is #7132's decision and it is already made.


Generated by Claude Code

…locale packs
The empty-state title was a raw English JSX literal —
`title="No activity recorded"`, not a `t()` call and not an inline
`defaultValue` — so it never reached the pack system and stayed English in all
ten locales. Measured by rendering `activities={[]}` under a zh `I18nProvider`:
the card read "Activity(0)No activity recorded" while its sibling
`RecordActivityTimeline` rendered "活动(0)全部动态暂无活动记录" from the same
packs.
The call site now reads `detail.noActivity` through `useDetailTranslation`, the
key the sibling already uses. Reusing it rather than minting a second key is a
measured decision, not an assumption: the `en` pack value for that key is
'No activity recorded', byte-identical to the literal it replaces, so both
surfaces were already saying the same words in English and a second key would
have forked one sentence across ten packs for no copy difference. The key is
already translated in all ten packs (read out of each pack object, with a
present-key control and an absent-key control), so no pack is edited here, and
`DETAIL_DEFAULT_TRANSLATIONS` already carries the byte-identical English row
that serves a provider-less host.
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3152.5 KB3191.4 KB
Main entry chunk (gzip)142.5 KB350 KB
Entry fileindex-utltXYJm.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.30KB116.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.67KB18.57KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)132.61KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)248.95KB63.55KB
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)202.31KB54.66KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.19KB27.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 and armed

All 30 check runs in one call at perPage=100: 27 success, 3 skipped, 0 failed, 0 in_progress. Undrafted, auto-merge SQUASH enabled at 0e210de56.

You verified the thing I got wrong, instead of taking my word

I told you to determine which i18n system this file belongs to rather than trust me — because I gave the wrong answer on objectui#7121, where engine.* strings turned out to live in a two-locale en/zh table outside the pack system, and following me would have added nine dead entries.

You went to the code: plugin-detail uses the ten-pack system (en/zh/ja/ko/de/fr/es/pt/ru/ar) reached through useDetailTranslation.ts:9 (createSafeTranslation over @object-ui/i18n), with DETAIL_DEFAULT_TRANSLATIONS as the provider-less mirror — not the metadata-admin table. Naming the file you verified it in is what makes that checkable rather than another assertion.

The pack probe avoided the trap it needed to

⛔ A dotted-key grep for detail.noActivity returns zero against the en pack that defines it, because the packs are nested. You read the key out of each pack object via tsx instead, with two controls: positive (detail.noRelatedRecords + detail.loading present in all ten) and negative (detail.zzzNotAKeyEver absent from all ten). That pairing is what makes "present in all ten" a reading rather than a hope — and all ten carry real translations, not English fallbacks.

This seat has been bitten by exactly that false zero before. Good.

Ruling 2 settled by rendering, not by assuming

The en value for detail.noActivity is byte-identical to the literal being replaced. So both surfaces already said the same words, and minting a second key would have forked one sentence across ten packs for zero copy difference. That is the right call, and it is only knowable by looking at both surfaces rather than reasoning that "two components might want different phrasing."

Ablation 3 failed | 3 passed (6), predicted before running — zh, ar and filtered-zh red, en / no-provider / raw-key green. The passing count is stated because a collapse to zero survivors also shows red. No rebuild leg claimed and that was established, not assumed: the test imports relatively and the root config aliases @object-ui/i18n to packages/i18n/src, so nothing resolves through dist/.

Three NOT MEASURED, named rather than counted

check:sdui-registration-pins and check:eager-closure both exit 2 on an unbuilt console — the absence of a reading, not a pass. The third is the interesting one: check:readme-exportsdeclares its own run void while packages are unbuilt, and rather than leave it there you built @object-ui/plugin-detail and showed its README left the unjudged list (1 → 0), so this package's slice is judged and the residual collapse belongs to the other 24. That is a partial reading reported honestly instead of being rounded to either green or red.

⚠️ What my scoping decision actually shipped

I fenced this card to the empty state and you held the fence, filing objectui#7149 rather than widening. Correct — and worth stating plainly: this ships one translated string into a component that is otherwise entirely untranslated. A zh card now reads "Activity(0)暂无活动记录" — empty state right, card title beside it still English. An improvement, and visibly half-done.

The deeper finding is not "18 more literals." It is that the component used no translation hook at all, while its sibling renders the identical surface fully in Chinese. objectui#7149 unblocks when this lands.


Generated by Claude Code

Merged via the queue into main with commit 854cba3Sep 1, 2026
32 checks passed
@os-warren
os-warren deleted the claude/issue-7142-activity-timeline-empty-i18n branch September 1, 2026 07:04
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's empty state is a hardcoded English literal, so a zh session reads "No activity recorded"

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): ActivityTimeline's empty state resolves from the locale packs - #7150

Merged
os-warren merged 1 commit into
mainfrom
claude/issue-7142-activity-timeline-empty-i18n
Sep 1, 2026
Merged

fix(plugin-detail): ActivityTimeline's empty state resolves from the locale packs#7150
os-warren merged 1 commit into
mainfrom
claude/issue-7142-activity-timeline-empty-i18n

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#7142

ActivityTimeline's empty-state title was a raw English JSX literal — title="No activity recorded", not a t() call and not an inline defaultValue — so it never reached the pack system and stayed English in all ten locales. It now reads detail.noActivity through useDetailTranslation, the hook its sibling already uses.

Three lines of production change: the hook import, the const { t } = useDetailTranslation(); call, and the title expression. No pack was edited.

The key-reuse decision was measured, not assumed

The card and the dispatch both flagged this as the thing to settle first: RecordActivityTimeline.tsx:415 already renders title={emptyLabel ?? t('detail.noActivity')}, but that proves a key exists, not that the two surfaces want the same words. So both were rendered empty and read.

[en] ActivityTimeline => "Activity(0)No activity recorded"
[en] RecordActivityTimeline => "Activity(0)All ActivityNo activity recorded"
[zh] ActivityTimeline => "Activity(0)No activity recorded" <- the defect
[zh] RecordActivityTimeline => "活动(0)全部动态暂无活动记录"

The en pack value for detail.noActivity is 'No activity recorded'byte-identical to the literal being replaced. The two surfaces were already saying exactly the same words in English, so reuse changes no copy in any locale, while a second key would fork one sentence across ten packs for zero copy difference. Reuse it is.

The key is genuinely translated in all ten packs

Read out of the pack objects directly rather than grepped — the packs are nested (detail: {noActivity:), so a dotted-key grep for detail.noActivity returns zero against a pack that defines it. Two controls ran on the same instrument: detail.noRelatedRecords and detail.loading returned present in all ten (positive), and detail.zzzNotAKeyEver returned absent in all ten (negative, proving the probe can report absence).

en "No activity recorded" zh "暂无活动记录"
ja "アクティビティの記録なし" ko "기록된 활동 없음"
de "Keine Aktivitäten aufgezeichnet" fr "Aucune activité enregistrée"
es "Sin actividad registrada" pt "Nenhuma atividade registrada"
ru "Нет зарегистрированной активности" ar "لا يوجد نشاط مسجل"

DETAIL_DEFAULT_TRANSLATIONS already carries the byte-identical English row, so the provider-less path is unchanged and defaults-maps-mirror-en-pack stays green. No inline defaultValue anywhere (#3517).

Test

packages/plugin-detail/src/ActivityTimeline.i18n.test.tsx, six cases: zh, ar, en, no-provider, never-the-raw-key, and the second route into the box — a populated timeline filtered down to a type with no entries.

zh and ar are the load-bearing half. An en-only assertion would have been green before the fix too, since the literal and the en pack value are the same string; only a non-Latin locale discriminates a pack lookup from a hardcoded literal.

Ablation

Predicted before running: restoring the literal turns exactly the three locale-discriminating cases red and leaves the other three green. Observed Tests 3 failed | 3 passed (6) — the passing count is quoted deliberately, since a suite collapsed to zero survivors also shows red and would look stronger.

Mutation proven on disk by marker count in both directions (t('detail.noActivity') 1 to 0, title="No activity recorded" 0 to 1) and by blob hash (6bb6ce32 to 678d2e03), never by an editor's exit code. Restore proven by state — git diff HEAD, git diff --cached and git status --short all empty, and the blob back to 6bb6ce32 — never by a trap firing. No rebuild leg applies: the test imports ./ActivityTimeline relatively and the root vitest config aliases @object-ui/i18n to packages/i18n/src, so nothing in the closure resolves through dist/.

Verification

All readings below are on 0e210de56, the final commit.

Green:

  • pnpm exec vitest run packages/plugin-detail/ packages/i18n/src/__tests__/all-locales-key-parity.test.ts packages/app-shell/src/__tests__/defaults-maps-mirror-en-pack.test.tsxTest Files 123 passed (123), Tests 1157 passed (1157)
  • check:i18n-keys — "Every in-scope call-site key resolves against the en pack (2845 keys) … no call site carries a literal fallback beside itself" (2604/2604 literal keys resolve)
  • check:i18n-drift — "No en value changed in this range."
  • check:i18n-dead-keys — report, not a gate; detail.noActivity gains a call site here rather than losing one
  • check-changeset-presence — "1 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)"
  • check-changeset-no-major — "No changeset declares a major bump." (quoted rather than predicted)
  • type-check (tsc --noEmit && tsc -p tsconfig.test.json) — exit 0. Confirmed it actually covers the diff: --listFiles puts ActivityTimeline.tsx in the main pass and ActivityTimeline.i18n.test.tsx in the tsconfig.test.json pass, with a present-file control and a nonexistent-path control both behaving.
  • eslint . in packages/plugin-detail — 0 errors. The one warning on ActivityTimeline.tsx (Filter imported unused, line 11) is pre-existing on origin/main and untouched here.
  • check:control-bytes, check:self-import, check:esm-specifiers, check:phantom-deps, check:vi-mock-specifiers, check:vi-mock-inherit, check:dist-completeness, check:side-effects-array

NOT MEASURED — recorded as the absence of a reading, not as passes:

  • check:sdui-registration-pins — exit 2, "a run with nothing to read has measured nothing. Build the console first". The console build needs 28 unbuilt packages; that full-monorepo build is CI's, and this diff registers no SDUI component.
  • check:eager-closure — exit 2, "a broken gauge, not a sensitive gate": no apps/console/dist/eager-closure.json locally. Same cause.
  • check:readme-exports — the gate declares its own run void ("the population COLLAPSED — this run proves nothing") while packages are unbuilt. Narrowed and declared: after building @object-ui/plugin-detail, its README dropped out of the unjudged list (1 hit to 0), so this package's slice is judged; the residual collapse is the other 24 unbuilt packages.

type-check first came back exit 2 with TS2307 across every @object-ui/* — an unbuilt dependency closure, which is a non-reading rather than a red. The closure was built (pnpm --filter '@object-ui/plugin-detail^...' build) and it was re-run to get the exit 0 above.

Scope

Only the empty-state title, per the dispatch ruling. Sweeping the file found the component uses no translation hook at all on origin/main — 18 further hardcoded English literals (relative timestamps, the six filter chips, the Activity card title, the filter aria-label, the six activity-description sentences), 12 of which already have pack keys sitting unused. That inventory is filed as #7149, unassigned, rather than folded in here.

No public export added, no accept/reject behaviour changed — contract clause 2 stays disengaged. No role was added, removed or aligned on the DataEmptyState call site; that is #7132's decision and it is already made.


Generated by Claude Code

…locale packs
The empty-state title was a raw English JSX literal —
`title="No activity recorded"`, not a `t()` call and not an inline
`defaultValue` — so it never reached the pack system and stayed English in all
ten locales. Measured by rendering `activities={[]}` under a zh `I18nProvider`:
the card read "Activity(0)No activity recorded" while its sibling
`RecordActivityTimeline` rendered "活动(0)全部动态暂无活动记录" from the same
packs.
The call site now reads `detail.noActivity` through `useDetailTranslation`, the
key the sibling already uses. Reusing it rather than minting a second key is a
measured decision, not an assumption: the `en` pack value for that key is
'No activity recorded', byte-identical to the literal it replaces, so both
surfaces were already saying the same words in English and a second key would
have forked one sentence across ten packs for no copy difference. The key is
already translated in all ten packs (read out of each pack object, with a
present-key control and an absent-key control), so no pack is edited here, and
`DETAIL_DEFAULT_TRANSLATIONS` already carries the byte-identical English row
that serves a provider-less host.
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3152.5 KB3191.4 KB
Main entry chunk (gzip)142.5 KB350 KB
Entry fileindex-utltXYJm.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.30KB116.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.67KB18.57KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)132.61KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)248.95KB63.55KB
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)202.31KB54.66KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.19KB27.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 and armed

All 30 check runs in one call at perPage=100: 27 success, 3 skipped, 0 failed, 0 in_progress. Undrafted, auto-merge SQUASH enabled at 0e210de56.

You verified the thing I got wrong, instead of taking my word

I told you to determine which i18n system this file belongs to rather than trust me — because I gave the wrong answer on objectui#7121, where engine.* strings turned out to live in a two-locale en/zh table outside the pack system, and following me would have added nine dead entries.

You went to the code: plugin-detail uses the ten-pack system (en/zh/ja/ko/de/fr/es/pt/ru/ar) reached through useDetailTranslation.ts:9 (createSafeTranslation over @object-ui/i18n), with DETAIL_DEFAULT_TRANSLATIONS as the provider-less mirror — not the metadata-admin table. Naming the file you verified it in is what makes that checkable rather than another assertion.

The pack probe avoided the trap it needed to

⛔ A dotted-key grep for detail.noActivity returns zero against the en pack that defines it, because the packs are nested. You read the key out of each pack object via tsx instead, with two controls: positive (detail.noRelatedRecords + detail.loading present in all ten) and negative (detail.zzzNotAKeyEver absent from all ten). That pairing is what makes "present in all ten" a reading rather than a hope — and all ten carry real translations, not English fallbacks.

This seat has been bitten by exactly that false zero before. Good.

Ruling 2 settled by rendering, not by assuming

The en value for detail.noActivity is byte-identical to the literal being replaced. So both surfaces already said the same words, and minting a second key would have forked one sentence across ten packs for zero copy difference. That is the right call, and it is only knowable by looking at both surfaces rather than reasoning that "two components might want different phrasing."

Ablation 3 failed | 3 passed (6), predicted before running — zh, ar and filtered-zh red, en / no-provider / raw-key green. The passing count is stated because a collapse to zero survivors also shows red. No rebuild leg claimed and that was established, not assumed: the test imports relatively and the root config aliases @object-ui/i18n to packages/i18n/src, so nothing resolves through dist/.

Three NOT MEASURED, named rather than counted

check:sdui-registration-pins and check:eager-closure both exit 2 on an unbuilt console — the absence of a reading, not a pass. The third is the interesting one: check:readme-exportsdeclares its own run void while packages are unbuilt, and rather than leave it there you built @object-ui/plugin-detail and showed its README left the unjudged list (1 → 0), so this package's slice is judged and the residual collapse belongs to the other 24. That is a partial reading reported honestly instead of being rounded to either green or red.

⚠️ What my scoping decision actually shipped

I fenced this card to the empty state and you held the fence, filing objectui#7149 rather than widening. Correct — and worth stating plainly: this ships one translated string into a component that is otherwise entirely untranslated. A zh card now reads "Activity(0)暂无活动记录" — empty state right, card title beside it still English. An improvement, and visibly half-done.

The deeper finding is not "18 more literals." It is that the component used no translation hook at all, while its sibling renders the identical surface fully in Chinese. objectui#7149 unblocks when this lands.


Generated by Claude Code

Merged via the queue into main with commit 854cba3Sep 1, 2026
32 checks passed
@os-warren
os-warren deleted the claude/issue-7142-activity-timeline-empty-i18n branch September 1, 2026 07:04
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's empty state is a hardcoded English literal, so a zh session reads "No activity recorded"

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): ActivityTimeline's empty state resolves from the locale packs - #7150

Merged
os-warren merged 1 commit into
mainfrom
claude/issue-7142-activity-timeline-empty-i18n
Sep 1, 2026
Merged

fix(plugin-detail): ActivityTimeline's empty state resolves from the locale packs#7150
os-warren merged 1 commit into
mainfrom
claude/issue-7142-activity-timeline-empty-i18n

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#7142

ActivityTimeline's empty-state title was a raw English JSX literal — title="No activity recorded", not a t() call and not an inline defaultValue — so it never reached the pack system and stayed English in all ten locales. It now reads detail.noActivity through useDetailTranslation, the hook its sibling already uses.

Three lines of production change: the hook import, the const { t } = useDetailTranslation(); call, and the title expression. No pack was edited.

The key-reuse decision was measured, not assumed

The card and the dispatch both flagged this as the thing to settle first: RecordActivityTimeline.tsx:415 already renders title={emptyLabel ?? t('detail.noActivity')}, but that proves a key exists, not that the two surfaces want the same words. So both were rendered empty and read.

[en] ActivityTimeline => "Activity(0)No activity recorded"
[en] RecordActivityTimeline => "Activity(0)All ActivityNo activity recorded"
[zh] ActivityTimeline => "Activity(0)No activity recorded" <- the defect
[zh] RecordActivityTimeline => "活动(0)全部动态暂无活动记录"

The en pack value for detail.noActivity is 'No activity recorded'byte-identical to the literal being replaced. The two surfaces were already saying exactly the same words in English, so reuse changes no copy in any locale, while a second key would fork one sentence across ten packs for zero copy difference. Reuse it is.

The key is genuinely translated in all ten packs

Read out of the pack objects directly rather than grepped — the packs are nested (detail: {noActivity:), so a dotted-key grep for detail.noActivity returns zero against a pack that defines it. Two controls ran on the same instrument: detail.noRelatedRecords and detail.loading returned present in all ten (positive), and detail.zzzNotAKeyEver returned absent in all ten (negative, proving the probe can report absence).

en "No activity recorded" zh "暂无活动记录"
ja "アクティビティの記録なし" ko "기록된 활동 없음"
de "Keine Aktivitäten aufgezeichnet" fr "Aucune activité enregistrée"
es "Sin actividad registrada" pt "Nenhuma atividade registrada"
ru "Нет зарегистрированной активности" ar "لا يوجد نشاط مسجل"

DETAIL_DEFAULT_TRANSLATIONS already carries the byte-identical English row, so the provider-less path is unchanged and defaults-maps-mirror-en-pack stays green. No inline defaultValue anywhere (#3517).

Test

packages/plugin-detail/src/ActivityTimeline.i18n.test.tsx, six cases: zh, ar, en, no-provider, never-the-raw-key, and the second route into the box — a populated timeline filtered down to a type with no entries.

zh and ar are the load-bearing half. An en-only assertion would have been green before the fix too, since the literal and the en pack value are the same string; only a non-Latin locale discriminates a pack lookup from a hardcoded literal.

Ablation

Predicted before running: restoring the literal turns exactly the three locale-discriminating cases red and leaves the other three green. Observed Tests 3 failed | 3 passed (6) — the passing count is quoted deliberately, since a suite collapsed to zero survivors also shows red and would look stronger.

Mutation proven on disk by marker count in both directions (t('detail.noActivity') 1 to 0, title="No activity recorded" 0 to 1) and by blob hash (6bb6ce32 to 678d2e03), never by an editor's exit code. Restore proven by state — git diff HEAD, git diff --cached and git status --short all empty, and the blob back to 6bb6ce32 — never by a trap firing. No rebuild leg applies: the test imports ./ActivityTimeline relatively and the root vitest config aliases @object-ui/i18n to packages/i18n/src, so nothing in the closure resolves through dist/.

Verification

All readings below are on 0e210de56, the final commit.

Green:

  • pnpm exec vitest run packages/plugin-detail/ packages/i18n/src/__tests__/all-locales-key-parity.test.ts packages/app-shell/src/__tests__/defaults-maps-mirror-en-pack.test.tsxTest Files 123 passed (123), Tests 1157 passed (1157)
  • check:i18n-keys — "Every in-scope call-site key resolves against the en pack (2845 keys) … no call site carries a literal fallback beside itself" (2604/2604 literal keys resolve)
  • check:i18n-drift — "No en value changed in this range."
  • check:i18n-dead-keys — report, not a gate; detail.noActivity gains a call site here rather than losing one
  • check-changeset-presence — "1 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)"
  • check-changeset-no-major — "No changeset declares a major bump." (quoted rather than predicted)
  • type-check (tsc --noEmit && tsc -p tsconfig.test.json) — exit 0. Confirmed it actually covers the diff: --listFiles puts ActivityTimeline.tsx in the main pass and ActivityTimeline.i18n.test.tsx in the tsconfig.test.json pass, with a present-file control and a nonexistent-path control both behaving.
  • eslint . in packages/plugin-detail — 0 errors. The one warning on ActivityTimeline.tsx (Filter imported unused, line 11) is pre-existing on origin/main and untouched here.
  • check:control-bytes, check:self-import, check:esm-specifiers, check:phantom-deps, check:vi-mock-specifiers, check:vi-mock-inherit, check:dist-completeness, check:side-effects-array

NOT MEASURED — recorded as the absence of a reading, not as passes:

  • check:sdui-registration-pins — exit 2, "a run with nothing to read has measured nothing. Build the console first". The console build needs 28 unbuilt packages; that full-monorepo build is CI's, and this diff registers no SDUI component.
  • check:eager-closure — exit 2, "a broken gauge, not a sensitive gate": no apps/console/dist/eager-closure.json locally. Same cause.
  • check:readme-exports — the gate declares its own run void ("the population COLLAPSED — this run proves nothing") while packages are unbuilt. Narrowed and declared: after building @object-ui/plugin-detail, its README dropped out of the unjudged list (1 hit to 0), so this package's slice is judged; the residual collapse is the other 24 unbuilt packages.

type-check first came back exit 2 with TS2307 across every @object-ui/* — an unbuilt dependency closure, which is a non-reading rather than a red. The closure was built (pnpm --filter '@object-ui/plugin-detail^...' build) and it was re-run to get the exit 0 above.

Scope

Only the empty-state title, per the dispatch ruling. Sweeping the file found the component uses no translation hook at all on origin/main — 18 further hardcoded English literals (relative timestamps, the six filter chips, the Activity card title, the filter aria-label, the six activity-description sentences), 12 of which already have pack keys sitting unused. That inventory is filed as #7149, unassigned, rather than folded in here.

No public export added, no accept/reject behaviour changed — contract clause 2 stays disengaged. No role was added, removed or aligned on the DataEmptyState call site; that is #7132's decision and it is already made.


Generated by Claude Code

…locale packs
The empty-state title was a raw English JSX literal —
`title="No activity recorded"`, not a `t()` call and not an inline
`defaultValue` — so it never reached the pack system and stayed English in all
ten locales. Measured by rendering `activities={[]}` under a zh `I18nProvider`:
the card read "Activity(0)No activity recorded" while its sibling
`RecordActivityTimeline` rendered "活动(0)全部动态暂无活动记录" from the same
packs.
The call site now reads `detail.noActivity` through `useDetailTranslation`, the
key the sibling already uses. Reusing it rather than minting a second key is a
measured decision, not an assumption: the `en` pack value for that key is
'No activity recorded', byte-identical to the literal it replaces, so both
surfaces were already saying the same words in English and a second key would
have forked one sentence across ten packs for no copy difference. The key is
already translated in all ten packs (read out of each pack object, with a
present-key control and an absent-key control), so no pack is edited here, and
`DETAIL_DEFAULT_TRANSLATIONS` already carries the byte-identical English row
that serves a provider-less host.
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3152.5 KB3191.4 KB
Main entry chunk (gzip)142.5 KB350 KB
Entry fileindex-utltXYJm.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.30KB116.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.67KB18.57KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)132.61KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)248.95KB63.55KB
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)202.31KB54.66KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.19KB27.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 and armed

All 30 check runs in one call at perPage=100: 27 success, 3 skipped, 0 failed, 0 in_progress. Undrafted, auto-merge SQUASH enabled at 0e210de56.

You verified the thing I got wrong, instead of taking my word

I told you to determine which i18n system this file belongs to rather than trust me — because I gave the wrong answer on objectui#7121, where engine.* strings turned out to live in a two-locale en/zh table outside the pack system, and following me would have added nine dead entries.

You went to the code: plugin-detail uses the ten-pack system (en/zh/ja/ko/de/fr/es/pt/ru/ar) reached through useDetailTranslation.ts:9 (createSafeTranslation over @object-ui/i18n), with DETAIL_DEFAULT_TRANSLATIONS as the provider-less mirror — not the metadata-admin table. Naming the file you verified it in is what makes that checkable rather than another assertion.

The pack probe avoided the trap it needed to

⛔ A dotted-key grep for detail.noActivity returns zero against the en pack that defines it, because the packs are nested. You read the key out of each pack object via tsx instead, with two controls: positive (detail.noRelatedRecords + detail.loading present in all ten) and negative (detail.zzzNotAKeyEver absent from all ten). That pairing is what makes "present in all ten" a reading rather than a hope — and all ten carry real translations, not English fallbacks.

This seat has been bitten by exactly that false zero before. Good.

Ruling 2 settled by rendering, not by assuming

The en value for detail.noActivity is byte-identical to the literal being replaced. So both surfaces already said the same words, and minting a second key would have forked one sentence across ten packs for zero copy difference. That is the right call, and it is only knowable by looking at both surfaces rather than reasoning that "two components might want different phrasing."

Ablation 3 failed | 3 passed (6), predicted before running — zh, ar and filtered-zh red, en / no-provider / raw-key green. The passing count is stated because a collapse to zero survivors also shows red. No rebuild leg claimed and that was established, not assumed: the test imports relatively and the root config aliases @object-ui/i18n to packages/i18n/src, so nothing resolves through dist/.

Three NOT MEASURED, named rather than counted

check:sdui-registration-pins and check:eager-closure both exit 2 on an unbuilt console — the absence of a reading, not a pass. The third is the interesting one: check:readme-exportsdeclares its own run void while packages are unbuilt, and rather than leave it there you built @object-ui/plugin-detail and showed its README left the unjudged list (1 → 0), so this package's slice is judged and the residual collapse belongs to the other 24. That is a partial reading reported honestly instead of being rounded to either green or red.

⚠️ What my scoping decision actually shipped

I fenced this card to the empty state and you held the fence, filing objectui#7149 rather than widening. Correct — and worth stating plainly: this ships one translated string into a component that is otherwise entirely untranslated. A zh card now reads "Activity(0)暂无活动记录" — empty state right, card title beside it still English. An improvement, and visibly half-done.

The deeper finding is not "18 more literals." It is that the component used no translation hook at all, while its sibling renders the identical surface fully in Chinese. objectui#7149 unblocks when this lands.


Generated by Claude Code

Merged via the queue into main with commit 854cba3Sep 1, 2026
32 checks passed
@os-warren
os-warren deleted the claude/issue-7142-activity-timeline-empty-i18n branch September 1, 2026 07:04
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's empty state is a hardcoded English literal, so a zh session reads "No activity recorded"

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): ActivityTimeline's empty state resolves from the locale packs - #7150

Merged
os-warren merged 1 commit into
mainfrom
claude/issue-7142-activity-timeline-empty-i18n
Sep 1, 2026
Merged

fix(plugin-detail): ActivityTimeline's empty state resolves from the locale packs#7150
os-warren merged 1 commit into
mainfrom
claude/issue-7142-activity-timeline-empty-i18n

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#7142

ActivityTimeline's empty-state title was a raw English JSX literal — title="No activity recorded", not a t() call and not an inline defaultValue — so it never reached the pack system and stayed English in all ten locales. It now reads detail.noActivity through useDetailTranslation, the hook its sibling already uses.

Three lines of production change: the hook import, the const { t } = useDetailTranslation(); call, and the title expression. No pack was edited.

The key-reuse decision was measured, not assumed

The card and the dispatch both flagged this as the thing to settle first: RecordActivityTimeline.tsx:415 already renders title={emptyLabel ?? t('detail.noActivity')}, but that proves a key exists, not that the two surfaces want the same words. So both were rendered empty and read.

[en] ActivityTimeline => "Activity(0)No activity recorded"
[en] RecordActivityTimeline => "Activity(0)All ActivityNo activity recorded"
[zh] ActivityTimeline => "Activity(0)No activity recorded" <- the defect
[zh] RecordActivityTimeline => "活动(0)全部动态暂无活动记录"

The en pack value for detail.noActivity is 'No activity recorded'byte-identical to the literal being replaced. The two surfaces were already saying exactly the same words in English, so reuse changes no copy in any locale, while a second key would fork one sentence across ten packs for zero copy difference. Reuse it is.

The key is genuinely translated in all ten packs

Read out of the pack objects directly rather than grepped — the packs are nested (detail: {noActivity:), so a dotted-key grep for detail.noActivity returns zero against a pack that defines it. Two controls ran on the same instrument: detail.noRelatedRecords and detail.loading returned present in all ten (positive), and detail.zzzNotAKeyEver returned absent in all ten (negative, proving the probe can report absence).

en "No activity recorded" zh "暂无活动记录"
ja "アクティビティの記録なし" ko "기록된 활동 없음"
de "Keine Aktivitäten aufgezeichnet" fr "Aucune activité enregistrée"
es "Sin actividad registrada" pt "Nenhuma atividade registrada"
ru "Нет зарегистрированной активности" ar "لا يوجد نشاط مسجل"

DETAIL_DEFAULT_TRANSLATIONS already carries the byte-identical English row, so the provider-less path is unchanged and defaults-maps-mirror-en-pack stays green. No inline defaultValue anywhere (#3517).

Test

packages/plugin-detail/src/ActivityTimeline.i18n.test.tsx, six cases: zh, ar, en, no-provider, never-the-raw-key, and the second route into the box — a populated timeline filtered down to a type with no entries.

zh and ar are the load-bearing half. An en-only assertion would have been green before the fix too, since the literal and the en pack value are the same string; only a non-Latin locale discriminates a pack lookup from a hardcoded literal.

Ablation

Predicted before running: restoring the literal turns exactly the three locale-discriminating cases red and leaves the other three green. Observed Tests 3 failed | 3 passed (6) — the passing count is quoted deliberately, since a suite collapsed to zero survivors also shows red and would look stronger.

Mutation proven on disk by marker count in both directions (t('detail.noActivity') 1 to 0, title="No activity recorded" 0 to 1) and by blob hash (6bb6ce32 to 678d2e03), never by an editor's exit code. Restore proven by state — git diff HEAD, git diff --cached and git status --short all empty, and the blob back to 6bb6ce32 — never by a trap firing. No rebuild leg applies: the test imports ./ActivityTimeline relatively and the root vitest config aliases @object-ui/i18n to packages/i18n/src, so nothing in the closure resolves through dist/.

Verification

All readings below are on 0e210de56, the final commit.

Green:

  • pnpm exec vitest run packages/plugin-detail/ packages/i18n/src/__tests__/all-locales-key-parity.test.ts packages/app-shell/src/__tests__/defaults-maps-mirror-en-pack.test.tsxTest Files 123 passed (123), Tests 1157 passed (1157)
  • check:i18n-keys — "Every in-scope call-site key resolves against the en pack (2845 keys) … no call site carries a literal fallback beside itself" (2604/2604 literal keys resolve)
  • check:i18n-drift — "No en value changed in this range."
  • check:i18n-dead-keys — report, not a gate; detail.noActivity gains a call site here rather than losing one
  • check-changeset-presence — "1 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)"
  • check-changeset-no-major — "No changeset declares a major bump." (quoted rather than predicted)
  • type-check (tsc --noEmit && tsc -p tsconfig.test.json) — exit 0. Confirmed it actually covers the diff: --listFiles puts ActivityTimeline.tsx in the main pass and ActivityTimeline.i18n.test.tsx in the tsconfig.test.json pass, with a present-file control and a nonexistent-path control both behaving.
  • eslint . in packages/plugin-detail — 0 errors. The one warning on ActivityTimeline.tsx (Filter imported unused, line 11) is pre-existing on origin/main and untouched here.
  • check:control-bytes, check:self-import, check:esm-specifiers, check:phantom-deps, check:vi-mock-specifiers, check:vi-mock-inherit, check:dist-completeness, check:side-effects-array

NOT MEASURED — recorded as the absence of a reading, not as passes:

  • check:sdui-registration-pins — exit 2, "a run with nothing to read has measured nothing. Build the console first". The console build needs 28 unbuilt packages; that full-monorepo build is CI's, and this diff registers no SDUI component.
  • check:eager-closure — exit 2, "a broken gauge, not a sensitive gate": no apps/console/dist/eager-closure.json locally. Same cause.
  • check:readme-exports — the gate declares its own run void ("the population COLLAPSED — this run proves nothing") while packages are unbuilt. Narrowed and declared: after building @object-ui/plugin-detail, its README dropped out of the unjudged list (1 hit to 0), so this package's slice is judged; the residual collapse is the other 24 unbuilt packages.

type-check first came back exit 2 with TS2307 across every @object-ui/* — an unbuilt dependency closure, which is a non-reading rather than a red. The closure was built (pnpm --filter '@object-ui/plugin-detail^...' build) and it was re-run to get the exit 0 above.

Scope

Only the empty-state title, per the dispatch ruling. Sweeping the file found the component uses no translation hook at all on origin/main — 18 further hardcoded English literals (relative timestamps, the six filter chips, the Activity card title, the filter aria-label, the six activity-description sentences), 12 of which already have pack keys sitting unused. That inventory is filed as #7149, unassigned, rather than folded in here.

No public export added, no accept/reject behaviour changed — contract clause 2 stays disengaged. No role was added, removed or aligned on the DataEmptyState call site; that is #7132's decision and it is already made.


Generated by Claude Code

…locale packs
The empty-state title was a raw English JSX literal —
`title="No activity recorded"`, not a `t()` call and not an inline
`defaultValue` — so it never reached the pack system and stayed English in all
ten locales. Measured by rendering `activities={[]}` under a zh `I18nProvider`:
the card read "Activity(0)No activity recorded" while its sibling
`RecordActivityTimeline` rendered "活动(0)全部动态暂无活动记录" from the same
packs.
The call site now reads `detail.noActivity` through `useDetailTranslation`, the
key the sibling already uses. Reusing it rather than minting a second key is a
measured decision, not an assumption: the `en` pack value for that key is
'No activity recorded', byte-identical to the literal it replaces, so both
surfaces were already saying the same words in English and a second key would
have forked one sentence across ten packs for no copy difference. The key is
already translated in all ten packs (read out of each pack object, with a
present-key control and an absent-key control), so no pack is edited here, and
`DETAIL_DEFAULT_TRANSLATIONS` already carries the byte-identical English row
that serves a provider-less host.
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3152.5 KB3191.4 KB
Main entry chunk (gzip)142.5 KB350 KB
Entry fileindex-utltXYJm.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.30KB116.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.67KB18.57KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)132.61KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)248.95KB63.55KB
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)202.31KB54.66KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.19KB27.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 and armed

All 30 check runs in one call at perPage=100: 27 success, 3 skipped, 0 failed, 0 in_progress. Undrafted, auto-merge SQUASH enabled at 0e210de56.

You verified the thing I got wrong, instead of taking my word

I told you to determine which i18n system this file belongs to rather than trust me — because I gave the wrong answer on objectui#7121, where engine.* strings turned out to live in a two-locale en/zh table outside the pack system, and following me would have added nine dead entries.

You went to the code: plugin-detail uses the ten-pack system (en/zh/ja/ko/de/fr/es/pt/ru/ar) reached through useDetailTranslation.ts:9 (createSafeTranslation over @object-ui/i18n), with DETAIL_DEFAULT_TRANSLATIONS as the provider-less mirror — not the metadata-admin table. Naming the file you verified it in is what makes that checkable rather than another assertion.

The pack probe avoided the trap it needed to

⛔ A dotted-key grep for detail.noActivity returns zero against the en pack that defines it, because the packs are nested. You read the key out of each pack object via tsx instead, with two controls: positive (detail.noRelatedRecords + detail.loading present in all ten) and negative (detail.zzzNotAKeyEver absent from all ten). That pairing is what makes "present in all ten" a reading rather than a hope — and all ten carry real translations, not English fallbacks.

This seat has been bitten by exactly that false zero before. Good.

Ruling 2 settled by rendering, not by assuming

The en value for detail.noActivity is byte-identical to the literal being replaced. So both surfaces already said the same words, and minting a second key would have forked one sentence across ten packs for zero copy difference. That is the right call, and it is only knowable by looking at both surfaces rather than reasoning that "two components might want different phrasing."

Ablation 3 failed | 3 passed (6), predicted before running — zh, ar and filtered-zh red, en / no-provider / raw-key green. The passing count is stated because a collapse to zero survivors also shows red. No rebuild leg claimed and that was established, not assumed: the test imports relatively and the root config aliases @object-ui/i18n to packages/i18n/src, so nothing resolves through dist/.

Three NOT MEASURED, named rather than counted

check:sdui-registration-pins and check:eager-closure both exit 2 on an unbuilt console — the absence of a reading, not a pass. The third is the interesting one: check:readme-exportsdeclares its own run void while packages are unbuilt, and rather than leave it there you built @object-ui/plugin-detail and showed its README left the unjudged list (1 → 0), so this package's slice is judged and the residual collapse belongs to the other 24. That is a partial reading reported honestly instead of being rounded to either green or red.

⚠️ What my scoping decision actually shipped

I fenced this card to the empty state and you held the fence, filing objectui#7149 rather than widening. Correct — and worth stating plainly: this ships one translated string into a component that is otherwise entirely untranslated. A zh card now reads "Activity(0)暂无活动记录" — empty state right, card title beside it still English. An improvement, and visibly half-done.

The deeper finding is not "18 more literals." It is that the component used no translation hook at all, while its sibling renders the identical surface fully in Chinese. objectui#7149 unblocks when this lands.


Generated by Claude Code

Merged via the queue into main with commit 854cba3Sep 1, 2026
32 checks passed
@os-warren
os-warren deleted the claude/issue-7142-activity-timeline-empty-i18n branch September 1, 2026 07:04
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's empty state is a hardcoded English literal, so a zh session reads "No activity recorded"

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): ActivityTimeline's empty state resolves from the locale packs - #7150

Merged
os-warren merged 1 commit into
mainfrom
claude/issue-7142-activity-timeline-empty-i18n
Sep 1, 2026
Merged

fix(plugin-detail): ActivityTimeline's empty state resolves from the locale packs#7150
os-warren merged 1 commit into
mainfrom
claude/issue-7142-activity-timeline-empty-i18n

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#7142

ActivityTimeline's empty-state title was a raw English JSX literal — title="No activity recorded", not a t() call and not an inline defaultValue — so it never reached the pack system and stayed English in all ten locales. It now reads detail.noActivity through useDetailTranslation, the hook its sibling already uses.

Three lines of production change: the hook import, the const { t } = useDetailTranslation(); call, and the title expression. No pack was edited.

The key-reuse decision was measured, not assumed

The card and the dispatch both flagged this as the thing to settle first: RecordActivityTimeline.tsx:415 already renders title={emptyLabel ?? t('detail.noActivity')}, but that proves a key exists, not that the two surfaces want the same words. So both were rendered empty and read.

[en] ActivityTimeline => "Activity(0)No activity recorded"
[en] RecordActivityTimeline => "Activity(0)All ActivityNo activity recorded"
[zh] ActivityTimeline => "Activity(0)No activity recorded" <- the defect
[zh] RecordActivityTimeline => "活动(0)全部动态暂无活动记录"

The en pack value for detail.noActivity is 'No activity recorded'byte-identical to the literal being replaced. The two surfaces were already saying exactly the same words in English, so reuse changes no copy in any locale, while a second key would fork one sentence across ten packs for zero copy difference. Reuse it is.

The key is genuinely translated in all ten packs

Read out of the pack objects directly rather than grepped — the packs are nested (detail: {noActivity:), so a dotted-key grep for detail.noActivity returns zero against a pack that defines it. Two controls ran on the same instrument: detail.noRelatedRecords and detail.loading returned present in all ten (positive), and detail.zzzNotAKeyEver returned absent in all ten (negative, proving the probe can report absence).

en "No activity recorded" zh "暂无活动记录"
ja "アクティビティの記録なし" ko "기록된 활동 없음"
de "Keine Aktivitäten aufgezeichnet" fr "Aucune activité enregistrée"
es "Sin actividad registrada" pt "Nenhuma atividade registrada"
ru "Нет зарегистрированной активности" ar "لا يوجد نشاط مسجل"

DETAIL_DEFAULT_TRANSLATIONS already carries the byte-identical English row, so the provider-less path is unchanged and defaults-maps-mirror-en-pack stays green. No inline defaultValue anywhere (#3517).

Test

packages/plugin-detail/src/ActivityTimeline.i18n.test.tsx, six cases: zh, ar, en, no-provider, never-the-raw-key, and the second route into the box — a populated timeline filtered down to a type with no entries.

zh and ar are the load-bearing half. An en-only assertion would have been green before the fix too, since the literal and the en pack value are the same string; only a non-Latin locale discriminates a pack lookup from a hardcoded literal.

Ablation

Predicted before running: restoring the literal turns exactly the three locale-discriminating cases red and leaves the other three green. Observed Tests 3 failed | 3 passed (6) — the passing count is quoted deliberately, since a suite collapsed to zero survivors also shows red and would look stronger.

Mutation proven on disk by marker count in both directions (t('detail.noActivity') 1 to 0, title="No activity recorded" 0 to 1) and by blob hash (6bb6ce32 to 678d2e03), never by an editor's exit code. Restore proven by state — git diff HEAD, git diff --cached and git status --short all empty, and the blob back to 6bb6ce32 — never by a trap firing. No rebuild leg applies: the test imports ./ActivityTimeline relatively and the root vitest config aliases @object-ui/i18n to packages/i18n/src, so nothing in the closure resolves through dist/.

Verification

All readings below are on 0e210de56, the final commit.

Green:

  • pnpm exec vitest run packages/plugin-detail/ packages/i18n/src/__tests__/all-locales-key-parity.test.ts packages/app-shell/src/__tests__/defaults-maps-mirror-en-pack.test.tsxTest Files 123 passed (123), Tests 1157 passed (1157)
  • check:i18n-keys — "Every in-scope call-site key resolves against the en pack (2845 keys) … no call site carries a literal fallback beside itself" (2604/2604 literal keys resolve)
  • check:i18n-drift — "No en value changed in this range."
  • check:i18n-dead-keys — report, not a gate; detail.noActivity gains a call site here rather than losing one
  • check-changeset-presence — "1 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)"
  • check-changeset-no-major — "No changeset declares a major bump." (quoted rather than predicted)
  • type-check (tsc --noEmit && tsc -p tsconfig.test.json) — exit 0. Confirmed it actually covers the diff: --listFiles puts ActivityTimeline.tsx in the main pass and ActivityTimeline.i18n.test.tsx in the tsconfig.test.json pass, with a present-file control and a nonexistent-path control both behaving.
  • eslint . in packages/plugin-detail — 0 errors. The one warning on ActivityTimeline.tsx (Filter imported unused, line 11) is pre-existing on origin/main and untouched here.
  • check:control-bytes, check:self-import, check:esm-specifiers, check:phantom-deps, check:vi-mock-specifiers, check:vi-mock-inherit, check:dist-completeness, check:side-effects-array

NOT MEASURED — recorded as the absence of a reading, not as passes:

  • check:sdui-registration-pins — exit 2, "a run with nothing to read has measured nothing. Build the console first". The console build needs 28 unbuilt packages; that full-monorepo build is CI's, and this diff registers no SDUI component.
  • check:eager-closure — exit 2, "a broken gauge, not a sensitive gate": no apps/console/dist/eager-closure.json locally. Same cause.
  • check:readme-exports — the gate declares its own run void ("the population COLLAPSED — this run proves nothing") while packages are unbuilt. Narrowed and declared: after building @object-ui/plugin-detail, its README dropped out of the unjudged list (1 hit to 0), so this package's slice is judged; the residual collapse is the other 24 unbuilt packages.

type-check first came back exit 2 with TS2307 across every @object-ui/* — an unbuilt dependency closure, which is a non-reading rather than a red. The closure was built (pnpm --filter '@object-ui/plugin-detail^...' build) and it was re-run to get the exit 0 above.

Scope

Only the empty-state title, per the dispatch ruling. Sweeping the file found the component uses no translation hook at all on origin/main — 18 further hardcoded English literals (relative timestamps, the six filter chips, the Activity card title, the filter aria-label, the six activity-description sentences), 12 of which already have pack keys sitting unused. That inventory is filed as #7149, unassigned, rather than folded in here.

No public export added, no accept/reject behaviour changed — contract clause 2 stays disengaged. No role was added, removed or aligned on the DataEmptyState call site; that is #7132's decision and it is already made.


Generated by Claude Code

…locale packs
The empty-state title was a raw English JSX literal —
`title="No activity recorded"`, not a `t()` call and not an inline
`defaultValue` — so it never reached the pack system and stayed English in all
ten locales. Measured by rendering `activities={[]}` under a zh `I18nProvider`:
the card read "Activity(0)No activity recorded" while its sibling
`RecordActivityTimeline` rendered "活动(0)全部动态暂无活动记录" from the same
packs.
The call site now reads `detail.noActivity` through `useDetailTranslation`, the
key the sibling already uses. Reusing it rather than minting a second key is a
measured decision, not an assumption: the `en` pack value for that key is
'No activity recorded', byte-identical to the literal it replaces, so both
surfaces were already saying the same words in English and a second key would
have forked one sentence across ten packs for no copy difference. The key is
already translated in all ten packs (read out of each pack object, with a
present-key control and an absent-key control), so no pack is edited here, and
`DETAIL_DEFAULT_TRANSLATIONS` already carries the byte-identical English row
that serves a provider-less host.
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3152.5 KB3191.4 KB
Main entry chunk (gzip)142.5 KB350 KB
Entry fileindex-utltXYJm.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.30KB116.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.67KB18.57KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)132.61KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)248.95KB63.55KB
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)202.31KB54.66KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.19KB27.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 and armed

All 30 check runs in one call at perPage=100: 27 success, 3 skipped, 0 failed, 0 in_progress. Undrafted, auto-merge SQUASH enabled at 0e210de56.

You verified the thing I got wrong, instead of taking my word

I told you to determine which i18n system this file belongs to rather than trust me — because I gave the wrong answer on objectui#7121, where engine.* strings turned out to live in a two-locale en/zh table outside the pack system, and following me would have added nine dead entries.

You went to the code: plugin-detail uses the ten-pack system (en/zh/ja/ko/de/fr/es/pt/ru/ar) reached through useDetailTranslation.ts:9 (createSafeTranslation over @object-ui/i18n), with DETAIL_DEFAULT_TRANSLATIONS as the provider-less mirror — not the metadata-admin table. Naming the file you verified it in is what makes that checkable rather than another assertion.

The pack probe avoided the trap it needed to

⛔ A dotted-key grep for detail.noActivity returns zero against the en pack that defines it, because the packs are nested. You read the key out of each pack object via tsx instead, with two controls: positive (detail.noRelatedRecords + detail.loading present in all ten) and negative (detail.zzzNotAKeyEver absent from all ten). That pairing is what makes "present in all ten" a reading rather than a hope — and all ten carry real translations, not English fallbacks.

This seat has been bitten by exactly that false zero before. Good.

Ruling 2 settled by rendering, not by assuming

The en value for detail.noActivity is byte-identical to the literal being replaced. So both surfaces already said the same words, and minting a second key would have forked one sentence across ten packs for zero copy difference. That is the right call, and it is only knowable by looking at both surfaces rather than reasoning that "two components might want different phrasing."

Ablation 3 failed | 3 passed (6), predicted before running — zh, ar and filtered-zh red, en / no-provider / raw-key green. The passing count is stated because a collapse to zero survivors also shows red. No rebuild leg claimed and that was established, not assumed: the test imports relatively and the root config aliases @object-ui/i18n to packages/i18n/src, so nothing resolves through dist/.

Three NOT MEASURED, named rather than counted

check:sdui-registration-pins and check:eager-closure both exit 2 on an unbuilt console — the absence of a reading, not a pass. The third is the interesting one: check:readme-exportsdeclares its own run void while packages are unbuilt, and rather than leave it there you built @object-ui/plugin-detail and showed its README left the unjudged list (1 → 0), so this package's slice is judged and the residual collapse belongs to the other 24. That is a partial reading reported honestly instead of being rounded to either green or red.

⚠️ What my scoping decision actually shipped

I fenced this card to the empty state and you held the fence, filing objectui#7149 rather than widening. Correct — and worth stating plainly: this ships one translated string into a component that is otherwise entirely untranslated. A zh card now reads "Activity(0)暂无活动记录" — empty state right, card title beside it still English. An improvement, and visibly half-done.

The deeper finding is not "18 more literals." It is that the component used no translation hook at all, while its sibling renders the identical surface fully in Chinese. objectui#7149 unblocks when this lands.


Generated by Claude Code

Merged via the queue into main with commit 854cba3Sep 1, 2026
32 checks passed
@os-warren
os-warren deleted the claude/issue-7142-activity-timeline-empty-i18n branch September 1, 2026 07:04
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's empty state is a hardcoded English literal, so a zh session reads "No activity recorded"

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): ActivityTimeline's empty state resolves from the locale packs - #7150

Merged
os-warren merged 1 commit into
mainfrom
claude/issue-7142-activity-timeline-empty-i18n
Sep 1, 2026
Merged

fix(plugin-detail): ActivityTimeline's empty state resolves from the locale packs#7150
os-warren merged 1 commit into
mainfrom
claude/issue-7142-activity-timeline-empty-i18n

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#7142

ActivityTimeline's empty-state title was a raw English JSX literal — title="No activity recorded", not a t() call and not an inline defaultValue — so it never reached the pack system and stayed English in all ten locales. It now reads detail.noActivity through useDetailTranslation, the hook its sibling already uses.

Three lines of production change: the hook import, the const { t } = useDetailTranslation(); call, and the title expression. No pack was edited.

The key-reuse decision was measured, not assumed

The card and the dispatch both flagged this as the thing to settle first: RecordActivityTimeline.tsx:415 already renders title={emptyLabel ?? t('detail.noActivity')}, but that proves a key exists, not that the two surfaces want the same words. So both were rendered empty and read.

[en] ActivityTimeline => "Activity(0)No activity recorded"
[en] RecordActivityTimeline => "Activity(0)All ActivityNo activity recorded"
[zh] ActivityTimeline => "Activity(0)No activity recorded" <- the defect
[zh] RecordActivityTimeline => "活动(0)全部动态暂无活动记录"

The en pack value for detail.noActivity is 'No activity recorded'byte-identical to the literal being replaced. The two surfaces were already saying exactly the same words in English, so reuse changes no copy in any locale, while a second key would fork one sentence across ten packs for zero copy difference. Reuse it is.

The key is genuinely translated in all ten packs

Read out of the pack objects directly rather than grepped — the packs are nested (detail: {noActivity:), so a dotted-key grep for detail.noActivity returns zero against a pack that defines it. Two controls ran on the same instrument: detail.noRelatedRecords and detail.loading returned present in all ten (positive), and detail.zzzNotAKeyEver returned absent in all ten (negative, proving the probe can report absence).

en "No activity recorded" zh "暂无活动记录"
ja "アクティビティの記録なし" ko "기록된 활동 없음"
de "Keine Aktivitäten aufgezeichnet" fr "Aucune activité enregistrée"
es "Sin actividad registrada" pt "Nenhuma atividade registrada"
ru "Нет зарегистрированной активности" ar "لا يوجد نشاط مسجل"

DETAIL_DEFAULT_TRANSLATIONS already carries the byte-identical English row, so the provider-less path is unchanged and defaults-maps-mirror-en-pack stays green. No inline defaultValue anywhere (#3517).

Test

packages/plugin-detail/src/ActivityTimeline.i18n.test.tsx, six cases: zh, ar, en, no-provider, never-the-raw-key, and the second route into the box — a populated timeline filtered down to a type with no entries.

zh and ar are the load-bearing half. An en-only assertion would have been green before the fix too, since the literal and the en pack value are the same string; only a non-Latin locale discriminates a pack lookup from a hardcoded literal.

Ablation

Predicted before running: restoring the literal turns exactly the three locale-discriminating cases red and leaves the other three green. Observed Tests 3 failed | 3 passed (6) — the passing count is quoted deliberately, since a suite collapsed to zero survivors also shows red and would look stronger.

Mutation proven on disk by marker count in both directions (t('detail.noActivity') 1 to 0, title="No activity recorded" 0 to 1) and by blob hash (6bb6ce32 to 678d2e03), never by an editor's exit code. Restore proven by state — git diff HEAD, git diff --cached and git status --short all empty, and the blob back to 6bb6ce32 — never by a trap firing. No rebuild leg applies: the test imports ./ActivityTimeline relatively and the root vitest config aliases @object-ui/i18n to packages/i18n/src, so nothing in the closure resolves through dist/.

Verification

All readings below are on 0e210de56, the final commit.

Green:

  • pnpm exec vitest run packages/plugin-detail/ packages/i18n/src/__tests__/all-locales-key-parity.test.ts packages/app-shell/src/__tests__/defaults-maps-mirror-en-pack.test.tsxTest Files 123 passed (123), Tests 1157 passed (1157)
  • check:i18n-keys — "Every in-scope call-site key resolves against the en pack (2845 keys) … no call site carries a literal fallback beside itself" (2604/2604 literal keys resolve)
  • check:i18n-drift — "No en value changed in this range."
  • check:i18n-dead-keys — report, not a gate; detail.noActivity gains a call site here rather than losing one
  • check-changeset-presence — "1 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)"
  • check-changeset-no-major — "No changeset declares a major bump." (quoted rather than predicted)
  • type-check (tsc --noEmit && tsc -p tsconfig.test.json) — exit 0. Confirmed it actually covers the diff: --listFiles puts ActivityTimeline.tsx in the main pass and ActivityTimeline.i18n.test.tsx in the tsconfig.test.json pass, with a present-file control and a nonexistent-path control both behaving.
  • eslint . in packages/plugin-detail — 0 errors. The one warning on ActivityTimeline.tsx (Filter imported unused, line 11) is pre-existing on origin/main and untouched here.
  • check:control-bytes, check:self-import, check:esm-specifiers, check:phantom-deps, check:vi-mock-specifiers, check:vi-mock-inherit, check:dist-completeness, check:side-effects-array

NOT MEASURED — recorded as the absence of a reading, not as passes:

  • check:sdui-registration-pins — exit 2, "a run with nothing to read has measured nothing. Build the console first". The console build needs 28 unbuilt packages; that full-monorepo build is CI's, and this diff registers no SDUI component.
  • check:eager-closure — exit 2, "a broken gauge, not a sensitive gate": no apps/console/dist/eager-closure.json locally. Same cause.
  • check:readme-exports — the gate declares its own run void ("the population COLLAPSED — this run proves nothing") while packages are unbuilt. Narrowed and declared: after building @object-ui/plugin-detail, its README dropped out of the unjudged list (1 hit to 0), so this package's slice is judged; the residual collapse is the other 24 unbuilt packages.

type-check first came back exit 2 with TS2307 across every @object-ui/* — an unbuilt dependency closure, which is a non-reading rather than a red. The closure was built (pnpm --filter '@object-ui/plugin-detail^...' build) and it was re-run to get the exit 0 above.

Scope

Only the empty-state title, per the dispatch ruling. Sweeping the file found the component uses no translation hook at all on origin/main — 18 further hardcoded English literals (relative timestamps, the six filter chips, the Activity card title, the filter aria-label, the six activity-description sentences), 12 of which already have pack keys sitting unused. That inventory is filed as #7149, unassigned, rather than folded in here.

No public export added, no accept/reject behaviour changed — contract clause 2 stays disengaged. No role was added, removed or aligned on the DataEmptyState call site; that is #7132's decision and it is already made.


Generated by Claude Code

…locale packs
The empty-state title was a raw English JSX literal —
`title="No activity recorded"`, not a `t()` call and not an inline
`defaultValue` — so it never reached the pack system and stayed English in all
ten locales. Measured by rendering `activities={[]}` under a zh `I18nProvider`:
the card read "Activity(0)No activity recorded" while its sibling
`RecordActivityTimeline` rendered "活动(0)全部动态暂无活动记录" from the same
packs.
The call site now reads `detail.noActivity` through `useDetailTranslation`, the
key the sibling already uses. Reusing it rather than minting a second key is a
measured decision, not an assumption: the `en` pack value for that key is
'No activity recorded', byte-identical to the literal it replaces, so both
surfaces were already saying the same words in English and a second key would
have forked one sentence across ten packs for no copy difference. The key is
already translated in all ten packs (read out of each pack object, with a
present-key control and an absent-key control), so no pack is edited here, and
`DETAIL_DEFAULT_TRANSLATIONS` already carries the byte-identical English row
that serves a provider-less host.
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3152.5 KB3191.4 KB
Main entry chunk (gzip)142.5 KB350 KB
Entry fileindex-utltXYJm.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.30KB116.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.67KB18.57KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)132.61KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)248.95KB63.55KB
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)202.31KB54.66KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.19KB27.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 and armed

All 30 check runs in one call at perPage=100: 27 success, 3 skipped, 0 failed, 0 in_progress. Undrafted, auto-merge SQUASH enabled at 0e210de56.

You verified the thing I got wrong, instead of taking my word

I told you to determine which i18n system this file belongs to rather than trust me — because I gave the wrong answer on objectui#7121, where engine.* strings turned out to live in a two-locale en/zh table outside the pack system, and following me would have added nine dead entries.

You went to the code: plugin-detail uses the ten-pack system (en/zh/ja/ko/de/fr/es/pt/ru/ar) reached through useDetailTranslation.ts:9 (createSafeTranslation over @object-ui/i18n), with DETAIL_DEFAULT_TRANSLATIONS as the provider-less mirror — not the metadata-admin table. Naming the file you verified it in is what makes that checkable rather than another assertion.

The pack probe avoided the trap it needed to

⛔ A dotted-key grep for detail.noActivity returns zero against the en pack that defines it, because the packs are nested. You read the key out of each pack object via tsx instead, with two controls: positive (detail.noRelatedRecords + detail.loading present in all ten) and negative (detail.zzzNotAKeyEver absent from all ten). That pairing is what makes "present in all ten" a reading rather than a hope — and all ten carry real translations, not English fallbacks.

This seat has been bitten by exactly that false zero before. Good.

Ruling 2 settled by rendering, not by assuming

The en value for detail.noActivity is byte-identical to the literal being replaced. So both surfaces already said the same words, and minting a second key would have forked one sentence across ten packs for zero copy difference. That is the right call, and it is only knowable by looking at both surfaces rather than reasoning that "two components might want different phrasing."

Ablation 3 failed | 3 passed (6), predicted before running — zh, ar and filtered-zh red, en / no-provider / raw-key green. The passing count is stated because a collapse to zero survivors also shows red. No rebuild leg claimed and that was established, not assumed: the test imports relatively and the root config aliases @object-ui/i18n to packages/i18n/src, so nothing resolves through dist/.

Three NOT MEASURED, named rather than counted

check:sdui-registration-pins and check:eager-closure both exit 2 on an unbuilt console — the absence of a reading, not a pass. The third is the interesting one: check:readme-exportsdeclares its own run void while packages are unbuilt, and rather than leave it there you built @object-ui/plugin-detail and showed its README left the unjudged list (1 → 0), so this package's slice is judged and the residual collapse belongs to the other 24. That is a partial reading reported honestly instead of being rounded to either green or red.

⚠️ What my scoping decision actually shipped

I fenced this card to the empty state and you held the fence, filing objectui#7149 rather than widening. Correct — and worth stating plainly: this ships one translated string into a component that is otherwise entirely untranslated. A zh card now reads "Activity(0)暂无活动记录" — empty state right, card title beside it still English. An improvement, and visibly half-done.

The deeper finding is not "18 more literals." It is that the component used no translation hook at all, while its sibling renders the identical surface fully in Chinese. objectui#7149 unblocks when this lands.


Generated by Claude Code

Merged via the queue into main with commit 854cba3Sep 1, 2026
32 checks passed
@os-warren
os-warren deleted the claude/issue-7142-activity-timeline-empty-i18n branch September 1, 2026 07:04
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's empty state is a hardcoded English literal, so a zh session reads "No activity recorded"

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): ActivityTimeline's empty state resolves from the locale packs - #7150

Merged
os-warren merged 1 commit into
mainfrom
claude/issue-7142-activity-timeline-empty-i18n
Sep 1, 2026
Merged

fix(plugin-detail): ActivityTimeline's empty state resolves from the locale packs#7150
os-warren merged 1 commit into
mainfrom
claude/issue-7142-activity-timeline-empty-i18n

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#7142

ActivityTimeline's empty-state title was a raw English JSX literal — title="No activity recorded", not a t() call and not an inline defaultValue — so it never reached the pack system and stayed English in all ten locales. It now reads detail.noActivity through useDetailTranslation, the hook its sibling already uses.

Three lines of production change: the hook import, the const { t } = useDetailTranslation(); call, and the title expression. No pack was edited.

The key-reuse decision was measured, not assumed

The card and the dispatch both flagged this as the thing to settle first: RecordActivityTimeline.tsx:415 already renders title={emptyLabel ?? t('detail.noActivity')}, but that proves a key exists, not that the two surfaces want the same words. So both were rendered empty and read.

[en] ActivityTimeline => "Activity(0)No activity recorded"
[en] RecordActivityTimeline => "Activity(0)All ActivityNo activity recorded"
[zh] ActivityTimeline => "Activity(0)No activity recorded" <- the defect
[zh] RecordActivityTimeline => "活动(0)全部动态暂无活动记录"

The en pack value for detail.noActivity is 'No activity recorded'byte-identical to the literal being replaced. The two surfaces were already saying exactly the same words in English, so reuse changes no copy in any locale, while a second key would fork one sentence across ten packs for zero copy difference. Reuse it is.

The key is genuinely translated in all ten packs

Read out of the pack objects directly rather than grepped — the packs are nested (detail: {noActivity:), so a dotted-key grep for detail.noActivity returns zero against a pack that defines it. Two controls ran on the same instrument: detail.noRelatedRecords and detail.loading returned present in all ten (positive), and detail.zzzNotAKeyEver returned absent in all ten (negative, proving the probe can report absence).

en "No activity recorded" zh "暂无活动记录"
ja "アクティビティの記録なし" ko "기록된 활동 없음"
de "Keine Aktivitäten aufgezeichnet" fr "Aucune activité enregistrée"
es "Sin actividad registrada" pt "Nenhuma atividade registrada"
ru "Нет зарегистрированной активности" ar "لا يوجد نشاط مسجل"

DETAIL_DEFAULT_TRANSLATIONS already carries the byte-identical English row, so the provider-less path is unchanged and defaults-maps-mirror-en-pack stays green. No inline defaultValue anywhere (#3517).

Test

packages/plugin-detail/src/ActivityTimeline.i18n.test.tsx, six cases: zh, ar, en, no-provider, never-the-raw-key, and the second route into the box — a populated timeline filtered down to a type with no entries.

zh and ar are the load-bearing half. An en-only assertion would have been green before the fix too, since the literal and the en pack value are the same string; only a non-Latin locale discriminates a pack lookup from a hardcoded literal.

Ablation

Predicted before running: restoring the literal turns exactly the three locale-discriminating cases red and leaves the other three green. Observed Tests 3 failed | 3 passed (6) — the passing count is quoted deliberately, since a suite collapsed to zero survivors also shows red and would look stronger.

Mutation proven on disk by marker count in both directions (t('detail.noActivity') 1 to 0, title="No activity recorded" 0 to 1) and by blob hash (6bb6ce32 to 678d2e03), never by an editor's exit code. Restore proven by state — git diff HEAD, git diff --cached and git status --short all empty, and the blob back to 6bb6ce32 — never by a trap firing. No rebuild leg applies: the test imports ./ActivityTimeline relatively and the root vitest config aliases @object-ui/i18n to packages/i18n/src, so nothing in the closure resolves through dist/.

Verification

All readings below are on 0e210de56, the final commit.

Green:

  • pnpm exec vitest run packages/plugin-detail/ packages/i18n/src/__tests__/all-locales-key-parity.test.ts packages/app-shell/src/__tests__/defaults-maps-mirror-en-pack.test.tsxTest Files 123 passed (123), Tests 1157 passed (1157)
  • check:i18n-keys — "Every in-scope call-site key resolves against the en pack (2845 keys) … no call site carries a literal fallback beside itself" (2604/2604 literal keys resolve)
  • check:i18n-drift — "No en value changed in this range."
  • check:i18n-dead-keys — report, not a gate; detail.noActivity gains a call site here rather than losing one
  • check-changeset-presence — "1 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)"
  • check-changeset-no-major — "No changeset declares a major bump." (quoted rather than predicted)
  • type-check (tsc --noEmit && tsc -p tsconfig.test.json) — exit 0. Confirmed it actually covers the diff: --listFiles puts ActivityTimeline.tsx in the main pass and ActivityTimeline.i18n.test.tsx in the tsconfig.test.json pass, with a present-file control and a nonexistent-path control both behaving.
  • eslint . in packages/plugin-detail — 0 errors. The one warning on ActivityTimeline.tsx (Filter imported unused, line 11) is pre-existing on origin/main and untouched here.
  • check:control-bytes, check:self-import, check:esm-specifiers, check:phantom-deps, check:vi-mock-specifiers, check:vi-mock-inherit, check:dist-completeness, check:side-effects-array

NOT MEASURED — recorded as the absence of a reading, not as passes:

  • check:sdui-registration-pins — exit 2, "a run with nothing to read has measured nothing. Build the console first". The console build needs 28 unbuilt packages; that full-monorepo build is CI's, and this diff registers no SDUI component.
  • check:eager-closure — exit 2, "a broken gauge, not a sensitive gate": no apps/console/dist/eager-closure.json locally. Same cause.
  • check:readme-exports — the gate declares its own run void ("the population COLLAPSED — this run proves nothing") while packages are unbuilt. Narrowed and declared: after building @object-ui/plugin-detail, its README dropped out of the unjudged list (1 hit to 0), so this package's slice is judged; the residual collapse is the other 24 unbuilt packages.

type-check first came back exit 2 with TS2307 across every @object-ui/* — an unbuilt dependency closure, which is a non-reading rather than a red. The closure was built (pnpm --filter '@object-ui/plugin-detail^...' build) and it was re-run to get the exit 0 above.

Scope

Only the empty-state title, per the dispatch ruling. Sweeping the file found the component uses no translation hook at all on origin/main — 18 further hardcoded English literals (relative timestamps, the six filter chips, the Activity card title, the filter aria-label, the six activity-description sentences), 12 of which already have pack keys sitting unused. That inventory is filed as #7149, unassigned, rather than folded in here.

No public export added, no accept/reject behaviour changed — contract clause 2 stays disengaged. No role was added, removed or aligned on the DataEmptyState call site; that is #7132's decision and it is already made.


Generated by Claude Code

…locale packs
The empty-state title was a raw English JSX literal —
`title="No activity recorded"`, not a `t()` call and not an inline
`defaultValue` — so it never reached the pack system and stayed English in all
ten locales. Measured by rendering `activities={[]}` under a zh `I18nProvider`:
the card read "Activity(0)No activity recorded" while its sibling
`RecordActivityTimeline` rendered "活动(0)全部动态暂无活动记录" from the same
packs.
The call site now reads `detail.noActivity` through `useDetailTranslation`, the
key the sibling already uses. Reusing it rather than minting a second key is a
measured decision, not an assumption: the `en` pack value for that key is
'No activity recorded', byte-identical to the literal it replaces, so both
surfaces were already saying the same words in English and a second key would
have forked one sentence across ten packs for no copy difference. The key is
already translated in all ten packs (read out of each pack object, with a
present-key control and an absent-key control), so no pack is edited here, and
`DETAIL_DEFAULT_TRANSLATIONS` already carries the byte-identical English row
that serves a provider-less host.
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3152.5 KB3191.4 KB
Main entry chunk (gzip)142.5 KB350 KB
Entry fileindex-utltXYJm.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.30KB116.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.67KB18.57KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)132.61KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)248.95KB63.55KB
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)202.31KB54.66KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.19KB27.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 and armed

All 30 check runs in one call at perPage=100: 27 success, 3 skipped, 0 failed, 0 in_progress. Undrafted, auto-merge SQUASH enabled at 0e210de56.

You verified the thing I got wrong, instead of taking my word

I told you to determine which i18n system this file belongs to rather than trust me — because I gave the wrong answer on objectui#7121, where engine.* strings turned out to live in a two-locale en/zh table outside the pack system, and following me would have added nine dead entries.

You went to the code: plugin-detail uses the ten-pack system (en/zh/ja/ko/de/fr/es/pt/ru/ar) reached through useDetailTranslation.ts:9 (createSafeTranslation over @object-ui/i18n), with DETAIL_DEFAULT_TRANSLATIONS as the provider-less mirror — not the metadata-admin table. Naming the file you verified it in is what makes that checkable rather than another assertion.

The pack probe avoided the trap it needed to

⛔ A dotted-key grep for detail.noActivity returns zero against the en pack that defines it, because the packs are nested. You read the key out of each pack object via tsx instead, with two controls: positive (detail.noRelatedRecords + detail.loading present in all ten) and negative (detail.zzzNotAKeyEver absent from all ten). That pairing is what makes "present in all ten" a reading rather than a hope — and all ten carry real translations, not English fallbacks.

This seat has been bitten by exactly that false zero before. Good.

Ruling 2 settled by rendering, not by assuming

The en value for detail.noActivity is byte-identical to the literal being replaced. So both surfaces already said the same words, and minting a second key would have forked one sentence across ten packs for zero copy difference. That is the right call, and it is only knowable by looking at both surfaces rather than reasoning that "two components might want different phrasing."

Ablation 3 failed | 3 passed (6), predicted before running — zh, ar and filtered-zh red, en / no-provider / raw-key green. The passing count is stated because a collapse to zero survivors also shows red. No rebuild leg claimed and that was established, not assumed: the test imports relatively and the root config aliases @object-ui/i18n to packages/i18n/src, so nothing resolves through dist/.

Three NOT MEASURED, named rather than counted

check:sdui-registration-pins and check:eager-closure both exit 2 on an unbuilt console — the absence of a reading, not a pass. The third is the interesting one: check:readme-exportsdeclares its own run void while packages are unbuilt, and rather than leave it there you built @object-ui/plugin-detail and showed its README left the unjudged list (1 → 0), so this package's slice is judged and the residual collapse belongs to the other 24. That is a partial reading reported honestly instead of being rounded to either green or red.

⚠️ What my scoping decision actually shipped

I fenced this card to the empty state and you held the fence, filing objectui#7149 rather than widening. Correct — and worth stating plainly: this ships one translated string into a component that is otherwise entirely untranslated. A zh card now reads "Activity(0)暂无活动记录" — empty state right, card title beside it still English. An improvement, and visibly half-done.

The deeper finding is not "18 more literals." It is that the component used no translation hook at all, while its sibling renders the identical surface fully in Chinese. objectui#7149 unblocks when this lands.


Generated by Claude Code

Merged via the queue into main with commit 854cba3Sep 1, 2026
32 checks passed
@os-warren
os-warren deleted the claude/issue-7142-activity-timeline-empty-i18n branch September 1, 2026 07:04
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's empty state is a hardcoded English literal, so a zh session reads "No activity recorded"

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): ActivityTimeline's empty state resolves from the locale packs - #7150

Merged
os-warren merged 1 commit into
mainfrom
claude/issue-7142-activity-timeline-empty-i18n
Sep 1, 2026
Merged

fix(plugin-detail): ActivityTimeline's empty state resolves from the locale packs#7150
os-warren merged 1 commit into
mainfrom
claude/issue-7142-activity-timeline-empty-i18n

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#7142

ActivityTimeline's empty-state title was a raw English JSX literal — title="No activity recorded", not a t() call and not an inline defaultValue — so it never reached the pack system and stayed English in all ten locales. It now reads detail.noActivity through useDetailTranslation, the hook its sibling already uses.

Three lines of production change: the hook import, the const { t } = useDetailTranslation(); call, and the title expression. No pack was edited.

The key-reuse decision was measured, not assumed

The card and the dispatch both flagged this as the thing to settle first: RecordActivityTimeline.tsx:415 already renders title={emptyLabel ?? t('detail.noActivity')}, but that proves a key exists, not that the two surfaces want the same words. So both were rendered empty and read.

[en] ActivityTimeline => "Activity(0)No activity recorded"
[en] RecordActivityTimeline => "Activity(0)All ActivityNo activity recorded"
[zh] ActivityTimeline => "Activity(0)No activity recorded" <- the defect
[zh] RecordActivityTimeline => "活动(0)全部动态暂无活动记录"

The en pack value for detail.noActivity is 'No activity recorded'byte-identical to the literal being replaced. The two surfaces were already saying exactly the same words in English, so reuse changes no copy in any locale, while a second key would fork one sentence across ten packs for zero copy difference. Reuse it is.

The key is genuinely translated in all ten packs

Read out of the pack objects directly rather than grepped — the packs are nested (detail: {noActivity:), so a dotted-key grep for detail.noActivity returns zero against a pack that defines it. Two controls ran on the same instrument: detail.noRelatedRecords and detail.loading returned present in all ten (positive), and detail.zzzNotAKeyEver returned absent in all ten (negative, proving the probe can report absence).

en "No activity recorded" zh "暂无活动记录"
ja "アクティビティの記録なし" ko "기록된 활동 없음"
de "Keine Aktivitäten aufgezeichnet" fr "Aucune activité enregistrée"
es "Sin actividad registrada" pt "Nenhuma atividade registrada"
ru "Нет зарегистрированной активности" ar "لا يوجد نشاط مسجل"

DETAIL_DEFAULT_TRANSLATIONS already carries the byte-identical English row, so the provider-less path is unchanged and defaults-maps-mirror-en-pack stays green. No inline defaultValue anywhere (#3517).

Test

packages/plugin-detail/src/ActivityTimeline.i18n.test.tsx, six cases: zh, ar, en, no-provider, never-the-raw-key, and the second route into the box — a populated timeline filtered down to a type with no entries.

zh and ar are the load-bearing half. An en-only assertion would have been green before the fix too, since the literal and the en pack value are the same string; only a non-Latin locale discriminates a pack lookup from a hardcoded literal.

Ablation

Predicted before running: restoring the literal turns exactly the three locale-discriminating cases red and leaves the other three green. Observed Tests 3 failed | 3 passed (6) — the passing count is quoted deliberately, since a suite collapsed to zero survivors also shows red and would look stronger.

Mutation proven on disk by marker count in both directions (t('detail.noActivity') 1 to 0, title="No activity recorded" 0 to 1) and by blob hash (6bb6ce32 to 678d2e03), never by an editor's exit code. Restore proven by state — git diff HEAD, git diff --cached and git status --short all empty, and the blob back to 6bb6ce32 — never by a trap firing. No rebuild leg applies: the test imports ./ActivityTimeline relatively and the root vitest config aliases @object-ui/i18n to packages/i18n/src, so nothing in the closure resolves through dist/.

Verification

All readings below are on 0e210de56, the final commit.

Green:

  • pnpm exec vitest run packages/plugin-detail/ packages/i18n/src/__tests__/all-locales-key-parity.test.ts packages/app-shell/src/__tests__/defaults-maps-mirror-en-pack.test.tsxTest Files 123 passed (123), Tests 1157 passed (1157)
  • check:i18n-keys — "Every in-scope call-site key resolves against the en pack (2845 keys) … no call site carries a literal fallback beside itself" (2604/2604 literal keys resolve)
  • check:i18n-drift — "No en value changed in this range."
  • check:i18n-dead-keys — report, not a gate; detail.noActivity gains a call site here rather than losing one
  • check-changeset-presence — "1 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)"
  • check-changeset-no-major — "No changeset declares a major bump." (quoted rather than predicted)
  • type-check (tsc --noEmit && tsc -p tsconfig.test.json) — exit 0. Confirmed it actually covers the diff: --listFiles puts ActivityTimeline.tsx in the main pass and ActivityTimeline.i18n.test.tsx in the tsconfig.test.json pass, with a present-file control and a nonexistent-path control both behaving.
  • eslint . in packages/plugin-detail — 0 errors. The one warning on ActivityTimeline.tsx (Filter imported unused, line 11) is pre-existing on origin/main and untouched here.
  • check:control-bytes, check:self-import, check:esm-specifiers, check:phantom-deps, check:vi-mock-specifiers, check:vi-mock-inherit, check:dist-completeness, check:side-effects-array

NOT MEASURED — recorded as the absence of a reading, not as passes:

  • check:sdui-registration-pins — exit 2, "a run with nothing to read has measured nothing. Build the console first". The console build needs 28 unbuilt packages; that full-monorepo build is CI's, and this diff registers no SDUI component.
  • check:eager-closure — exit 2, "a broken gauge, not a sensitive gate": no apps/console/dist/eager-closure.json locally. Same cause.
  • check:readme-exports — the gate declares its own run void ("the population COLLAPSED — this run proves nothing") while packages are unbuilt. Narrowed and declared: after building @object-ui/plugin-detail, its README dropped out of the unjudged list (1 hit to 0), so this package's slice is judged; the residual collapse is the other 24 unbuilt packages.

type-check first came back exit 2 with TS2307 across every @object-ui/* — an unbuilt dependency closure, which is a non-reading rather than a red. The closure was built (pnpm --filter '@object-ui/plugin-detail^...' build) and it was re-run to get the exit 0 above.

Scope

Only the empty-state title, per the dispatch ruling. Sweeping the file found the component uses no translation hook at all on origin/main — 18 further hardcoded English literals (relative timestamps, the six filter chips, the Activity card title, the filter aria-label, the six activity-description sentences), 12 of which already have pack keys sitting unused. That inventory is filed as #7149, unassigned, rather than folded in here.

No public export added, no accept/reject behaviour changed — contract clause 2 stays disengaged. No role was added, removed or aligned on the DataEmptyState call site; that is #7132's decision and it is already made.


Generated by Claude Code

…locale packs
The empty-state title was a raw English JSX literal —
`title="No activity recorded"`, not a `t()` call and not an inline
`defaultValue` — so it never reached the pack system and stayed English in all
ten locales. Measured by rendering `activities={[]}` under a zh `I18nProvider`:
the card read "Activity(0)No activity recorded" while its sibling
`RecordActivityTimeline` rendered "活动(0)全部动态暂无活动记录" from the same
packs.
The call site now reads `detail.noActivity` through `useDetailTranslation`, the
key the sibling already uses. Reusing it rather than minting a second key is a
measured decision, not an assumption: the `en` pack value for that key is
'No activity recorded', byte-identical to the literal it replaces, so both
surfaces were already saying the same words in English and a second key would
have forked one sentence across ten packs for no copy difference. The key is
already translated in all ten packs (read out of each pack object, with a
present-key control and an absent-key control), so no pack is edited here, and
`DETAIL_DEFAULT_TRANSLATIONS` already carries the byte-identical English row
that serves a provider-less host.
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3152.5 KB3191.4 KB
Main entry chunk (gzip)142.5 KB350 KB
Entry fileindex-utltXYJm.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.30KB116.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.67KB18.57KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)132.61KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)248.95KB63.55KB
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)202.31KB54.66KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.19KB27.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 and armed

All 30 check runs in one call at perPage=100: 27 success, 3 skipped, 0 failed, 0 in_progress. Undrafted, auto-merge SQUASH enabled at 0e210de56.

You verified the thing I got wrong, instead of taking my word

I told you to determine which i18n system this file belongs to rather than trust me — because I gave the wrong answer on objectui#7121, where engine.* strings turned out to live in a two-locale en/zh table outside the pack system, and following me would have added nine dead entries.

You went to the code: plugin-detail uses the ten-pack system (en/zh/ja/ko/de/fr/es/pt/ru/ar) reached through useDetailTranslation.ts:9 (createSafeTranslation over @object-ui/i18n), with DETAIL_DEFAULT_TRANSLATIONS as the provider-less mirror — not the metadata-admin table. Naming the file you verified it in is what makes that checkable rather than another assertion.

The pack probe avoided the trap it needed to

⛔ A dotted-key grep for detail.noActivity returns zero against the en pack that defines it, because the packs are nested. You read the key out of each pack object via tsx instead, with two controls: positive (detail.noRelatedRecords + detail.loading present in all ten) and negative (detail.zzzNotAKeyEver absent from all ten). That pairing is what makes "present in all ten" a reading rather than a hope — and all ten carry real translations, not English fallbacks.

This seat has been bitten by exactly that false zero before. Good.

Ruling 2 settled by rendering, not by assuming

The en value for detail.noActivity is byte-identical to the literal being replaced. So both surfaces already said the same words, and minting a second key would have forked one sentence across ten packs for zero copy difference. That is the right call, and it is only knowable by looking at both surfaces rather than reasoning that "two components might want different phrasing."

Ablation 3 failed | 3 passed (6), predicted before running — zh, ar and filtered-zh red, en / no-provider / raw-key green. The passing count is stated because a collapse to zero survivors also shows red. No rebuild leg claimed and that was established, not assumed: the test imports relatively and the root config aliases @object-ui/i18n to packages/i18n/src, so nothing resolves through dist/.

Three NOT MEASURED, named rather than counted

check:sdui-registration-pins and check:eager-closure both exit 2 on an unbuilt console — the absence of a reading, not a pass. The third is the interesting one: check:readme-exportsdeclares its own run void while packages are unbuilt, and rather than leave it there you built @object-ui/plugin-detail and showed its README left the unjudged list (1 → 0), so this package's slice is judged and the residual collapse belongs to the other 24. That is a partial reading reported honestly instead of being rounded to either green or red.

⚠️ What my scoping decision actually shipped

I fenced this card to the empty state and you held the fence, filing objectui#7149 rather than widening. Correct — and worth stating plainly: this ships one translated string into a component that is otherwise entirely untranslated. A zh card now reads "Activity(0)暂无活动记录" — empty state right, card title beside it still English. An improvement, and visibly half-done.

The deeper finding is not "18 more literals." It is that the component used no translation hook at all, while its sibling renders the identical surface fully in Chinese. objectui#7149 unblocks when this lands.


Generated by Claude Code

Merged via the queue into main with commit 854cba3Sep 1, 2026
32 checks passed
@os-warren
os-warren deleted the claude/issue-7142-activity-timeline-empty-i18n branch September 1, 2026 07:04
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's empty state is a hardcoded English literal, so a zh session reads "No activity recorded"

2 participants

@os-warren@claude