fix(fields,components): read the selectFirst gate joiner from the locale pack - #4739
Conversation
…ale pack
The dependency-gate sentence is deliberately shared between the lookup widget
and the form renderer, but each caller filled its {{fields}} slot with its own
hardcoded separator — ', ' in LookupField, ' / ' in the form renderer and in
OptionsEmptyState — so a field gated on two parents read two different ways
depending on which side produced it. A list separator is a locale property, so
all three now read validation.formInvalidJoiner, the key already shipped in the
ten packs for the invalid-submit toast's field list.
Fixes #4026
…locale The probe renders the hint it received rather than writing it to a module variable during render (react-hooks/globals). Adds the changeset for the fields + components patch.
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
|
PM review — ACCEPT (reviewer of record, session Independently verified against the branch, not the report:
Correction accepted, publicly: the dev observed that "objectui has no skip-changeset label" is not literally accurate — the label exists in the registry (open PR #4639 carries it). The operative fact stands (no gate consumes it; applying it exempts nothing, which is why three PRs were corrected today), but future dispatches from this seat will say "no skip-changeset mechanism — the label exists but nothing reads it." Thanks for measuring rather than repeating me. Body: Generated by Claude Code |
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
Fixes #4026
The dependency-gate hint now enumerates its controlling fields with the locale's own
list separator, and reads identically whichever caller produced it.
The defect
lookup.selectFirstandfields.options.selectFirstare deliberately one wording — theform renderer's own comment says the two callers exist so "the gate can never read
differently depending on which side produced it". That invariant held for the sentence
but not for its
{{fields}}slot, which each call site filled with its own hardcodedseparator, and not even the same one:
packages/fields/src/widgets/LookupField.tsx(dependsOnFieldsText)lookup.selectFirst', 'packages/components/src/renderers/form/form.tsx(gatedHintmemo)fields.options.selectFirst' / 'packages/fields/src/widgets/OptionsEmptyState.tsxfields.options.selectFirst' / 'So a field gated on two parents read
Select Account, Lead Source firstfrom the lookupand
Select Account / Lead Source firstfrom the form. A list separator is a property ofthe locale rather than of the code, so under zh/ja both spellings were also wrong for the
script (U+3001 is the CJK enumeration comma), as they were under ar (U+060C).
The fix
All three call sites now read
validation.formInvalidJoiner— the key objectstack#5407already shipped in all ten packs for the invalid-submit toast's field list, which is the
same class of truncated-name list. One key, every caller: a second, gate-specific key
would have recreated exactly the divergence the shared sentence exists to prevent, which
is the constraint the card pins.
No locale-pack edits. The only new declaration is
validation.formInvalidJoinerin@object-ui/fields'createSafeTranslationdefaults table, whose value is theenpack's', '— byte-identical to the literalLookupFieldpreviously hardcoded, so aprovider-less English render is unchanged. That table's own discipline is that a default is
declared where it is read.
The third call site, and why it is in scope
The card names two sites.
OptionsEmptyStateis a third, and it is the "standalone widget"caller of
fields.options.selectFirstthat the form renderer's comment refers to — ithardcoded
' / 'too. Changing only the two named sites would have left the sharedsentence still reading two ways within one key, defeating the card's stated invariant, so
it is fixed here and named rather than filed. It meets the in-place bar: same defect class,
same mechanical fix already pinned by the adjudicated route, same package and gate
families, and no open PR touches the file (checked against the open PR list at dispatch
time — #4655, #4639, #4093, none of which touch these three files).
Verification
All of the below run at
954e71ec4, the pushed head, working tree clean.vitest run packages/fields/ packages/components/type-check(fields, components)eslint . --quiet(fields, components)check:control-bytescheck:i18n-keyscheck:i18n-driftcheck:i18n-dead-keyscheck:phantom-depscheck-changeset-presencecheck-changeset-no-majorThe
check:i18n-*family was not in the dispatched gate list; it was re-derived from theactual changed paths (a new
t()call site plus a defaults-table entry) and added.Reverse verification
The fix was committed first, then ablated by restoring all four changed source files from
origin/main, and the new test re-run. It went red in the expected direction, 10 of 19cases failing, and the failure messages reproduce the reported defect verbatim:
Worth recording honestly: 9 cases stayed green under the ablation, and that is
informative rather than a gap. The
OptionsEmptyState-vs-form comparison passed becauseboth sides hardcoded the same wrong
' / '— they agreed while both were broken, whichis precisely why the lookup-vs-form equality is the load-bearing assertion. The en/de/ko
separator cases passed because
', 'happens to be those packs' correct value, which isthe "right by accident" half objectstack#5407 documented. The fix was then restored from
the branch and the union re-run at the final head.
Test
One new file,
packages/fields/src/__tests__/selectFirst-gate-joiner-locale-parity.test.tsx(19 cases over en/de/ko/zh/ja/ar). It lives in
@object-ui/fieldsbecause that is the onlypackage that can see both sides — fields depends on components, not the reverse, and the
existing split in
form-depends-on-labels.test.tsxexists for that same reason.It asserts equality between the callers rather than three copies of an expected string:
a joiner that later changes wrongly-but-consistently is a translation bug, while one that
changes inconsistently is this defect returning. The per-script cases (zh/ja get U+3001, ar
gets U+060C + space, Latin scripts and ko get
', ') cover the first half, mirroring theshape of the existing
validation-list-joiner-locale-paritytest.The form side is read through a probe widget registered as
field:select, so the comparedstring is
gatedHint's own output with no other widget's formatting in between; the lookupside is read from the gated trigger's
title, which is the whole sentence and nothingelse. The probe renders what it received rather than writing to a module variable during
render — the latter is a side effect
react-hooks/globalsrejects.Changeset
.changeset/selectfirst-gate-joiner-locale.md,patchfor@object-ui/fieldsand@object-ui/components. Nomajor, per the version-alignment rule. objectui has noskip-changesetmechanism and none was applied.Generated by Claude Code