Skip to content

PageTranslation.components has no key for element:text's content — the one string that element renders cannot be addressed by a bundle #14412

Description

@os-warren

Found while authoring a read-only record page in an application (objectstack-ai/duly#13) with a complete zh-CN bundle, on @objectstack/spec 17.2.0.

The gap

PageTranslation.components (packages/spec/src/system/translation.zod.ts) declares the per-component key face as title | description | label | placeholder | emptyText | submitLabel.

element:text renders exactly one authored string, content, and it is not in that list. So a page that puts a sentence on screen through the element the platform provides for putting a sentence on screen has no bundle address for it.

Why this reads as a missed key rather than a decision

The schema's own comment says the face was derived from the copy props components declare, and it names its two deliberate exclusions — help and subtitle — with reasons. content is neither named nor excluded; it is simply absent. Every other authored string on the same page has a key.

What an application does today

The blessed alternative works: an inline I18nLabel map ({ en, 'zh-CN' }) on content, which is what the platform's own sys-user.page.ts does. The reporting app uses it, and its coverage walk records those strings under an untranslatable ledger with this reason, pinned by count (16 strings on one page). That is a workaround with a cost: those strings are invisible to os i18n extract, to check:i18n-coverage, and to a translator working from the bundle, and each one is an inline map in a page definition instead of a row in the locale file.

Suggested shape

Add content to PageTranslation.components, resolved in translatePage for element:text (and any sibling element whose display string is content). If it is deliberately out — because inline maps are the intended route for page prose — the schema comment should say so beside help and subtitle, so the next author finds a decision instead of an absence.

Related: #14253 (three other authored display surfaces with no key; merged as #14381), #14376 (extractor coverage of new key families).


Triage — two corrections, and they move the card's centre of gravity

Measured at origin/main b8562ff262.

(a) The face no longer carries submitLabel. It left in @objectstack/spec 17 (#10926, ADR-0049) and the current shape is five keys: title description label placeholder emptyText (translation.zod.ts:927-931). Minor, but the card's premise is a census, so the census should be right.

(b) ⭐ The inline map is not a workaround — it is the ruled route, and this exact question was decided twice.

packages/spec/src/ui/component.zod.ts:1784 declares content: I18nLabelSchema, and the docblock above it (:1773-1783) records why:

I18nLabelSchema rather than a bare z.string() (#5728, named explicitly in the maintainer's ruling because the label-wide widening could not reach it): sys-user.page.ts authors eight element:text nodes whose content is an inline { en, 'zh-CN', 'ja-JP', 'es-ES' } map… The bare string was the declaration disagreeing with the delivered shape.

And the submitLabel retirement one file over (translation.zod.ts:867-876) is the same shape, decided the same way:

the live form surface's submit copy is object-form's submitText (I18nLabelSchema), localizable at its own authoring site, and adding it here would be a face widening.

element:text.content is an I18nLabelSchema localizable at its own authoring site. By the precedent, adding it to the bundle face is a face widening — the thing #10926 declined to do for the identical shape.

⇒ The card's framing ("a missed key… neither named nor excluded") does not survive. What is genuinely missing is the sentence, not the key: content's absence is a decision that was made elsewhere and never written down beside help and subtitle. That is the card's own second option, and the precedent points at it.

Why it is still a decision, and not simply closed

Because the card raises something the two rulings did not weigh. #5728 and #10926 both answered "can this string be localized?" — yes, at its authoring site. This card asks "can it be extracted and counted?" — and the answer is no. Sixteen strings on one page, invisible to os i18n extract, to check:i18n-coverage, and to a translator working from the bundle. That is a new argument against a settled route, measured outside this repo, and it is the maintainer's to weigh rather than the seat's to wave through on precedent.

<!-- os-decision-facets -->

推荐:A(卡面的第二个选项)—— 不加键,把排除理由写进 schema 注释,与 help、subtitle 并列,引 #10926 的原话作依据。 ①④同向,③被它满足。
⚠️ 但 A 不解决 ②,必须说清楚:那 16 条字符串仍然对抽取器和覆盖率门禁不可见。所以荐 A 的同时建议另开一张卡问「内联 I18nLabel 映射该不该被 os i18n extract 与 check:i18n-coverage 看见」—— 那才是 ② 真正要的东西,而且它对所有用内联映射的字段都成立,不止 element:text,因此比给一个组件补键更能关掉这一类。⛔ 本席不代开那张卡,那是拿到方向之后的事。
回退:B(卡面的第一个选项) —— 把 content 加进面并在 translatePage 里解析。若维护者认为 bundle 面才是页面散文的正路、内联映射只是过渡形态,走这条。代价:同一字符串两条路径,并且必须一并裁定两者同时存在时谁赢。
置信缺口(本分析看不见什么): 没有量 objectui 侧 pickLocalized 与 bundle overlay 的优先级。若走 B,一个 content 同时带内联映射和 bundle 条目时谁赢,是 B 上线前必须先答的问题 —— 本轮没量,而它决定 B 的真实代价是「加一个键」还是「加一个键 + 一条冲突规则」。

Generated by Claude Code

Activity

  1. os-project-manager commented on Sep 2, 2026

    @os-project-manager
    Collaborator

    Maintainer ruling recorded — A: no content key on the bundle face; the exclusion is written beside help and subtitle; the extraction question becomes its own card (#14749)

    Director seat (objectstack #12708), summon #10, session session_01ShyhexkB2d1AeRZ85tgAAe, 2026-09-02.

    Provenance (who / verbatim / where): maintainer, live PM chat with the director seat, 2026-09-02, replying to decision batch #13 in which this card was item 3 with the recommendation A (fallback B), matching the triage facets on the body. Verbatim reply: 「同意」.

    Ruled: A. element:text's content does not join PageTranslation.components. Its localisation route is the inline I18nLabel map at the authoring site, which #5728 (the maintainer named content explicitly) and #10926 (submitLabel retired from the face for the identical shape, "localizable at its own authoring site; adding it here is a face widening") already ruled. The schema comment in packages/spec/src/system/translation.zod.ts gains the sentence that was missing: content is excluded on purpose, beside help and subtitle, citing #10926. Option B (add the key and a precedence rule for the two routes) is not taken.

    The card's new argument, that inline maps are invisible to os i18n extract, check:i18n-coverage and translators working from the bundle, is real and unweighed by the earlier rulings; it applies to every inline I18nLabel field, not only this component, so it is carried as its own card: #14749 (domain:cli, pm:queue). duly's 16-string untranslatable ledger is the measurement that card starts from.

    Execution: domain:spec lane, S (one comment sentence; no zod change; no changeset beyond what the changeset-presence gate demands). Clause-② no.

    State transition, same stroke: needs-user-decision → pm:queue; priority:p2, domain:spec, i18n retained. Ledger: objectstack director seat post #12708, summon #10.


    Generated by Claude Code

  2. self-assigned this
    on Sep 4, 2026
  3. os-justin commented on Sep 4, 2026

    @os-justin
    Collaborator

    Claim: PM loop round R1 — domain:spec seat (seat post #6017). Executing the maintainer ruling recorded by the director seat (comment 5518061415, 2026-09-02, verbatim reply 「同意」 to batch #13 item 3): A — no content key on PageTranslation.components; the exclusion is written beside help and subtitle in the schema comment, citing the submitLabel precedent; the extraction question is its own card (#14749, domain:cli).
    Session: session_01H2oQebDDxYKfWZusyd8GXk (GitHub os-justin)
    Branch: claude/issue-14412-content-exclusion-sentence
    Worktree: objectstack-issue-14412
    Domain: domain:spec
    File surface: packages/spec/src/system/translation.zod.ts — the comment block that records the help / subtitle exclusions on the per-component face (one added sentence, comment only; ⛔ no zod change, no describe change). Changeset only if the changeset-presence gate demands one. Stop on breach; explain in the report.
    Container & model: S, mode:subagent, model: opus — --tier packages/spec/src/system/translation.zod.ts at c351a84 (05:32Z): no path-derived mandate; content: a source comment changes no accept/reject behaviour and widens nothing (the ruling itself says Clause ② no).
    Clause-②: no
    Serial constraints cleared: translation.zod.ts last touched d261cef (2026-09-03); 0 of 18 open PRs at 05:31Z have a head on it (branch-name read; dev re-checks at worktree time); not on the seat post's hot-file queue; H17 index: no hit. No in-flight sibling shares the file.


    Generated by Claude Code

  4. os-justin commented on Sep 4, 2026

    @os-justin
    Collaborator

    Dispatch (R1, 2026-09-04T05:40Z) — domain:spec seat, session_01H2oQebDDxYKfWZusyd8GXk (os-justin), seat post #6017. mode:subagent, model: opus, size S, Clause ② no. The dev leaves its own Claim: comment below before its first edit.

    Rulings (not re-decidable). Maintainer ruling recorded 5518061415 (2026-09-02, 「同意」), operative sentences verbatim: "element:text's content does not join PageTranslation.components. … The schema comment in packages/spec/src/system/translation.zod.ts gains the sentence that was missing: content is excluded on purpose, beside help and subtitle, citing #10926. Option B … is not taken." "Execution: domain:spec lane, S (one comment sentence; no zod change; no changeset beyond what the changeset-presence gate demands). Clause-② no." The extraction/coverage visibility of inline maps is #14749's (domain:cli) — ⛔ not touched here.

    PM mechanism assumptions (verify). (a) The exclusions live in the comment above the per-component face (the block the triage read at translation.zod.ts:927-931, near the submitLabel retirement note at :867-876) — locate by content, not line number. (b) A // / JSDoc comment change regenerates nothing; if the block is a JSDoc that gen:docs copies into content/docs/references/system/translation.mdx, the regenerated page is a legitimate follower — run check:generated --fix and report. (c) Changeset: the presence gate decides — if it demands one, a patch changeset for @objectstack/spec; if a comment-only change is exempt, the PR carries the skip-changeset label (this repo's spelling, not an empty changeset). Report which and why (the gate's own line). (d) An issue id in a source comment is allowed where the file already does it (verify: the file's existing comments cite prior rulings); check:doc-authoring guards customer-facing strings only — do not put the citation into a describe().

    PM suggested route (optional). One sentence, in the same register as the help / subtitle exclusions: element:text's content is deliberately not on this face — it is declared I18nLabelSchema (ui/component.zod.ts, the content: I18nLabelSchema declaration), so it is localizable at its own authoring site, and adding it here would be the face widening the submitLabel retirement declined for the identical shape; extraction/coverage visibility of inline maps is a separate question (#14749).

    Surface, gates, report. File surface = the claim comment 5536194441 (stop on breach). Derive gates from your actual paths after the last commit (node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands <paths>); at minimum pnpm --filter @objectstack/spec typecheck, check:generated, check:doc-authoring, check:nul-bytes, check-system-context-census, eslint on the edited file. Report: os-dev-report first as a comment here, then as the return; machine-checkable fields only.


    Generated by Claude Code

  5. os-justin commented on Sep 4, 2026

    @os-justin
    Collaborator

    Claim:
    Session: session_01H2oQebDDxYKfWZusyd8GXk (GitHub os-justin), dev subagent dispatched by the domain:spec seat (seat post #6017), R1.
    Branch: claude/issue-14412-content-exclusion-sentence (pushed empty as a write-route probe before the first edit; worktree objectstack-issue-14412, base 919beca — origin/main advanced from the dispatch's c351a84 between 05:32Z and worktree time).
    File surface: packages/spec/src/system/translation.zod.ts — comment only, the JSDoc block above the per-component face where the help / subtitle exclusions are recorded. No zod change, no describe() change. A .changeset/*.md only if the changeset-presence gate demands one; otherwise the skip-changeset label.
    Assignee: untouched — it is the PM's (os-justin).
    Clause-②: no

    Premise re-verified at base 919beca before editing (all four faces hold, one line number drifted):

    • Per-component face is five keys — title description label placeholder emptyText — at translation.zod.ts:927-931.
    • The submitLabel retirement note is at :867-876; the two deliberate exclusions (help, subtitle) with their reasons are at :878-888.
    • content is neither named nor excluded anywhere in translation.zod.ts (the four content hits in that file are at :486, :511, :519, :550, all prose in the retired-key conversion region, none on this face).
    • element:text's content: I18nLabelSchema is declared in packages/spec/src/ui/component.zod.ts at :1819 (inside ElementTextPropsSchema, opened at :1802) — the dispatch's :1784 had drifted; located by content, not line number.

    premise_still_valid: true. Proceeding with ruling A.


    Generated by Claude Code

  6. os-justin commented on Sep 4, 2026

    @os-justin
    Collaborator

    os-dev-report

    {
      "issue": 14412,
      "status": "done",
      "branch": "claude/issue-14412-content-exclusion-sentence",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/15216",
      "premise_still_valid": true,
      "summary": "Ruling A executed as written: no key added to PageTranslation.components; the deliberate exclusion of element:text's content is now recorded in the schema comment beside help and subtitle, citing the #10926 submitLabel precedent, and pointing the extraction/coverage question at #14749 without touching it. One bullet added to the exclusion list plus its lead-in updated from 'Two deliberate exclusions' to 'Three' so the count stays true. Comment only: no zod key, no describe() change, no resolver change. The changeset-presence gate has no path filter and skip-changeset is reserved for diffs that publish nothing from a released package, so a patch changeset for @objectstack/spec was added on evidence that this diff does publish: packages/spec is not private and its npm files array includes src/**/*.zod.ts, so npm pack --dry-run --json lists src/system/translation.zod.ts among the 2087 tarball files, and the comment text also lands in 36 dist files (0 of them .d.ts, so the type surface is unchanged). Worktree base was 919beca, not the dispatch's c351a84 — origin/main advanced between the PM's read at 05:32Z and worktree time; PR base sha is 50d6c924.",
      "premise_evidence": {
        "face_is_five_keys": "translation.zod.ts:927-931 — title, description, label, placeholder, emptyText; submitLabel absent as the triage said",
        "existing_exclusions": "translation.zod.ts:878-888 name help and subtitle with reasons; the submitLabel retirement note is at :867-876",
        "content_absent_from_face": "the only four 'content' hits in translation.zod.ts are :486 :511 :519 :550, all prose in the retired-key conversion region — content is neither named nor excluded on this face",
        "content_declared_as_i18n_label": "ui/component.zod.ts:1819 declares content: I18nLabelSchema inside ElementTextPropsSchema (opened :1802). The dispatch cited :1784, which had drifted; located by content, not line number",
        "citation_precedent_holds": "translation.zod.ts already cites issue numbers in comments 15 times, so the #10926 / #14749 citations follow the file's own convention; the citation is in the JSDoc, not in any describe()",
        "issue_body_intact": "no sanitizer truncation — the body ends with its signature footer and the os-decision-facets marker is HTML-escaped, not eaten"
      },
      "files_changed": [
        "packages/spec/src/system/translation.zod.ts (comment only, +11/-2)",
        ".changeset/spec-text-content-exclusion-recorded.md (new, patch for @objectstack/spec)"
      ],
      "deviations_from_claim_surface": "none — git diff --name-only against the merge base is exactly the two paths above; the changeset was the branch the claim comment pre-authorised ('a changeset only if the gate demands one')",
      "gates": {
        "head_sha": "3bf408ba",
        "derivation": "node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack, run from the worktree after the last commit with no path arguments — it read the change set itself (2 paths; committed 2, working tree 0, untracked 0; three-dot semantics) and returned 47 families across 30 workflow files. --repo assertion checked against this checkout's origin and holds. Exit 0.",
        "green": {
          "spec build": "exit 0 — 'check-dts-emitted: @objectstack/spec - 34/34 declared declaration file(s) present.'",
          "spec check:generated": "exit 0 — 'All 15 generated artifacts are up to date.' Nothing regenerated, so PM assumption (b) resolves negative: this JSDoc block is NOT copied into content/docs/references/, check:docs needed no --fix, and the PR carries no generated follower",
          "spec typecheck": "exit 0 — includes check:scripts-typecheck and check:test-typecheck ('54 file(s) / 261 error(s) / 145 pinned signature(s) held in test-typecheck-debt.json', unchanged)",
          "spec test (full package)": "exit 0 — 'Test Files 469 passed (469)', 'Tests 12542 passed (12542)', duration 411.94s",
          "spec test (targeted)": "exit 0 — src/system/translation.test.ts, 'Tests 119 passed (119)'; it is the only test file in the repo referencing PageTranslation / translationDataShape, and it pins none of the comment text",
          "spec source audits check:generated does not run (8)": "all exit 0 — check:browser-reachable-entries, check:dual-source-exports, check:empty-state, check:entry-nameability, check:exported-any, check:llms-txt, check:variant-docs, check:yaml-examples",
          "repo check gates (14)": "all exit 0 — check:doc-authoring, check:nul-bytes, check:changeset-gate-self-tests, check:cross-package-test-inputs, check:logger-receiver-detach, check:merge-driver, check:objectui-changeset, check:page-declaration-shape, check:pm-half-states, check:published-files, check:slot-lookup, check:spec-parsed-alias, check:test-source-alias, check:type-source-resolution",
          "lint check:doc-formula-expressions": "exit 0 after its prerequisites were built (see not_measured_then_measured)",
          "node script gates and self-tests (13)": "all exit 0 — check-adr-0087-registration --self-test, check-changeset-no-major --self-test, check-ci-filter-parity, check-comment-mask-adoption (+ --self-test), check-dev-prereqs --self-test, check-empty-changeset --self-test, check-keyed-text-bounds (+ --self-test), check-plugin-teardown-shape (+ --self-test, '47 cases pass'), check-system-context-census (+ --self-test), check-undeclared-dep-imports (+ --self-test), docs-audit/check-affected-docs, docs-audit/check-drift-comment, pm/release-rehearsal-clone --self-test",
          "pnpm lint (repo-wide)": "exit 0 — eslint . --no-inline-config run WHOLE, not narrowed, so no narrowing argument is owed; eslint on the edited file alone also exit 0"
        },
        "not_measured": {
          "check:dual-build-cjs-loads": "exit 3, NOT a failure and NOT a pass — its own verdict line reads 'Run pnpm build first. This is NOT a pass: nothing was measured.' It needs a built tree for 76 packages; a comment-only diff cannot affect CJS load behaviour and CI builds it anyway. Declared, not counted as green."
        },
        "not_measured_then_measured": {
          "check:doc-formula-expressions": "first two runs exit 3, 'PREREQUISITE NOT MET' — @objectstack/formula unbuilt, then @objectstack/lint unbuilt. Both built under the shared lock; third run exit 0. The first two readings are reported as unrun, not as reds.",
          "check-plugin-teardown-shape --self-test": "first run exit 1, but its own text was a refusal, not a finding: 'cannot read the positive control at 621a487607881c66b2899b7e3477115229a156b4 ... Deepen the clone'. This checkout is shallow (git rev-parse --is-shallow-repository = true). A targeted 'git fetch origin 621a4876...' made the pinned fixture reachable; re-run exit 0, '47 cases pass'. The non-self-test gate was exit 0 in both readings."
        },
        "exit_capture": "every gate was run as 'cmd > logfile 2>&1; EXIT=$?' — redirect first, capture second, never through a pipe. Heavy runs (build, typecheck, tests) went through scripts/pm/os-verify-lock.sh and are read from its VERDICT line, not a bare $?; each printed 'VERDICT command-exit 0'.",
        "ci_status_at_report_time": "in_progress — reported at draft-PR time per the dispatch contract; CI convergence is the PM's"
      },
      "tests": "pnpm --filter @objectstack/spec test at head 3bf408ba: 'Test Files 469 passed (469)', 'Tests 12542 passed (12542)'. Targeted first: src/system/translation.test.ts, 'Tests 119 passed (119)'. pnpm --filter @objectstack/spec typecheck exit 0. No ablation was run and none is claimed: the diff adds no guard, no test and no gate, so there is nothing whose failure could be demonstrated by mutation — a comment carries no accept/reject behaviour. The reverse-verification that IS meaningful here was done instead and is stated above: check:generated proves the comment reaches no generated artifact, and the npm-pack / dist greps prove it does reach the published tarball, which is what settled the changeset question.",
      "mcp_calls": "7 — issue_read get, issue_read get_comments, add_issue_comment (claim), create_pull_request, pull_request_read (body readback), add_issue_comment (this report), issue_read get_comments (report readback). REST was probed first per the channel rule and is 403 for this seat on repo-scoped endpoints, authenticated or not; gh is absent. No list_issues, no search_issues, no wide scans.",
      "open_questions": [],
      "out_of_scope_findings": []
    }

    Generated by Claude Code

  7. os-justin commented on Sep 4, 2026

    @os-justin
    Collaborator

    ACCEPT — PR #15216 (head 3bf408ba), reviewed 2026-09-04T06:18Z by the domain:spec seat, session_01H2oQebDDxYKfWZusyd8GXk (os-justin), seat post #6017. Clause ② no (comment-only; no accept/reject behaviour moves) — no contract review owed.

    Checklist readings, taken on GitHub, not from the report: draft ✓ · base main ✓ · first body line Fixes #14412 ✓ (closing on merge is right — the card's whole deliverable is this sentence) · get_files = exactly the claimed surface (packages/spec/src/system/translation.zod.ts +11/−2, comment block only; .changeset/spec-text-content-exclusion-recorded.md new) · no content/docs/releases/** · @objectstack/spec is published and its files array ships src/**/*.zod.ts, so the patch changeset is the correct form over skip-changeset (the dev's npm pack --dry-run reading; accepted) · governed-surface predicate --test on the two paths: 0 of 2 hit · origin/main churn on the file since dispatch: none (last touch PR #14381, 2026-09-02) · diff matches ruling 5518061415 verbatim — content recorded as a deliberate exclusion beside help and subtitle, citing the submitLabel precedent, the extractor question pointed at #14749 and not addressed · no model identifiers in PR title/body/changeset/comment · test evidence is real commands with quoted verdict lines at the head sha. Deviations: none (the dispatch's component.zod.ts:1784 line reference had drifted to :1819; the dev relocated by content — a dispatch-side slip, not a dev deviation). Report notes the container's GITHUB_TOKEN answers 403 on repo-scoped REST — MCP channel used, recorded on the seat post.

    Landing: CI on 3bf408ba was still converging at 06:17Z (Test Core shards, Lint & Repo Gates, Type Check jobs in progress; 12 completed checks all success/skipped). On all-green: ready → squash auto-merge → queue; pm:dispatched strips on MERGED with the landing note here.


    Generated by Claude Code

  8. os-justin commented on Sep 4, 2026

    @os-justin
    Collaborator

    Landing provenance (2026-09-04T06:41Z, domain:spec seat, session_01H2oQebDDxYKfWZusyd8GXk): PR #15216 head 3bf408ba — all 34 check runs success or skipped (Test Core rollup 06:39:25Z, Lint & Repo Gates 06:27:49Z, Type Check · workspace 06:28:32Z), mergeable_state: clean, governed-surface predicate 0 of 2. ACCEPT 5536542172 (Clause ② n/a) is the review of record. Flipped ready → squash auto-merge → merge queue; on MERGED: pm:dispatched stripped, landing note here with the translation.zod.ts probe on origin/main.


    Generated by Claude Code

  9. os-justin commented on Sep 4, 2026

    @os-justin
    Collaborator

    Landed (2026-09-04T07:13Z, domain:spec seat, session_01H2oQebDDxYKfWZusyd8GXk): PR #15216 MERGED via the queue as squash 1c6f7b4a (07:11:37Z); this card closed by its Fixes line. Probes on origin/main 1c6f7b4a: the content exclusion sentence present in packages/spec/src/system/translation.zod.ts (1 hit; control: the help exclusion, 2 hits), .changeset/spec-text-content-exclusion-recorded.md present. pm:dispatched stripped in this stroke (read back). Lane close-out check since seating: only this card and #14180 (the predecessor's) closed — no silent close by a closing keyword. Downstream: the extractor/coverage question stays on #14749 (domain:cli).


    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

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions