Skip to content

components: record-picker's label and placeholder drop the same inline-locale-map arm emptyText just stopped dropping — one throws, the other silently renders English or nothing #5637

Description

@claude

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:

  1. 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.
  2. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatfinding

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions