Found while implementing #5590 (PR #5636), which fixed the third key of this same block. Not fixed there — outside that card's render-site fence, which the dispatching seat drew deliberately.
What
packages/components/src/renderers/basic/record-picker.tsx has three props whose contract is the I18nLabel union. #5590 fixed one (emptyText). The other two are still unresolved, and they fail in two different ways.
Measured on the installed pin (@objectstack/spec@17.0.0 GA), ElementRecordPickerPropsSchema's own shape — all three members resolve to optional -> union -> string | record:
top-level keys: object, labelField, valueField, label, filter, sort, limit,
targetVariable, placeholder, emptyText, displayField,
searchFields, multiple, aria
label union => string | record
placeholder union => string | record
1. placeholder — the map arm THROWS
placeholder is read raw (props.placeholder ?? 'Select a record…', :164 before #5590) and handed to SelectValue. Measured, rendering the block with placeholder: { en: 'Owner', 'zh-CN': '负责人' }:
placeholder=MAP lang=zh-CN :: THREW Objects are not valid as a React child
(found: object with keys {en, zh-CN}).
Identical harm to the one #5590 just removed from emptyText: not a mis-render, the whole picker subtree fails.
2. label — worse than a throw, because it looks like it works
label goes through this file's local toText() helper, whose object branch is
return String(o.label ?? o.name ?? o.title ?? o.en ?? '');
That reaches o.enunconditionally, so it is not locale resolution — it is an English pick wearing locale resolution's clothes. Two measured consequences, same fixture shapes, viewer language zh-CN:
label={ en: 'Owner', 'zh-CN': '负责人' } -> RENDERED text="Owner…" (English to a zh-CN viewer)
label={ 'zh-CN': '负责人', ja: '担当' } -> RENDERED text="Select a record…" (label element ABSENT)
The second is the quiet one. With no en entry toText returns '', and the render site is {label && ...} — so a correctly authored map that simply omits English makes the picker's label disappear, with nothing thrown and nothing logged.
3. The declarations say 'string' for both
inputs still declares { name: 'placeholder', type: 'string' } and { name: 'label', type: 'string' }. Under the ComponentInput.type rule (packages/types/src/base.ts) that narrowing is correct while the renderer drops the arm — the same reasoning emptyText carried until #5590 — so the declarations should move only in the change that makes the arms render, not before.
What a fix would decide
emptyText has just settled the shape for this file: resolve at the read site with pickLocalized, then widen the ComponentMeta arms in the same change. placeholder looks like a direct repeat of that.
label is not a repeat and is the reason this is filed rather than folded in:
toText is shared — it also renders row values (toText(row?.[labelField]), :206). Those are record field values, not I18nLabel, so changing toText itself changes a second, unrelated call site. The narrow move is to resolve label through pickLocalized at its own read site and leave toText to the row values.- Sibling read sites pair
pickLocalized with translateLabel for label specifically (containers.tsx:569, :831) — whether this block's label should follow that pairing or use bare pickLocalized is a real choice, not a mechanical one.
Filed unassigned and unqueued: whether these two travel together, and whether label wants the translateLabel pairing, is a triage call.
Generated by Claude Code
Found while implementing #5590 (PR #5636), which fixed the third key of this same block. Not fixed there — outside that card's render-site fence, which the dispatching seat drew deliberately.
What
packages/components/src/renderers/basic/record-picker.tsxhas three props whose contract is theI18nLabelunion. #5590 fixed one (emptyText). The other two are still unresolved, and they fail in two different ways.Measured on the installed pin (
@objectstack/spec@17.0.0GA),ElementRecordPickerPropsSchema's own shape — all three members resolve tooptional -> union -> string | record:1.
placeholder— the map arm THROWSplaceholderis read raw (props.placeholder ?? 'Select a record…',:164before #5590) and handed toSelectValue. Measured, rendering the block withplaceholder: { en: 'Owner', 'zh-CN': '负责人' }:Identical harm to the one #5590 just removed from
emptyText: not a mis-render, the whole picker subtree fails.2.
label— worse than a throw, because it looks like it workslabelgoes through this file's localtoText()helper, whose object branch isThat reaches
o.enunconditionally, so it is not locale resolution — it is an English pick wearing locale resolution's clothes. Two measured consequences, same fixture shapes, viewer languagezh-CN:The second is the quiet one. With no
enentrytoTextreturns'', and the render site is{label && ...}— so a correctly authored map that simply omits English makes the picker's label disappear, with nothing thrown and nothing logged.3. The declarations say
'string'for bothinputsstill declares{ name: 'placeholder', type: 'string' }and{ name: 'label', type: 'string' }. Under theComponentInput.typerule (packages/types/src/base.ts) that narrowing is correct while the renderer drops the arm — the same reasoningemptyTextcarried until #5590 — so the declarations should move only in the change that makes the arms render, not before.What a fix would decide
emptyTexthas just settled the shape for this file: resolve at the read site withpickLocalized, then widen theComponentMetaarms in the same change.placeholderlooks like a direct repeat of that.labelis not a repeat and is the reason this is filed rather than folded in:toTextis shared — it also renders row values (toText(row?.[labelField]),:206). Those are record field values, notI18nLabel, so changingtoTextitself changes a second, unrelated call site. The narrow move is to resolvelabelthroughpickLocalizedat its own read site and leavetoTextto the row values.pickLocalizedwithtranslateLabelforlabelspecifically (containers.tsx:569,:831) — whether this block'slabelshould follow that pairing or use barepickLocalizedis a real choice, not a mechanical one.Filed unassigned and unqueued: whether these two travel together, and whether
labelwants thetranslateLabelpairing, is a triage call.Generated by Claude Code