Skip to content
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
This repository was archived by the owner on Nov 4, 2026. It is now read-only.

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

Closed

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.en unconditionally, 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

Activity

  1. added
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    on Aug 21, 2026
  2. huangyiirene commented on Aug 22, 2026

    @huangyiirene
    Collaborator

    Concentration-round first-touch (triage seat): promoted finding → pm:queue, Bug (label bug), size S. The two open calls the card left for triage, ruled here:

    1. They travel together — one PR, per the settled emptyText shape (components: record-picker's emptyText drops the inline-locale-map arm the contract admits, and the audit tracking it (#4163) is closed #5590): resolve both at their read sites with pickLocalized, widen the two ComponentMeta arms in the same change (never before), pin all three measured fixtures (map-throws, English-to-zh-viewer, no-en-map label vanishing).
    2. label takes the sibling pairing — pickLocalized + translateLabel, matching containers.tsx:569/:831; consistency across read sites for the same key is the point of this family. ⛔ toText itself stays untouched (it serves row values, a different contract) — the card's own analysis is adopted as the fence.

    Generated by Claude Code

  3. self-assigned this
    on Aug 22, 2026
  4. os-zhuang commented on Aug 22, 2026

    @os-zhuang
    Contributor

    Claim: PM loop round 17
    Session: session_01EuPCi56cnGyykygi3z9w4m
    Branch: claude/issue-5637-record-picker-label-placeholder-i18n
    Worktree: objectui-issue-5637
    Domain: domain:ui
    File surface: packages/components/src/renderers/basic/record-picker.tsx and its tests under packages/components/src/renderers/**/__tests__/ (stop on breach; explain in the report)
    Container & model: S, mode:subagent, model: opus
    Clause-②: no
    Serial constraints cleared: no in-flight card shares this file. Round-17 siblings fenced disjoint — #5710 (packages/types + packages/providers) and #5619 (packages/auth + app-shell + apps/console). Predecessor on this exact file, fa140b8 (#5590/PR #5636, the emptyText arm), is merged — this card is the remaining two keys of that same block.

    Tier basis. node scripts/pm/dispatch-gates.mjs --tier packages/components/src/renderers/basic/record-picker.tsx — run at dispatch — returns no path-derived mandate (floor sonnet · default opus · ceiling fable). Content judgement: the change widens two ComponentMeta input arms from type: 'string' to the I18nLabel union, which on its face touches an authorable declaration — but it inherits the already-cleared mother ruling #5590 on the same file and the same block, whose reason applies verbatim here: the declaration moves only in the change that makes the arm render, so this is declared = enforced restoration, not a new capability. Mother's reason re-checked on this branch rather than assumed, per the same-family rule. Clause ② therefore does not trip; tier stays at the default judgement档 because the fix carries i18n render semantics rather than being purely gate-decided.

    Premise re-verified before dispatch, on origin/main = 7138bc1. Triage's concentration-round first-touch (2026-08-22T13:12Z) settled both calls the card left open, and is carried into the brief as the ruling, not as a suggestion: (1) the two keys travel together in one PR on the settled emptyText shape, widening the two ComponentMeta arms in the same change and never before; (2) label takes the sibling pairing pickLocalized + translateLabel matching containers.tsx:569/:831; ⛔ toText itself stays untouched — it serves row values, a different contract, and the card's own analysis is adopted as the fence.


    Generated by Claude Code

  5. os-zhuang commented on Aug 22, 2026

    @os-zhuang
    Contributor
    {
      "issue": 5637,
      "status": "rework",
      "branch": "claude/issue-5637-record-picker-label-placeholder-i18n",
      "pr": "https://github.com/objectstack-ai/objectui/pull/5714",
      "premise_still_valid": true,
      "issue_body_complete": "YES — read in full plus both comments (triage first-touch 2026-08-22T13:12Z, and the round-17 claim). The body ends with the `What a fix would decide` section closing on the translateLabel pairing question and the attribution footer. Not truncated.",
      "summary": "Both keys now resolve at their own read site through pickLocalized: `label = pickLocalized(props.label, language)` and `placeholder = pickLocalized(props.placeholder ?? 'Select a record…', language)`, default-before-resolution matching the settled emptyText shape. toText is byte-unchanged (verified: the only diff line matching its body text is a new comment) and still serves the row values. The two ComponentMeta inputs widen from `type: 'string'` to `['string','object']` in this same change, with both descriptions rewritten from renderer behaviour. STATUS IS rework, NOT done, for ONE reason: the translateLabel half of the Zone-1 ruling is NOT implemented. translateLabel and KNOWN_LABEL_DICT are module-private to renderers/layout/containers.tsx, which the dispatch fences as hard out-of-scope ('every other renderer in packages/components'). Reaching them needs either an export from that fenced file plus a new basic/ -> layout/ static import between renderer families (a repo that budgets its eager import closure in bytes), or a hoist into a shared module across more out-of-fence files. Neither is mechanical, so I stopped at the fence and escalated rather than crossing it. All three MEASURED fixtures are fixed; only the pairing is outstanding.",
      "deviations_from_dispatch": [
        "PR body first line is `Part of #5637`, NOT the `Fixes #5637` the dispatch specified. Standing clause: use Part of when merging should not close the card. A binding ruling clause is unimplemented, so a merge that closed #5637 would close it with that clause silently open. One-line edit if PM rules otherwise. Flagged in the PR body itself as the first thing a reader sees.",
        "No new issue filed for the translateLabel gap — it stays on #5637, which `Part of` keeps open. Filing a second tracker would split it."
      ],
      "tests": "RED-FIRST (unfixed tree, new test file, viewer zh-CN) — `npx vitest run <new test>`: `Test Files 1 failed (1)` / `Tests 6 failed | 5 passed (11)`. All three card-measured modes reproduced verbatim: (1) placeholder map -> `Error: Objects are not valid as a React child (found: object with keys {en, zh-CN}).`; (2) label map -> `AssertionError: expected 'Owner' to be '负责人'`; (3) label map without `en` -> `AssertionError: expected null not to be null` (label element gone). The 5 greens are the controls + reachability. GREEN, all at 907ec3e64 (the tree of the PR's only commit): new test + 3 neighbours -> `Test Files 4 passed (4)` / `Tests 38 passed (38)`; `vitest run packages/components` -> `Test Files 182 passed (182)` / `Tests 1638 passed (1638)`. No ablation was run — a genuine red-first measurement against the unmutated pre-fix tree replaces it, so there is no mutation to confirm on disk and no rebuild leg to report. ASSERTION MECHANISM (dispatch Zone 3b): rendered output only. The registered renderer is invoked directly via ComponentRegistry.get inside an I18nProvider pinned to a language; the throw case is read as 'renders, and the trigger textContent contains the resolved string'; the language a viewer sees is `container.querySelector('label')?.textContent`, asserted for zh-CN AND en on the SAME map (an `.en` pick passes the English half alone); presence/absence is `expect(labelEl(container)).not.toBeNull()` asserted BEFORE its text, paired with a control that the element stays absent when `label` is absent.",
      "gates_run": [
        "pnpm --filter '@object-ui/components^...' build — exit 0 (first command in the new worktree)",
        "pnpm --filter @object-ui/components type-check — exit 0; script echoed `tsc --noEmit && tsc -p tsconfig.test.json`, so not a zero-match no-op",
        "eslint . in packages/components — `✖ 909 problems (0 errors, 909 warnings)`, 396 files (count read from --format json). All 909 pre-existing; my two files contribute 0 errors",
        "vitest run packages/components — `Test Files 182 passed (182)` / `Tests 1638 passed (1638)`",
        "apps/console declaration-parity tests (component-input-union-specimens, registry-inputs-spec-parity, ga-honoured-inputs-author-reach, public-contract) — `Test Files 4 passed (4)` / `Tests 100 passed (100)`. RUN, never edited: the widening needed no out-of-fence change, unlike #5590 which had to flip a console specimen",
        "pnpm check:control-bytes — `✅ check-control-bytes: OK (scanned 4767 tracked text file(s); skipped 85 binary).`",
        "pnpm check:i18n-keys — `Every in-scope call-site key resolves against the en pack (2924 keys) …`",
        "pnpm check:i18n-drift — `No en value changed in this range.`",
        "pnpm check:i18n-dead-keys — report-only by its own header, exit 0",
        "pnpm check:doc-types — `✅ Every documented component type is registered.`",
        "pnpm check:spec-symbols — `✅ spec symbol derivation: 1294 files scanned against 4966 spec export names`",
        "pnpm changeset:check — `✅ No changeset declares a 'major' bump.`"
      ],
      "declared_narrowing": "pnpm lint is `turbo run lint` over 42 packages with a lint script; I ran @object-ui/components only. Three pieces of evidence, per the narrowing rule: (1) population read from eslint's own config — that package's whole `eslint .`, its entire file set, not a diff-derived subset; (2) file count 396, read from `--format json` output length, not estimated; (3) invariance — eslint.config.js declares no `project`/`projectService` (no type-aware linting), and no rule under eslint-rules/ reads any file other than the one it lints, so a change confined to packages/components cannot move a verdict in another package's run. CI runs the full farm regardless. NOT narrowed anywhere else: the full components suite and every dispatch-named gate ran unrestricted.",
      "files_changed": [
        "packages/components/src/renderers/basic/record-picker.tsx (modified — read sites, prop types, two ComponentMeta inputs arms + descriptions; toText body untouched)",
        "packages/components/src/renderers/basic/__tests__/record-picker-label-placeholder-i18n.test.tsx (new, 11 cases — new directory, matching existing renderers/action|complex|form/__tests__/, which is the test surface the fence names; #5590 used packages/components/src/__tests__/ instead. Both are collected by the dom project's packages/**/*.test.tsx glob)",
        ".changeset/record-picker-label-placeholder-i18n-5637.md (new, patch on @object-ui/components)"
      ],
      "pm_assumptions_measured": [
        "1. MOTHER LANDED — CONFIRMED. fa140b8 is in origin/main = 7138bc1; its diff was read before writing and this change is deliberately the same move (same helper, same default-before-resolution ordering, same declare-in-the-same-change rule, same comment register).",
        "2. LINE NUMBERS DRIFTED — CONFIRMED and re-derived. The card's `:164` and `:206` are both stale. Current tree before my change: label read at :172, placeholder at :173, row-value toText at :229. Not one card line number is cited in the PR. Two the card gives are still accurate: containers.tsx:569 and :831 still hold the sibling pairing.",
        "3. THE THREE FIXTURES — CONFIRMED, all three reproduced red before the fix (quoted above) and all three pinned. Assumption 3's framing is exactly right: it is three modes, not one, and the two silent ones are the reason a throw-only fix would be half a fix.",
        "4. toText IS SHARED — CONFIRMED, not routed around on faith. It has exactly two call sites in this file: the label read (which I moved off it) and `toText(row?.[labelField])` at what is now :229, which renders record FIELD values. Left untouched.",
        "DRIFT FOUND: the installed spec pin is @objectstack/spec 17.1.0, not the 17.0.0 GA the card and #5590 both state. The union is unchanged on all three keys (measured: optional -> union -> [string | record]; map safeParse OK on each; label:42 REJECTED), so the premise holds — but the version both documents cite is stale."
      ],
      "open_questions": [
        {
          "question": "The Zone-1 ruling says `label` takes the sibling pairing pickLocalized + translateLabel. translateLabel and its KNOWN_LABEL_DICT are module-private to renderers/layout/containers.tsx, which the dispatch fences as hard out-of-scope. The ruling is not re-adjudicable but it is not reachable from inside the fence either. Which gives?",
          "options": [
            "A — Widen the fence for a follow-up patch on this same claim: export translateLabel from containers.tsx and import it in record-picker.tsx. Smallest diff (one keyword + one import), but it adds a basic/ -> layout/ static import between renderer families in a repo that budgets the eager import closure in bytes (scripts/check-eager-closure-budget.mjs). Both are already pulled by the renderers barrel, so the closure likely does not move — but basic/ is separately importable, so the coupling is real for narrower entry points.",
            "B — Hoist translateLabel + KNOWN_LABEL_DICT into @object-ui/i18n as a shared export, then apply the pairing at all three sites. Architecturally the honest home if the pairing is meant to generalise to author-supplied labels; a larger change across packages/i18n and containers.tsx, and it deserves its own card.",
            "C — Ship the pickLocalized half (this PR) and re-scope the pairing clause to a separate card, letting #5637 close on the defect it measured."
          ],
          "recommendation": "C, with a measurement PM should weigh before choosing A or B for the follow-up. translateLabel is not a general locale resolver: it is a hardcoded dictionary of well-known TAB tokens (Details, Related, Notes, Files, …) with entries for zh-CN and zh-TW ONLY, and both live call sites translate labels that buildDefaultTabs GENERATED rather than ones an author wrote. Applying it to record_picker.label means the platform silently rewrites an author's literal English string — and only for Chinese viewers. Open card #4645 (pm:on-hold) already records that same dictionary leaving built-in tabs untranslated in ja-JP and es-ES while zh-CN is fine, so extending its reach would widen a surface that is already known-incomplete. None of that re-opens the ruling; it is the input the fence decision needs, and it is why I did not cross the fence on my own judgement."
        }
      ],
      "out_of_scope_findings": [
        "filed as #5717: element:text_input's label / placeholder / description are the INVERSE of this card — text-input.tsx:96-98 already resolves all three through pickLocalized, the spec contract for all three is the I18nLabel union (measured on 17.1.0), but the ComponentMeta entries still declare a single 'string' arm, so validateTree reports `type-mismatch :: <element:text_input> prop \"label\" expected a string` on a legal write the renderer renders correctly. Measured through the same manifestFromConfigs + validateTree path the console specimen test uses. Dedupe-searched twice before filing (no hit); explicitly distinguished from the component-input-union-specimens.test.ts:214 control on element:text_input.defaultValue, whose type-mismatch on a map is CORRECT and must stay. Filed unassigned, unlabelled, unqueued.",
        "NOT filed — scanned and dropped: other renderers reading a placeholder into a React child position (notably renderers/form/date-picker.tsx:53, `<span>{schema.placeholder || 'Pick a date'}</span>`). Those read a form-field node schema, and I could not establish within bounds that the key's contract admits the I18nLabel union — no form/field spec schema I probed carries a top-level placeholder. Reporting the candidate rather than filing an unmeasured card. Note #4163, the original 'audit every I18nLabel read the compiler cannot see' card, closed as completed on 2026-08-15 while this gap was open and has no open successor; whether to re-open that audit is a triage call, not mine."
      ],
      "resource_notes": "Every build/test went through scripts/pm/os-verify-lock.sh (objectstack's; objectui has no copy). Longest hold: 7m03s for the full components suite. Longest wait: 8m09s on the last probe — siblings were busy; waited in-turn with no-lock work in between, never ended a turn on a queue. Worktree removed without --force after the PR opened; node_modules deleted first. No repo-wide scan run locally (the 42-package turbo lint is CI's; see declared_narrowing)."
    }

    Generated by Claude Code

  6. os-zhuang commented on Aug 22, 2026

    @os-zhuang
    Contributor

    os-dev-report

    Supersedes the previous comment — the GitHub body sanitizer ate its HTML-comment marker (read back: the body now begins with a blank line, no marker), which would make it invisible to a marker-based scan. Posted again with the marker as literal text, per the fallback rule. Three short angle-bracket fragments were eaten from the JSON in that copy too and are respelled here. No content changes otherwise.

    {
      "issue": 5637,
      "status": "rework",
      "branch": "claude/issue-5637-record-picker-label-placeholder-i18n",
      "pr": "https://github.com/objectstack-ai/objectui/pull/5714",
      "premise_still_valid": true,
      "issue_body_complete": "YES — read in full plus both comments (triage first-touch 2026-08-22T13:12Z, and the round-17 claim). The body ends with the `What a fix would decide` section closing on the translateLabel pairing question and the attribution footer. Not truncated.",
      "summary": "Both keys now resolve at their own read site through pickLocalized: `label = pickLocalized(props.label, language)` and `placeholder = pickLocalized(props.placeholder ?? 'Select a record…', language)`, default-before-resolution matching the settled emptyText shape. toText is byte-unchanged (verified: the only diff line matching its body text is a new comment) and still serves the row values. The two ComponentMeta inputs widen from `type: 'string'` to `['string','object']` in this same change, with both descriptions rewritten from renderer behaviour. STATUS IS rework, NOT done, for ONE reason: the translateLabel half of the Zone-1 ruling is NOT implemented. translateLabel and KNOWN_LABEL_DICT are module-private to renderers/layout/containers.tsx, which the dispatch fences as hard out-of-scope ('every other renderer in packages/components'). Reaching them needs either an export from that fenced file plus a new basic/ -> layout/ static import between renderer families (a repo that budgets its eager import closure in bytes), or a hoist into a shared module across more out-of-fence files. Neither is mechanical, so I stopped at the fence and escalated rather than crossing it. All three MEASURED fixtures are fixed; only the pairing is outstanding.",
      "deviations_from_dispatch": [
        "PR body first line is `Part of #5637`, NOT the `Fixes #5637` the dispatch specified. Standing clause: use Part of when merging should not close the card. A binding ruling clause is unimplemented, so a merge that closed this card would close it with that clause silently open. One-line edit if PM rules otherwise. Flagged in the PR body itself as the first thing a reader sees.",
        "No new issue filed for the translateLabel gap — it stays on this card, which `Part of` keeps open. Filing a second tracker would split it."
      ],
      "tests": "RED-FIRST (unfixed tree, new test file, viewer zh-CN) — vitest run over the new test file: `Test Files 1 failed (1)` / `Tests 6 failed | 5 passed (11)`. All three card-measured modes reproduced verbatim: (1) placeholder map -> `Error: Objects are not valid as a React child (found: object with keys {en, zh-CN}).`; (2) label map -> `AssertionError: expected 'Owner' to be '负责人'`; (3) label map without `en` -> `AssertionError: expected null not to be null` (label element gone). The 5 greens are the controls + reachability. GREEN, all at 907ec3e64 (the tree of the PR's only commit): new test + 3 neighbours -> `Test Files 4 passed (4)` / `Tests 38 passed (38)`; `vitest run packages/components` -> `Test Files 182 passed (182)` / `Tests 1638 passed (1638)`. No ablation was run — a genuine red-first measurement against the unmutated pre-fix tree replaces it, so there is no mutation to confirm on disk and no rebuild leg to report. ASSERTION MECHANISM (dispatch Zone 3b): rendered output only. The registered renderer is invoked directly via ComponentRegistry.get inside an I18nProvider pinned to a language; the throw case is read as 'renders, and the trigger textContent contains the resolved string'; the language a viewer sees is `container.querySelector('label')?.textContent`, asserted for zh-CN AND en on the SAME map (an `.en` pick passes the English half alone); presence/absence is `expect(labelEl(container)).not.toBeNull()` asserted BEFORE its text, paired with a control that the element stays absent when `label` is absent.",
      "gates_run": [
        "pnpm --filter '@object-ui/components^...' build — exit 0 (first command in the new worktree)",
        "pnpm --filter @object-ui/components type-check — exit 0; script echoed `tsc --noEmit && tsc -p tsconfig.test.json`, so not a zero-match no-op",
        "eslint . in packages/components — `✖ 909 problems (0 errors, 909 warnings)`, 396 files (count read from --format json). All 909 pre-existing; my two files contribute 0 errors",
        "vitest run packages/components — `Test Files 182 passed (182)` / `Tests 1638 passed (1638)`",
        "apps/console declaration-parity tests (component-input-union-specimens, registry-inputs-spec-parity, ga-honoured-inputs-author-reach, public-contract) — `Test Files 4 passed (4)` / `Tests 100 passed (100)`. RUN, never edited: the widening needed no out-of-fence change, unlike #5590 which had to flip a console specimen",
        "pnpm check:control-bytes — `✅ check-control-bytes: OK (scanned 4767 tracked text file(s); skipped 85 binary).`",
        "pnpm check:i18n-keys — `Every in-scope call-site key resolves against the en pack (2924 keys) …`",
        "pnpm check:i18n-drift — `No en value changed in this range.`",
        "pnpm check:i18n-dead-keys — report-only by its own header, exit 0",
        "pnpm check:doc-types — `✅ Every documented component type is registered.`",
        "pnpm check:spec-symbols — `✅ spec symbol derivation: 1294 files scanned against 4966 spec export names`",
        "pnpm changeset:check — `✅ No changeset declares a 'major' bump.`"
      ],
      "declared_narrowing": "pnpm lint is `turbo run lint` over 42 packages with a lint script; I ran @object-ui/components only. Three pieces of evidence, per the narrowing rule: (1) population read from eslint's own config — that package's whole `eslint .`, its entire file set, not a diff-derived subset; (2) file count 396, read from `--format json` output length, not estimated; (3) invariance — eslint.config.js declares no `project`/`projectService` (no type-aware linting), and no rule under eslint-rules/ reads any file other than the one it lints, so a change confined to packages/components cannot move a verdict in another package's run. CI runs the full farm regardless. NOT narrowed anywhere else: the full components suite and every dispatch-named gate ran unrestricted.",
      "files_changed": [
        "packages/components/src/renderers/basic/record-picker.tsx (modified — read sites, prop types, two ComponentMeta inputs arms + descriptions; toText body untouched)",
        "packages/components/src/renderers/basic/__tests__/record-picker-label-placeholder-i18n.test.tsx (new, 11 cases — new directory, matching existing renderers/action|complex|form/__tests__/, which is the test surface the fence names; #5590 used packages/components/src/__tests__/ instead. Both are collected by the dom project's packages/**/*.test.tsx glob)",
        ".changeset/record-picker-label-placeholder-i18n-5637.md (new, patch on @object-ui/components)"
      ],
      "pm_assumptions_measured": [
        "1. MOTHER LANDED — CONFIRMED. fa140b8 is in origin/main = 7138bc1; its diff was read before writing and this change is deliberately the same move (same helper, same default-before-resolution ordering, same declare-in-the-same-change rule, same comment register).",
        "2. LINE NUMBERS DRIFTED — CONFIRMED and re-derived. The card's `:164` and `:206` are both stale. Current tree before my change: label read at :172, placeholder at :173, row-value toText at :229. Not one card line number is cited in the PR. Two the card gives are still accurate: containers.tsx:569 and :831 still hold the sibling pairing.",
        "3. THE THREE FIXTURES — CONFIRMED, all three reproduced red before the fix (quoted above) and all three pinned. Assumption 3's framing is exactly right: it is three modes, not one, and the two silent ones are the reason a throw-only fix would be half a fix.",
        "4. toText IS SHARED — CONFIRMED, not routed around on faith. It has exactly two call sites in this file: the label read (which I moved off it) and `toText(row?.[labelField])` at what is now :229, which renders record FIELD values. Left untouched.",
        "DRIFT FOUND: the installed spec pin is @objectstack/spec 17.1.0, not the 17.0.0 GA the card and #5590 both state. The union is unchanged on all three keys (measured: optional -> union -> string | record; map safeParse OK on each; label:42 REJECTED), so the premise holds — but the version both documents cite is stale."
      ],
      "open_questions": [
        {
          "question": "The Zone-1 ruling says `label` takes the sibling pairing pickLocalized + translateLabel. translateLabel and its KNOWN_LABEL_DICT are module-private to renderers/layout/containers.tsx, which the dispatch fences as hard out-of-scope. The ruling is not re-adjudicable but it is not reachable from inside the fence either. Which gives?",
          "options": [
            "A — Widen the fence for a follow-up patch on this same claim: export translateLabel from containers.tsx and import it in record-picker.tsx. Smallest diff (one keyword + one import), but it adds a basic/ -> layout/ static import between renderer families in a repo that budgets the eager import closure in bytes (scripts/check-eager-closure-budget.mjs). Both are already pulled by the renderers barrel, so the closure likely does not move — but basic/ is separately importable, so the coupling is real for narrower entry points.",
            "B — Hoist translateLabel + KNOWN_LABEL_DICT into @object-ui/i18n as a shared export, then apply the pairing at all three sites. Architecturally the honest home if the pairing is meant to generalise to author-supplied labels; a larger change across packages/i18n and containers.tsx, and it deserves its own card.",
            "C — Ship the pickLocalized half (this PR) and re-scope the pairing clause to a separate card, letting this one close on the defect it measured."
          ],
          "recommendation": "C, with a measurement PM should weigh before choosing A or B for the follow-up. translateLabel is not a general locale resolver: it is a hardcoded dictionary of well-known TAB tokens (Details, Related, Notes, Files, …) with entries for zh-CN and zh-TW ONLY, and both live call sites translate labels that buildDefaultTabs GENERATED rather than ones an author wrote. Applying it to record_picker.label means the platform silently rewrites an author's literal English string — and only for Chinese viewers. Open card #4645 (pm:on-hold) already records that same dictionary leaving built-in tabs untranslated in ja-JP and es-ES while zh-CN is fine, so extending its reach would widen a surface that is already known-incomplete. None of that re-opens the ruling; it is the input the fence decision needs, and it is why I did not cross the fence on my own judgement."
        }
      ],
      "out_of_scope_findings": [
        "filed as #5717: element:text_input's label / placeholder / description are the INVERSE of this card — text-input.tsx:96-98 already resolves all three through pickLocalized, the spec contract for all three is the I18nLabel union (measured on 17.1.0), but the ComponentMeta entries still declare a single 'string' arm, so validateTree reports a `type-mismatch` diagnostic reading `prop \"label\" expected a string` on a legal write the renderer renders correctly. Measured through the same manifestFromConfigs + validateTree path the console specimen test uses. Dedupe-searched twice before filing (no hit); explicitly distinguished from the component-input-union-specimens.test.ts:214 control on element:text_input.defaultValue, whose type-mismatch on a map is CORRECT and must stay. Filed unassigned, unlabelled, unqueued.",
        "NOT filed — scanned and dropped: other renderers reading a placeholder into a React child position (notably renderers/form/date-picker.tsx:53, which puts `schema.placeholder || 'Pick a date'` inside a span element). Those read a form-field node schema, and I could not establish within bounds that the key's contract admits the I18nLabel union — no form/field spec schema I probed carries a top-level placeholder. Reporting the candidate rather than filing an unmeasured card. Note #4163, the original 'audit every I18nLabel read the compiler cannot see' card, closed as completed on 2026-08-15 while this gap was open and has no open successor; whether to re-open that audit is a triage call, not mine."
      ],
      "resource_notes": "Every build/test went through scripts/pm/os-verify-lock.sh (objectstack's; objectui has no copy). Longest hold: 7m03s for the full components suite. Longest wait: 8m09s on the last probe — siblings were busy; waited in-turn with no-lock work in between, never ended a turn on a queue. Worktree removed without --force after the PR opened; node_modules deleted first. No repo-wide scan run locally (the 42-package turbo lint is CI's; see declared_narrowing)."
    }

    Generated by Claude Code

  7. os-zhuang commented on Aug 22, 2026

    @os-zhuang
    Contributor

    ACCEPT on the measured defect — PR #5714 enqueued · reviewer of record, domain:ui seat, round 17

    Verdict differs from the dev's self-report, deliberately. It filed status: rework; I am recording ACCEPT on the measured defect, with the ruling's second clause carved out to a decision — because nothing in the delivered work is defective. What is outstanding is a ruled clause that is unreachable from inside the fence, which is a scoping question, not rework.

    Gates read by name at head 907ec3e64: 19 success + 3 skipped (dependabot, Test (coverage), the unexpanded shard placeholder) = 22. Green. Path surface is 3 files (record-picker.tsx, its new test, changeset) — no governed path. Flipped ready, auto-merge SQUASH enabled. ⚠️ The PR's first line is Part of #5637, so merging will not close this card — that is correct and deliberate, and the body is left untouched rather than round-tripped through this tooling.

    Verified against the tree, not the report: toText's body is byte-unchanged (the only removed line is the read site; toText(row?.[labelField]) still serves row values); both ComponentMeta arms widen 'string' → ['string','object'] in the same commit as the renderer change, which is the order the ruling required. The test carries non-vacuity controls in both directions — not.toBeNull() asserted before textContent, paired with a control that the element stays absent when label is absent — and asserts the same fixture under two languages, which is what makes an .en pick (the actual bug) impossible to pass.


    The carve-out: clause 2 is unreachable, and the measurement argues against it

    Triage ruled label takes the sibling pairing pickLocalized + translateLabel. ⛔ I am not overruling that, and this comment does not re-adjudicate it. I am reporting that implementing it requires a decision neither triage nor this seat may make, and routing it accordingly.

    Why it is unreachable: translateLabel and KNOWN_LABEL_DICT are module-private to renderers/layout/containers.tsx, which this card's fence marks hard out-of-scope. Reaching them needs either an export from a fenced file plus a new basic/ → layout/ static import between renderer families, or a hoist into a shared module across more out-of-fence files. The dev stopped at the fence and escalated instead of crossing it — the correct move.

    Three measurements that were not available when clause 2 was written:

    1. translateLabel is not a locale resolver. It is a hardcoded dictionary of well-known tab tokens (Details, Related, Notes, Files, …) with entries for zh-CN and zh-TW only.
    2. Both live call sites translate labels that buildDefaultTabs generated. A record_picker.label is author-supplied — so the pairing would mean the platform silently rewrites an author's literal string, and only for Chinese viewers.
    3. console i18n residual on 17.0.0 GA: built-in record-detail tabs untranslated in ja-JP / es-ES (zh-CN fine), plus two English aria-labels in every locale #4645 (pm:on-hold) already records this same dictionary leaving built-in tabs untranslated in ja-JP and es-ES while zh-CN is fine. Extending its reach widens a surface already recorded as incomplete and already parked.

    The consistency argument behind clause 2 is real — same key family, same read-site treatment. What the measurement shows is that the two sibling sites and this one are not the same case: generated tokens versus authored text.


    四维分析(业务角度)

    一句话问题:作者在 record picker 上手写了英文 label「Owner」,中文用户应该看到「Owner」,还是平台自作主张把它改写成「负责人」?

    选项 客户可感知的代价
    A 从 containers.tsx 导出 translateLabel,在 record-picker 里用 中文用户看到被平台改写的 label;日语/西语用户看到原文。作者无法关掉。另加一条 basic/ → layout/ 静态导入,而 eager closure 预算目前只剩 1.9% 余量
    B 把 translateLabel + 词表搬进 @object-ui/i18n,三处统一用 同 A 的语义后果,但架构诚实;且必须先补全语言(#4645 的债),是一张独立的大卡
    C 不把 pairing 用到作者手写的 label 上,本条款按「不适用」收口 中文用户看到作者写的原文。想要多语言的作者改写 inline locale map —— 而那正是本 PR 刚修好的能力
    • 实际业务需求:这个键是作者手写的,不是平台生成的。作者写「Owner」的意图通常就是让所有人看到「Owner」;真要多语言,spec 已经给了 inline locale map 这条声明式的路(本 PR 刚把它修通)。查表改写解决的是"作者没声明多语言",而那个场景的正解是让作者去声明。
    • 项目长远合理性:平台在这件事上已经选边了 —— 声明式(作者写 map,平台解析)而不是猜测式(平台查表改写字面量)。把猜测式推到新表面,是往回走一步,而且是往一个已知不完整的表上走。
    • 防 AI 写错:AI 产出 metadata 时,如果平台会悄悄改写字面量,AI 写下的和用户看到的就不一致,且不可预测(取决于是否命中那张表)。inline locale map 是 AI 能稳定产对的形状;查表改写是它无法预料的。
    • 创业阶段不扩散:console i18n residual on 17.0.0 GA: built-in record-detail tabs untranslated in ja-JP / es-ES (zh-CN fine), plus two English aria-labels in every locale #4645 已经因为这张表不完整被 hold 了。给一个 on-hold 的债扩大作用面,是在没有拉动的情况下扩散需求。

    推荐 C,回退 B(若确实要泛化,先把词表搬进 i18n 并补全语言,再谈应用面)。

    本分析看不见什么:真实租户里 record_picker.label 的实际写法分布。如果绝大多数作者其实写的是英文单词、并且期待平台本地化,那 A/B 的价值比我估的高 —— 这一点我在仓内测不到。


    State transition, in the same write as this comment: pm:dispatched → needs-user-decision, assignee cleared. The delivered work is done and enqueued; what remains on this card is the decision above. ⛔ The card does not close when #5714 merges.

    Out-of-scope finding verified: #5717 filed — element:text_input is the exact inverse of this card (renderer already resolves all three keys through pickLocalized, but ComponentMeta still declares a single 'string' arm, so validateTree reports a type-mismatch on a legal write the renderer renders correctly). Dedupe-searched twice, and explicitly distinguished from the element:text_input.defaultValue control whose type-mismatch is correct and must stay.

    ⚠️ One item for triage, not actioned here: the dev reports that #4163 — the original "audit every I18nLabel read the compiler cannot see" card — closed as completed on 2026-08-15 while this gap was open, and has no open successor. Two cards in two rounds have now come out of that supposedly-closed audit. Whether to re-open it is triage's call.

    Drift worth fixing at the source: the installed pin is @objectstack/spec 17.1.0, not the 17.0.0 GA that both this card and #5590 state. The union is unchanged on all three keys, so the premise held — but the version both documents cite is stale.


    Generated by Claude Code

  8. removed their assignment
    on Aug 22, 2026
  9. os-zhuang commented on Aug 23, 2026

    @os-zhuang
    Contributor

    Maintainer ruling recorded (2026-08-23, live PM chat, verbatim: 「其他同意你的意见」— adopting recommendation C): the translateLabel pairing clause is ruled NOT APPLICABLE to author-supplied labels. An author's literal string is rendered as written; authors who want localization declare an inline locale map — the capability PR #5714 delivered (merged). The dictionary stays scoped to generated tab tokens; its language-completeness debt remains parked on #4645, whose reach is deliberately NOT widened.

    Card closes completed (same stroke): the measured defect landed via PR #5714; clause 2 is resolved by this ruling with zero further code change. For the next reader: this closure supersedes the earlier Zone-1 clause-2 wording — do not re-open the pairing for record_picker.label without a new ruling. The #4163 audit-gap observation (two cards in two rounds out of a closed audit) stays with the triage seat: a third instance triggers a fresh audit card.


    Generated by Claude Code

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatneeds-user-decision

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions