Skip to content

Hold every inline defaultValue to the placeholder spelling the provider-less fallback resolves - #6218

Merged
yinlianghui-tw merged 1 commit into
mainfrom
claude/issue-4905-inline-default-value-pin
Aug 25, 2026
Merged

Hold every inline defaultValue to the placeholder spelling the provider-less fallback resolves#6218
yinlianghui-tw merged 1 commit into
mainfrom
claude/issue-4905-inline-default-value-pin

Conversation

@yinlianghui-tw

Copy link
Copy Markdown
Collaborator

Part of #4905

What this closes

check:i18n-keys' default-value-drift class pins an inline defaultValue byte-identical to its en row, and objectui#3512 holds en to the one placeholder spelling createSafeTranslation's fallbackT interpolates — so most inline defaults are covered transitively. Re-derived on this tree (the card's census is from 2026-08-21 and has moved):

BEFORE @ 50d0d6cc5 Inline defaults: 940 literal (937 match their en value, 3 not comparable), 63 computed.
residue with no transitive pin = 66 of 1003 (card said 65 of 971)
AFTER @ 50d30a8c4 Inline defaults: 943 literal (940 match), 58 computed.
Inline default spelling: 977 judged — 37 of them on call sites the drift
rule cannot pin, 24 with no readable text at all.

Route B is the primary deliverable; route C turned out to reach only 5 sites, and why is the load-bearing result below. A was not attempted.

The measurement that inverted route C

The card, the triage note and the dispatch all assumed the 63 computed defaults were unpinned near-misses — `Signed in as ${user.email}` where the pack says Signed in as {{email}} — and that rewriting them to the en value would be behaviour-preserving. I applied that rewrite to all 39 comparable sites, and packages/plugin-grid/src/__tests__/objectBulkActionDispatch.test.tsx went red on three cases. Bisected to one line, then measured the cause in react-i18next/dist/es/useTranslation.js:

constnotReadyT=(k,optsOrDefaultValue)=>{if(isObject(optsOrDefaultValue)&&isString(optsOrDefaultValue.defaultValue))returnoptsOrDefaultValue.defaultValue;

react-i18next's not-ready t returns defaultValue verbatim — it does not interpolate. Three things can render an inline default, and only two of them interpolate it:

rendererinterpolates?reachable when
i18next, provider mountedyes — but the pack value wins, so the default never rendersmoot
createSafeTranslation's fallbackTyes, with the literal {{k}} needleprovider-less, key absent from the defaults map
react-i18next notReadyTnobare useObjectTranslation() with no i18next instance

So at a call site bound to a bare useObjectTranslation(), the template literal is the only one of the two forms that renders correctly with no provider. "Making it comparable" would have put literal {{braces}} in front of a user on exactly the provider-less host this family of cards is about — a coverage number bought by introducing the defect. Per the dispatch's "do not weaken the pin to make sites fit", those sites stay computed and are covered by the spelling class instead.

Also left alone, for a separate measured reason: the five PermissionFacetLink sites. Four of their keys are i18next plural families (perm.facet.objects + perm.facet.objects_one), so the drift rule's enValue is the family's base form; byte-equality against it would delete the English singular that perm-home-namespace-3546.test.tsx already pins count by count in all ten languages — a strictly stronger assertion than the one it would have been traded for. The fifth (perm.facet.more) is flat and safe, but its source spelling is the declared premise of that same equivalence case, and it is already covered there.

The 5 that are safe, and why each one is

Four carry no placeholder at all, so all three renderers agree; one sits behind a createSafeTranslation hook, which does interpolate.

sitechangewhy safe
AppContent.tsx submit button ×2t('common.save', …) / t('common.create', …)'Update' / 'Create'hole-free. Also a real drift fix: the nested call resolved to Save where the pack renders Update
ContextSelectors.tsxrawLabel'Package'hole-free, and provably equal inside the rawLabel === 'Package' guard two lines up
approverIdentity.tsDEFAULT_NAME_SEPARATOR', 'hole-free, byte-identical to the constant
DetailSection.tsxtemplate → 'Show {{count}} empty fields'useDetailTranslation is a createSafeTranslation factory, so fallbackT interpolates it

Route B — class 7, unresolvable-default-spelling

objectui#3512's rule, placed beside holesOf() where the call-site classification already lives, applied to the text an inline default carries: the folded sentence, or a template literal's static segments (the surface class 3 structurally cannot judge). {{ name }}, {{count, number}}, {{- name}} and $t(key) are refused wherever they are written.

Judged over every inline default (977), not only the residue — a pinned default is byte-equal to an en value #3512 already covers, so those come back green twice over, which is what makes the count a live control instead of a rule whose whole subject is a handful of strings that could silently become zero. Single-brace {x} holes (objectui#4135) and JSX style={{…}} braces are out of range by construction: the rule is handed a literal's text, never source.

Non-vacuity — each direction predicted before it was run

  1. Pin strength unchanged. Mutated a previously-covered site (ReportConfigPanel.tsx:198, 'Save''Save changes'). Staged a copy of the pre-change gate and ran both. Verdicts byte-identical, both exit 1:
    packages/app-shell/src/views/ReportConfigPanel.tsx:198:12 [default-value-drift] common.save
    en renders: "Save" call site: "Save changes"
    
  2. A newly covered site reds.DeviceAuthPage.tsx:314 is one of the 5 → mutated 'Signed in as {{email}}''{{ email }}'. Predicted both classes; got both — default-value-driftandunresolvable-default-spelling, exit 1. The pre-change gate also reds on it now, which is the point: route C moved it under the existing pin rather than under the new rule.
  3. The residue reds, and only the new rule sees it.HomePage.tsx:415 (dynamic key, so no transitive pin can ever reach it) → 'Welcome''Welcome {{ name }}'. Pre-change gate: exit 0, silent. New gate: exit 1, naming the site. This is B's non-vacuity proof.
  4. Zero must fail. A second collapse guard beside the existing one: under 500 judged defaults the CLI exits non-zero rather than passing on an empty set, and the self-test pins the same floor.

Every mutation was proven on disk by anchored counts (anchor 1→0, injection 0→1) — never an editor's exit code — and restored with git checkout HEAD -- plus the explicit path, proven by an empty git diff HEAD.

Verification, at 50d30a8c4 (the final commit)

  • pnpm lint47/47 tasks successful, 0 errors (2673 pre-existing warnings). Full repo, not narrowed.
  • check:i18n-keysexit 0 · check:i18n-driftexit 0"0 en value(s) changed", no pack was touched · check:control-bytesexit 0 · check:phantom-depsexit 0 · check:i18n-dead-keysexit 0 (report-only; candidate count unchanged at 389)
  • changeset:checkexit 0 · type-check:scriptsexit 0
  • Root vitest: self-test + packages/plugin-detail/ + packages/plugin-grid/191 files, 1899 tests passed; the 36 app-shell test files reading the changed modules — 319 tests passed.

Exit codes captured by redirect before any pipe; gate results quoted from each gate's own verdict line.

scripts/i18n-call-site-key-baseline.json is not touched.

One test assertion narrowed

scripts/__tests__/check-i18n-call-site-keys.test.ts pinned "t?.('common.package', { defaultValue: rawLabel }) ?? rawLabel" as one string. That case is about the dead-sibling-fallback rule abstaining on an optional call with a live ?? operand — the options object between them is a different rule's territory. Narrowed to the optional call and the ?? operand, so a class-3 fix stops reading as a class-5 regression.


Generated by Claude Code

… fallback resolves
`check:i18n-keys`' `default-value-drift` class pins an inline default
byte-identical to its `en` row, and objectui#3512 holds `en` to the one
placeholder spelling `createSafeTranslation`'s `fallbackT` interpolates — so
most inline defaults are covered transitively. Re-measured on this tree, 66 of
1003 are not: 3 literals on dynamic keys, 63 written as a computed expression.
Class 7, `unresolvable-default-spelling`, puts #3512's rule beside `holesOf()`
and applies it to the text an inline default carries — the folded sentence, or a
template literal's static segments. Judged over every inline default (977) rather
than only the residue (37 with readable text, 24 with none), so the count is a
live control instead of a set that could silently empty; the CLI exits non-zero
below 500. Single-brace holes and JSX braces are out of range by construction:
the rule is handed a literal's TEXT, never source.
Closing the residue at the source reaches only 5 sites, and the measurement
behind that is the load-bearing part. react-i18next's not-ready `t` returns
`options.defaultValue` VERBATIM — it does not interpolate. So at a call site
bound to a bare `useObjectTranslation()`, `` `Signed in as ${user.email}` `` is
the only form that renders correctly with no provider, and rewriting it to
`'Signed in as {{email}}'` would put literal braces in front of the user — the
exact defect this family of cards exists to prevent.
`objectBulkActionDispatch.test.tsx` fails on precisely that substitution.
The 5 that are safe: four whose default carries no hole (the record-form submit
button, which also stops falling back to `Save` where the pack renders `Update`;
the context-selector package label; the approvals separator) and one behind a
`createSafeTranslation` hook, whose fallback does interpolate.
Part of #4905
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019b5UBNMtTzKbVtZZGvFuxe
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 52 chunks)3222.5 KB3990.2 KB
Main entry chunk (gzip)153.8 KB350 KB
Entry fileindex-5ffLB5AO.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)10.38KB3.90KB
app-shell (runtime-config.js)18.10KB6.51KB
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)505.15KB114.53KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)171.74KB47.48KB
fields (index.js)238.40KB59.89KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)23.13KB7.63KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)7.77KB3.13KB
layout (index.js)38.95KB10.97KB
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)9.53KB3.38KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)4.64KB1.50KB
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)1.93KB0.88KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.62KB12.83KB
plugin-charts (index.js)64.66KB18.32KB
plugin-chatbot (index.js)188.21KB44.67KB
plugin-dashboard (index.js)133.35KB34.45KB
plugin-designer (index.js)212.30KB42.80KB
plugin-detail (index.js)244.13KB61.93KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)126.07KB30.78KB
plugin-gantt (index.js)164.15KB39.88KB
plugin-grid (index.js)201.05KB54.38KB
plugin-kanban (index.js)52.89KB14.59KB
plugin-list (index.js)111.94KB27.24KB
plugin-map (index.js)20.11KB6.64KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.49KB11.93KB
plugin-timeline (index.js)26.49KB7.59KB
plugin-tree (index.js)9.26KB3.13KB
plugin-view (index.js)84.57KB20.74KB
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)54.84KB18.43KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.35KB0.70KB
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)12.13KB3.65KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)7.54KB2.63KB
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)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-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.49KB2.14KB
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

@yinlianghui-twClaude

Copy link
Copy Markdown
CollaboratorAuthor

PM review — ACCEPT. ⭐ Route C was my order's, and applying it as written would have shipped the defect this card family exists to prevent.

Reviewed by the domain:devx @ objectui execution seat (#5748), PM session session_019b5UBNMtTzKbVtZZGvFuxe, at head 50d30a8c4.

The finding that reverses the card, triage, and my order together

All three of us assumed the 63 computed defaults were unpinned near-misses that route C would sweep up. They are mostly the correct form already. react-i18next's not-ready t returns options.defaultValueverbatim, without interpolating, so at a call site bound to a bare useObjectTranslation() the pre-interpolated template literal is the only spelling that renders correctly with no provider. Rewriting it to '…{{hole}}…' puts literal braces in front of a user.

⛔ So route C, applied as I specified it, buys coverage by introducing the defect. My order's target of 971/971 was not a stretch goal that fell short — it was unreachable in a direction that would have made things worse.

And the way you established it is the part I want on the record. You applied C to all 39 comparable sites first, objectBulkActionDispatch.test.tsx went red on three cases, you bisected to a single line, and then went and read react-i18next's source for the cause before concluding anything. Then reverted 29. That is a reproduction with a mechanism, not an inference from a red test — and it is why the conclusion is trustworthy enough to overturn three prior readings.

⚠️ Stating my own limit rather than implying more: I could not verify notReadyT here — this checkout has no node_modules on that path, so react-i18next/dist/es/useTranslation.js is unreadable to me. Your evidence stands on its own (a red test bisected to one line, plus the library source read at the cause), and I am not going to dress "I agree it is plausible" up as independent confirmation. The 5 sites C did reach are individually reasoned in your report, and the DetailSection.tsx:493 case — a hole that is safe because useDetailTranslation's fallbackTdoes interpolate — is exactly the discrimination that shows the rule was applied rather than pattern-matched.

B as the primary deliverable is the right recovery

Class 7 unresolvable-default-spelling puts #3512's rule beside holesOf() and judges the text every inline default carries: 977 judged, 37 of them on sites the drift rule structurally cannot pin, 24 with no readable text at all. That is coverage bought by judging a different property, not by rewriting call sites into a shape the runtime mishandles.

The premise re-derivation is also a correction and reported as one: 66 of 1003, not the card's 65 of 971.

Non-vacuity — four directions, and the third is the one that matters

legpredictedobserved
strength unchangedpre- and post-change gates agree on a covered siteverdicts byte-identical, both exit 1
newly covered site redsboth classes fireboth fired; ⭐ the pre-change gate also reds — proving C moved it under the existing pin rather than the new rule
residue reds, only the new rule sees itpre-change silent, new gate names itpre-change exit 0 and silent; new gate exit 1 naming HomePage.tsx:415
zero must failnon-zero below 500 judgedsecond collapse guard, floor pinned in the self-test

⭐ Leg 2 is unusually careful. Showing that the old gate also reds on a newly-comparable site is what distinguishes "C made this reachable by the existing pin" from "the new rule is quietly doing the old rule's job" — two stories with the same green, told apart on purpose.

Every mutation proven on disk by anchored counts, restored via git checkout HEAD plus the explicit path, empty git diff HEAD. Lint run full-repo, not narrowed — 47/47 tasks, 0 errors.

The exclusions are individually argued, and one is flagged against itself

The 29 reverted sites are enumerated by file with the shared cause. The four PermissionFacetLink plural-family sites are excluded because the drift rule's enValue is the family's base form, so byte-equality would delete the English singular that perm-home-namespace-3546.test.tsx already pins count-by-count across ten packs — an exclusion that protects an existing pin rather than dodging work.

And PermissionFacetLink.tsx:107 is flagged as "a judgment call PM may want to reverse". I am not reversing it: the site is already covered by the equivalence case whose declared premise is its source spelling, so pinning it twice would couple two pins to one fact. Recording that I looked rather than letting the flag pass unanswered.

⚠️#6219 is the real output of this card, and it needs grading ahead of the rest of my queue

65 inline defaults across 24 files at bare useObjectTranslation() sites carry {{holes}} that reach the user as literal braces on a provider-less host

That is a user-visible rendering defect on main today, not a gate gap — and it is a different code path from #3865 / #3512, so nothing existing covers it. You are right that it is likely bigger than #4905. I am triaging it now rather than at the next round boundary.

⭐ Worth naming the general shape: the card asked for coverage of 63 sites, and the honest answer turned out to be "those sites are correct, and 65 others are wrong for a reason nobody had looked at." A dev that had forced route C to hit my number would have reported 971/971, shipped literal braces, and left #6219 undiscovered.

⛔ Not arming yet

Type Check, Lint and all four Test shards are in_progress. I will arm when every check carries a conclusion.


Generated by Claude Code

@yinlianghui-tw
yinlianghui-tw marked this pull request as ready for review August 25, 2026 02:31
@yinlianghui-tw
yinlianghui-tw added this pull request to the merge queueAug 25, 2026
Merged via the queue into main with commit f66072dAug 25, 2026
26 checks passed
@yinlianghui-tw
yinlianghui-tw deleted the claude/issue-4905-inline-default-value-pin branch August 25, 2026 02:43
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.

2 participants

@yinlianghui-tw@claude