Skip to content

fix(i18n): remove five dead objectView/appDesigner locale keys, add reverse sweep - #4731

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-4658-dead-locale-keys
Aug 15, 2026
Merged

fix(i18n): remove five dead objectView/appDesigner locale keys, add reverse sweep#4731
yinlianghui merged 1 commit into
mainfrom
claude/issue-4658-dead-locale-keys

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes#4658

What changed

Deleted five locale keys with no t()/tt() call site anywhere in the repo,
from all ten packs (en plus the nine translations, so
all-locales-key-parity.test.ts stays green):

  • console.objectView.striped, console.objectView.bordered,
    console.objectView.virtualScroll — grid options objectstack#7176
    retired upstream (maintainer-ruled 2026-08-10) and objectui stopped
    declaring/forwarding in #4649. Dead permanently, not merely dormant.
  • appDesigner.stripedRows, appDesigner.bordered — textually similar name
    only; independently dead. No control in AppCreationWizard.tsx, no entry
    in useDesignerTranslation.ts's DESIGNER_DEFAULT_TRANSLATIONS fallback
    map, no mention anywhere in the repo outside the locale packs.

Added scripts/check-i18n-dead-keys.mjs, the reverse-sweep the card asked
for: pack key set minus referenced key set. Report-only — exit 0 always
unless --strict is passed (nothing calls --strict; not wired into any
workflow). It reuses check-i18n-call-site-keys.mjs's own AST walk rather
than a second parser: analyze() was extended (not duplicated) to also
collect, during the same pass that already classifies every call site,
referencedKeys (literal + plural-suffixed), referencedBranches
(returnObjects: true consumption), and dynamicHeads (template-key static
prefixes — the call-site gate's own missing-prefix class read in reverse).

A candidate the AST pass cannot explain also gets a text safety net: one
grep -rF pass over the whole repo (not just packages/+apps/, which is
the AST walk's scope) for the literal dotted key string, excluding the locale
packs themselves. Zero hits anywhere → CONFIRMED; some hit elsewhere (an
indirect labelKey: 'foo.bar' property, a doc mention, a test fixture) →
NEEDS-REVIEW, not confirmed dead. This is what lets the report tell a
truly-dead key apart from one only the AST pass's own blind spots (calls
outside packages/+apps/, keys reached through a variable rather than a
literal argument) made look dead.

Premise verification (issue's mechanism assumptions)

  1. All five keys defined in all ten packs, zero call sites anywhere
    confirmed independently three ways: (a) check-i18n-dead-keys.mjs's AST
    pass found no t()/tt() call site; (b) the text safety net found zero
    textual occurrences of any of the five dotted key strings anywhere outside
    the locale packs (not even in comments/docs/tests); (c) direct greps for
    the bare identifiers (striped, bordered, virtualScroll,
    stripedRows) confirm the only non-locale, non-unrelated hits are the
    objectql.zod.ts comment documenting the upstream retirement and the
    block-config.ts comment documenting the #4649 field removal — both
    corroborating evidence, neither a consumer.
  2. check-i18n-call-site-keys.mjs's parsing machinery reused, not
    duplicated
    analyze() was extended in place; the new script imports
    analyze() and collectEnKeys() directly. The one place a second,
    independent pass was necessary (the text safety net) is documented as such
    in the new script's header, with the specific gap it closes (indirect
    references, and examples//e2e/ — measured empty of t()/tt() calls
    today, so the AST walk's packages/+apps/-only scope costs nothing on
    this run, but is flagged as a scope note rather than silently matched).
  3. The three existing i18n gates each read the direction that can't see
    this class
    — confirmed by reading each gate's own header (already
    documented there, cross-checked against behavior): all-locales-key-parity
    is pack-vs-pack (full parity on a key nobody wants); check-i18n-*-keys
    is call-site-to-en (asks whether a wanted key exists, never whether an
    existing key is wanted); check-i18n-en-drift fires on en VALUE change
    (a key that never changes never registers, and removal is explicitly
    parity's business per its own output — see the check:i18n-drift run
    below).

Sweep report / measured false-positive rate

Run against this PR's tree (d3d028631): 2927 pack keys, 13776 referenced.
626 candidates, 384 CONFIRMED, 242 NEEDS-REVIEW (38.7%) — i.e. a naive
AST-only reverse sweep (no text safety net) would have been wrong about
2 in 5 of its own candidates. That NEEDS-REVIEW rate is this PR's answer to
"is this worth red-ifying yet": no — even with the text safety net, a
CONFIRMED verdict still needs a human before deletion, so --strict stays
unwired.

Spot-verification beyond the five keys actually deleted here (sampled, not
exhaustive — the full 384 is far bigger than this card's scope):
workflow.* (54/58 confirmed) traces to ProcessDesigner.tsx, which
hardcodes English directly and imports no translation hook at all;
publicForm.demo.* (36/36 confirmed) has zero footprint anywhere including
outside t() calls. Filed as #4730 (unassigned, finding label,
observation-class) rather than expanded in this PR — out of #4658's
five-key scope, and #4730 is not addressed here.

Tests

NODE_OPTIONS=--max-old-space-size=4096 pnpm exec vitest run packages/i18n/ scripts/__tests__/ --maxWorkers=2
Test Files 89 passed (89)
Tests 1787 passed (1787)
pnpm run type-check:scripts # tsc -p tsconfig.scripts.json — clean
node scripts/check-i18n-call-site-keys.mjs
Every in-scope call-site key resolves against the en pack (2927 keys), …
node scripts/check-i18n-en-drift.mjs
0 en value(s) changed (0 key(s) added, 5 removed — those are
all-locales-key-parity's), … No en value changed in this range.
pnpm run check:control-bytes
✅ check-control-bytes: OK (scanned 4255 tracked text file(s); skipped 85 binary).
node scripts/check-changeset-presence.mjs
✅ 10 source file(s) of 1 released package(s) changed, and this change
declares 1 changeset(s).
node scripts/check-changeset-no-major.mjs
✅ No changeset declares a `major` bump.
node_modules/.bin/eslint scripts/check-i18n-dead-keys.mjs \
scripts/check-i18n-call-site-keys.mjs scripts/__tests__/check-i18n-dead-keys.test.ts
(clean, no output)

New self-test (scripts/__tests__/check-i18n-dead-keys.test.ts, 16 tests):
positive controls for all three "still live" escape hatches (literal+plural-
suffix, returnObjects branch, dynamic-template head — each must keep a key
OUT of the candidate set), a dead-key positive control (must be CONFIRMED), an
indirect-reference control (must be NEEDS-REVIEW, not CONFIRMED), and the
locale-pack self-reference exclusion (a key's own definition line in en.ts
must never itself count as a textual hit — the one bug that would silently
empty the CONFIRMED tier entirely; caught during development when an earlier
docstring draft used a real target key as an illustrative example and
self-polluted its own report entry, fixed by not spelling a real candidate
key in the script's own comments).

All gate/test output above and in this body is from the same commit —
git rev-parse --short HEAD = d3d028631, run after the final commit
(re-run if this PR gets a post-review push).

Notes

  • check-i18n-call-site-keys.mjs was extended, not forked: analyze()
    gained three new Set fields (referencedKeys, referencedBranches,
    dynamicHeads), populated inline at the exact points the existing walk
    already computes the underlying facts. No existing finding/counter logic
    changed — pinned by the pre-existing self-test suite staying green
    unmodified (97 tests, same file).
  • scripts/ changes do not need a changeset (check-changeset-presence.mjs
    confirms: 10 files under a released package's src/, 0 under an ignored
    package — scripts/ isn't under any package's src/ at all).
  • --strict exists on the new script deliberately, so a future decision to
    red-ify this needs no rewrite — just wiring, once #4730's backlog is
    worked down enough that CONFIRMED stops meaning "hundreds, mostly
    unreviewed."

Generated by Claude Code


Generated by Claude Code

…everse sweep
console.objectView.striped/bordered/virtualScroll and appDesigner.stripedRows/
bordered had no t()/tt() call site anywhere in the repo, in any of the ten
packs. The console.objectView three describe grid options objectstack#7176
retired upstream and objectui stopped forwarding in #4649; the appDesigner
pair was independently dead by name coincidence only.
Adds scripts/check-i18n-dead-keys.mjs, a report-only reverse sweep (pack key
set minus referenced key set) that reuses check-i18n-call-site-keys.mjs's own
AST walk (extended, not duplicated, to also collect referenced keys/branches/
dynamic-template heads during the same pass) plus a whole-repo text safety
net that demotes indirectly-referenced keys to a NEEDS-REVIEW tier instead of
false-confirming them dead. Not wired into CI - report-only by design.
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RnQd8iMMUwXQEV1crFmQiQ
@vercel

vercelBot commented Aug 15, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectuiIgnoredIgnoredAug 15, 2026 7:42pm

Request Review

@yinlianghuiClaude

Copy link
Copy Markdown
CollaboratorAuthor

Review verdict: ACCEPT (reviewer of record, PM session session_01RnQd8iMMUwXQEV1crFmQiQ).

Verified against this PR, not the report: fifteen changed files; Fixes #4658 the only closing keyword; the i18n changeset covers the ten pack edits per the presence gate's own count. I independently re-verified all five deleted keys' reference faces on main — zero hits outside the locale definitions. The sweep lands exactly the shape the dispatch asked for and then earns its keep: reuse-not-fork proven by the untouched 97-test suite staying green after analyze() grew three fields; the NEEDS-REVIEW tier converts the false-positive risk into a measured number (38.7% of candidates — the quantified case for keeping --strict unwired); and the self-test's positive controls cover every escape hatch including the self-pollution trap the dev caught in its own docstring draft. The 384-key broader surface is correctly filed as its own card rather than absorbed. Gates pinned to d3d028631, matching this PR's current head.

Will flip ready + enable auto-merge once Lint, Type Check, and Test shards 1–4 conclude success on this head.


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Main entry (gzip)24.7 KB350 KB
Entry fileindex-DQz-ygvc.js
StatusPASS

📦 Bundle Size Report

PackageSizeGzipped
app-shell (index.js)9.56KB3.59KB
app-shell (runtime-config.js)7.42KB2.32KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)8.92KB3.41KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)1.17KB0.53KB
auth (AuthProvider.js)25.13KB5.40KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.13KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.64KB2.21KB
auth (SocialSignInButtons.js)9.60KB3.89KB
auth (UserMenu.js)3.40KB1.22KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)38.46KB10.17KB
auth (createAuthenticatedFetch.js)6.34KB2.43KB
auth (index.js)2.35KB1.07KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.02KB0.88KB
auth (useIsWorkspaceAdmin.js)1.61KB0.85KB
collaboration (CommentThread.js)26.07KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.65KB0.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)490.20KB108.71KB
core (index.js)3.79KB1.52KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)157.05KB43.28KB
fields (index.js)230.33KB57.14KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (i18n.js)4.32KB1.77KB
i18n (index.js)3.35KB1.38KB
i18n (pickLocalized.js)3.69KB1.73KB
i18n (provider.js)23.12KB7.62KB
i18n (useDisplayLocale.js)2.84KB1.45KB
i18n (useObjectLabel.js)27.59KB6.63KB
i18n (useSafeTranslation.js)7.77KB3.13KB
layout (index.js)38.98KB10.85KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.74KB
mobile (index.js)1.50KB0.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.71KB0.42KB
mobile (useResponsiveConfig.js)1.36KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)9.35KB3.31KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)3.89KB1.22KB
permissions (evaluator.js)4.41KB1.44KB
permissions (index.js)0.91KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.52KB
permissions (usePermissions.js)1.81KB0.83KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.86KB12.91KB
plugin-charts (index.js)64.44KB18.28KB
plugin-chatbot (index.js)181.21KB43.14KB
plugin-dashboard (index.js)121.90KB31.76KB
plugin-designer (index.js)212.39KB42.83KB
plugin-detail (index.js)239.81KB59.97KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)114.72KB27.70KB
plugin-gantt (index.js)164.30KB40.02KB
plugin-grid (index.js)192.10KB51.24KB
plugin-kanban (index.js)52.74KB14.53KB
plugin-list (index.js)111.14KB26.92KB
plugin-map (index.js)18.16KB5.81KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)41.38KB11.09KB
plugin-timeline (index.js)26.68KB7.66KB
plugin-tree (index.js)8.50KB2.88KB
plugin-view (index.js)83.81KB20.49KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.71KB3.53KB
providers (index.js)0.44KB0.22KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.67KB2.37KB
react (LazyPluginLoader.js)3.77KB1.33KB
react (SchemaRenderer.js)27.64KB9.44KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.28KB0.68KB
react (schema-input.js)1.45KB0.83KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)4.09KB1.74KB
sdui-parser (index.js)4.47KB2.03KB
sdui-parser (parse.js)10.04KB2.82KB
sdui-parser (types.js)0.29KB0.24KB
sdui-parser (validate.js)4.69KB1.48KB
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)0.20KB0.18KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)0.20KB0.18KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.87KB0.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-retry.js)4.32KB2.02KB
types (index.js)3.05KB1.52KB
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 (system-fields.js)3.33KB1.54KB
types (theme.js)0.20KB0.18KB
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

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Dead locale strings: console.objectView.striped/bordered/virtualScroll and appDesigner.stripedRows/bordered have no call site in any of the ten packs

2 participants

@yinlianghui@claude