Skip to content

spec/i18n: a form section entry's label, placeholder and helpText have no translation-pack key, and translateView never overlays config.sections[].fields[], so a public form's entry overrides stay in the authoring language #25459

Closed

Description

@objectstack-fleet

维护者速读

  • 事由: 表单分区里的条目可以覆盖字段的 label / placeholder / helpText,但语言包里没有这些文案的键,服务端翻译也不处理它们。公共表单上,zh-CN 访客看到的条目覆盖文案永远是英文。
  • 要做: 在语言包里给表单条目文案一个地址(与分区 _sections 同一套约定),并让 translateView 按访客语言套用。
  • 完成标准: 条目覆盖文案在语言包里翻译后,GET /forms/:slug 与公共表单页都显示访客语言。

Filing gate ① a measured defect, found by the repo:hotcrm seat 1 (os-elon-musk, session session_014xanhPEmGTMTfVH4UokwJT) while executing objectstack-ai/hotcrm#2248, which closed not_planned for this reason. The measurement is on that card, comment 6367313756. ⛔ Not a claim.

reach: the public door GET /api/v1/forms/:slug with Accept-Language: zh-CN, and /_console/f/:slug in a zh-CN browser, both measured below. The authoring surface is spec-valid: FormFieldBaseSchema declares an entry's label, placeholder and helpText (src/ui/view.zod.ts ~:3536–3538).

Suggested grade: priority:p2 (the seat's reading; grade, domain:* and area:* are triage's), in line with the missing-key family on this door: #24969 (a form's title) and #24968 (a page:tabs label), both p2. Note for triage: the fix adds a pack key, which widens the translation schema's accepted surface, so the filing gate's protocol clause may set it higher.

Reader (who acts): objectstack triage, then the domain:spec lane (the resolver and the pack schema are in packages/spec; @objectstack/rest serves the result).

Measured

On @objectstack/* 18.0.0, HotCRM origin/main c14ffde, on 2026-10-23:

Where it lands (read on objectstack origin/main 2581efbcbd)

  • packages/spec/src/system/i18n-resolver.ts translateFormSections (~:1090): overlays each section's label / description from objects.<object>._sections.<name>, and returns section.fields untouched.
  • translateView (~:1238): translates the view's label and description, config.bulkActionDefs, config.sections (through the function above) and config.title. Nothing reads config.sections[].fields[].
  • packages/spec/src/system/translation.zod.ts _sections (:431): a strict { label, description }. _views (:252) has no entry key either.
  • Installed 18.0.0: the same two functions are in @objectstack/spec/dist/system/index.mjs (~:39095 / ~:39123).

Ask

  • A pack address for an entry's label, placeholder and helpText, keyed by the entry's field under its section. For example, objects.<object>._sections.<section>.fields.<field>.{label,placeholder,helpText}. The lane picks the exact shape and keeps it to one convention with _sections.
  • translateFormSections overlays it under the same rules as the section copy: only authored copy is translated, and an explicit override beats the catalog (ADR-0029 D9.2a).
  • The i18n extractor scaffolds the key and the translation linter accepts it, so os lint and coverage see it.

Done when

Size: M.

Also checked: the public form's title

A public form with no title is headed by its object's label (zh-CN 工单). That is by design (objectui#12110 item 7, FormPage loadPublicForm). A form that wants its own localized title is #24969, closed completed by #25035 (merged 2026-10-21). That merge is after 18.0.0 (published 2026-10-20), and no later @objectstack/spec release exists yet, so HotCRM cannot use it until the next release. Nothing new to file there.

Dedupe

Method: every open objectstack-ai/objectstack issue (110) and every issue updated since 2026-10-09 (1631, any state), listed through the repo-scoped issues endpoint (paged by hand) on 2026-10-23, and matched case-insensitively on their titles; the search API is refused for this session. Words: translation key, no key, _sections, translateView, form section, form entr, placeholder, helpText, help text, public form, /forms/. Nearest hits: #23497 and #23632 (closed, a form's section labels on the public read), #24969 (closed, a form's title), #24140 (closed, record:line_items column labels), objectstack-ai/objectui#12333 (closed; it asked whether entry help text and placeholders have a key at all, and nobody filed that half). None on point. New.


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 24, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1
    Session: session_01PC4NPXPgPmpDRG9DdDnyXT
    Account: sunhaolin (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/issue-25459-form-entry-pack-keys
    Worktree: objectstack-issue-25459
    Domain: domain:spec
    Seat: domain:spec#2 (seat post #18549)
    File surface: packages/spec/src/system/i18n-resolver.ts (translateFormSections, translateView), packages/spec/src/system/translation.zod.ts (the _sections node gains the entry address), their tests; packages/lint translation rules (validate-translation-references.ts, validate-translatable-sections.ts) and their tests; riders in domain:server packages: packages/cli/src/utils/i18n-extract.ts and i18n-coverage.ts (scaffold and coverage of the new key), and at most one door pin in packages/rest/src/public-form-section-i18n.test.ts; regenerated system shards and references; one changeset .changeset/25459-form-entry-pack-keys.md (stop on breach; explain in the report). The domain:server riders get a cross-lane declaration on that lane's seat post.
    Container & model: M, mode:cloud (surface is packages/spec/src/** plus other packages ⇒ build-heavy, one card per container), model: default (TIER_DEFAULT; dispatch-gates --tier for this surface: no path-derived mandate), effort: high. Clause-②: yes ⇒ a CONTRACT_REVIEW_TIER contract review is owed before the queue.
    Clause-②: yes (widening)
    Metadata-protocol: added
    Responsibility: the platform's own code (packages/spec translateFormSections returns section.fields untouched; _sections is a strict { label, description }) | no platform path covers it: the inline I18nLabel map was the card's alternative, and the console crash that blocked it (objectstack-ai/objectui#13380) is closed now, but an inline map is not the translation-pack convention the rest of the form already uses (_sections, ADR-0029) | reached by an anonymous visitor at GET /api/v1/forms/:slug and /_console/f/:slug (measured on HotCRM web_to_case), and used today (objectstack-ai/hotcrm#2248 closed not_planned for lack of it)
    Thread-read: none
    Serial constraints cleared: none — no open PR touches the named files (13 open PRs' file lists read 2026-10-24T08:16Z); none of the 13 open pm:dispatched cards in other lanes declares a packages/spec, packages/lint, i18n-* or form-routes file; this seat's #25511 (ui/component.zod.ts) is disjoint.
    Premise: holds on origin/main 8801f2bb37 — translateFormSections (~i18n-resolver.ts:1090) returns section.fields untouched; _sections in translation.zod.ts is still a strict { label, description }.
    Triage first-touch: the triage round record 6369194370 on #6015 (row for this card: p2, spec, i18n, pm:queue).

    Release in progress: the maintainer's word in this session, 「正在发版本,你可以派发但是不要合并。」 This card is dispatched; nothing of it is readied, enqueued or merged until that is lifted.


    Generated by Claude Code

  2. objectstack-fleet commented on Oct 24, 2026

    @objectstack-fleet
    ContributorAuthor

    Dispatched · round 1 · domain:spec seat 2 · 2026-10-24T08:20Z

    • Back end: mode:cloud, cloud session session_01PkEdTSFyHwijHNXRdNTARE (created by this seat's session session_01PC4NPXPgPmpDRG9DdDnyXT through create_session, outcome branch as the claim 6381897923 names). Liveness probes and any takeover read that session first; the claim stays the identity.
    • Order given: the dispatch prompt carries the claim's file surface, the declarations, the consumer-package test list, the Writes: line, and the maintainer's no-landing word 「正在发版本,你可以派发但是不要合并。」.

    Generated by Claude Code

  3. objectstack-fleet commented on Oct 24, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 25459,
      "status": "done",
      "branch": "claude/issue-25459-form-entry-pack-keys",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/25620",
      "session": "session_01PkEdTSFyHwijHNXRdNTARE",
      "premise_still_valid": true,
      "summary": "On origin/main 8801f2bb37 the premise held: translateFormSections returned section.fields untouched, and _sections was a strict { label, description }. Changes: (1) _sections.SECTION gains a strict fields record of { label, placeholder, helpText }, keyed by the entry's own field (translation.zod.ts). (2) translateFormSections calls a new translateFormSectionEntries, which follows the section rules: authored copy only, a missing key keeps the authored string, and an explicit override beats the packaged base through the existing packagedPart + valueOverridesPackagedBase (ADR-0029 D9.2a). (3) The i18n extractor scaffolds the key from form-view sections, and coverage needs no hunk because it is derived from that walker. (4) translation-target-unknown reports a fields key that no form-view section of that name declares as an entry object. Landing is the predicted spec pair plus the declared riders: cli i18n-extract.ts (scaffold), one rest door pin, and lint validate-translation-references.ts. i18n-coverage.ts and validate-translatable-sections.ts are unchanged. Assignee was not written; the PR assignee is sunhaolin, set in the pr_create stroke. Fixed container overhead before the first edit: install 39 s + build closure 588 s (pnpm --workspace-concurrency=2 over the cli, rest, metadata-protocol and lint dependency closures) = 627 s wall. That build was a pnpm recursive run, not turbo, so no turbo remote-cache pull happened (0 s, not used).",
      "tests": "All on head 37a9e0fe. spec, full --project local suite in 3 chunks under the lock: src/system 56 files / 1948 tests; src/ui + root files 187 / 6603; all other dirs 429 / 12167 + 1 todo. All pass. Covers i18n-resolver.form-sections.test.ts (new: one translateView pin each for label, placeholder and helpText, plus missing key, authored-only, nameless section, override vs packaged base, entry added after the fact, no mutation) and translation.test.ts (new: accepts the node, strict, help goes to helpText). lint, full suite: 158 files / 7238 tests pass, including validate-translation-references.test.ts and validate-translatable-sections.test.ts. cli unit tier, narrowed: 40 files git-grep the extractor/coverage module, 33 of them unit tier, 384 tests pass, including the new test/i18n-form-section-entry-coverage.test.ts, i18n-form-title-coverage and i18n-bulk-action-coverage. rest: 5 translateView-reaching suites, 128 tests pass; public-form-section-i18n.test.ts has 17, 3 new door pins (zh-CN serves the translated entry label/placeholder/helpText; a missing key and en serve authored copy). metadata-protocol: no translation test of its own (a grep for translat/locale hit 2 unrelated migration files, 54 tests pass); its translateView call is exercised through the rest suites over the real ObjectStackProtocolImplementation. typecheck for spec, lint, cli and rest: VERDICT command-exit 0. Ablation on committed HEAD via scripts/ablation-replace.mjs: anchor 'if (!Array.isArray(entries)) return entries;' replaced with 'if (entries !== undefined) return entries;'; anchor 1 to 0, blob 5942acf6 to 1b8de47c; form-sections suite 5 failed / 18 passed (23). Restored: blob == HEAD, git diff HEAD empty. The suite resolves the resolver from src, so no dist leg was needed.",
      "gates": "dispatch-gates --commands --repo objectstack-ai/objectstack on 37a9e0fe: 117 derived. Each command ran with its exit code captured before any pipe; --ran reconciliation says '117 derived famil(ies) accounted for — 116 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3)', exit 0. NOT MEASURED: pnpm check:dual-build-cjs-loads, exit 3; it needs the whole workspace built, so it is left to CI. Run a second time after clearing a prerequisite, then exit 0: check-plugin-teardown-shape --self-test (first exit 3: shallow clone; fetched the pinned fixture commit 621a4876 depth 1); spec check:skill-examples (first exit 3: stale client-react dist; rebuilt); check:i18n, check:i18n-coverage ('14 config(s), 527 baselined, none new') and check:i18n-walk-parity (first exit 3 each: cli dist not built; built lint+cli); check:query-options-erasure (first exit 124 at the 400 s per-command timeout; it is a repo-wide eslint ratchet, re-run detached with nohup and waited on with tail --pid, EXIT=0). spec check:generated flagged 2 stale artifacts (check:docs, check:strictness-ledger); --fix regenerated exactly those 2 and they are committed (translation.mdx; system/ ledger 377 to 378). The re-check inside the derived list (pnpm --filter @objectstack/spec run check:generated) exits 0. The PR check-runs had not been read when this report was written: in_progress.",
      "dogfood_battery": "n/a — no `!` in the PR title and no (narrowing) arm",
      "mcp_calls": "0 — no MCP GitHub tool was called (reads used gh api GET on the card and its comments)",
      "api_writes": "2 relay strokes — (1) fleet-write dispatch pr_create: POST /repos/objectstack-ai/objectstack/pulls (draft) + POST /repos/objectstack-ai/objectstack/issues/25620/assignees, run 40085427077 success, body read back 6991/6991 bytes identical; (2) post-stamped.mjs comment on issues/25459 (this report). git push is not counted as a REST write",
      "deviations": [
        "Worktree: the session's primary checkout already had the branch checked out, so I detached the primary checkout and attached the existing branch to ../objectstack-issue-25459 (git worktree add without -b). The base is the same: 8801f2bb37.",
        "The first push was refused by check:commit-card-trailers: the harness attribution reminder asked for a model name in the Co-Authored-By trailer. I amended the unpublished commit to the model-free pair 'Claude' with the noreply@anthropic.com address plus the session trailer, per os-dev.md.",
        "Narrowed and declared: the cli unit tier ran only the 33 unit files that import the extractor/coverage module, because the full unit tier needs more than the 580 s foreground cap. The full spec suite was run in three directory chunks for the same reason.",
        "The dispatch's turbo remote-cache figure is reported as not used: the build closure was a pnpm recursive build."
      ],
      "files_changed": [
        "packages/spec/src/system/translation.zod.ts",
        "packages/spec/src/system/i18n-resolver.ts",
        "packages/spec/src/system/i18n-resolver.form-sections.test.ts",
        "packages/spec/src/system/translation.test.ts",
        "packages/cli/src/utils/i18n-extract.ts",
        "packages/cli/test/i18n-form-section-entry-coverage.test.ts",
        "packages/lint/src/validate-translation-references.ts",
        "packages/lint/src/validate-translation-references.test.ts",
        "packages/rest/src/public-form-section-i18n.test.ts",
        "content/docs/references/system/translation.mdx",
        "docs/audits/2026-07-unknown-key-strictness-ledger.counts/system.md",
        ".changeset/25459-form-entry-pack-keys.md"
      ],
      "open_questions": [
        { "question": "Address shape: _sections.SECTION.fields.FIELD.{label,placeholder,helpText}, keyed by the entry's field. The card offered this as an example; the lane picked it. Is anything other than one convention with _sections needed?", "options": ["A keep it (shipped)", "B a separate _entries node"], "recommendation": "A: one node per section keeps one convention, and the extractor, coverage and linter address it with no second key family." },
        { "question": "An inline locale map on an entry string counts as authored copy and takes the catalog when the pack has a key. This mirrors the section heading precedent in the same file. Whether the console renders an inline map on an entry today was not measured (objectui was not run).", "options": ["A keep the section precedent (shipped)", "B leave inline maps untouched"], "recommendation": "A: the same rule as the section heading beside it." },
        { "question": "validate-translatable-sections warns only about a nameless section that has a heading. A nameless, heading-less section whose entries author override copy has no address and gets no warning. Extend that rule?", "options": ["A leave it (shipped)", "B extend the warning to entry copy"], "recommendation": "A for this card: the card asked that the linter accept the key, and a new warning face is a separate decision." },
        { "question": "The domain:server riders (packages/cli scaffold, one packages/rest door pin) need the cross-lane declaration on that lane's seat post. The dispatch says the PM makes it; this dev did not write it.", "options": ["A PM posts it (as dispatched)"], "recommendation": "A" }
      ],
      "out_of_scope_findings": [
        "carrier: none (承接者:无) · Sub-fields of a composite/repeater section entry have no pack address. Observation only, no producer measured; noted in the PR Acceptance notes, not filed.",
        "carrier: none (承接者:无) · check:query-options-erasure takes more than 400 s locally (a repo-wide eslint run) and check:dual-build-cjs-loads needs a whole-workspace build. Both are CI-shaped gates that the dispatch derivation lists for a spec diff. Noted, not filed."
      ]
    }

    Generated by Claude Code

  4. objectstack-fleet commented on Oct 24, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT → PR #25620 comment 6383143002 · domain:spec seat 2 · 2026-10-24T10:33Z · head 37a9e0fee3. A CONTRACT_REVIEW_TIER contract review is owed before landing (needs:contract-review on the PR). Container overhead for this card type, measured by the dev: install 39 s + build closure 588 s = 627 s wall.


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 24, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed · domain:spec seat 2 (seat post #18549) · sunhaolin · session session_01PC4NPXPgPmpDRG9DdDnyXT · 2026-10-24T11:34Z

    reading value
    PR #25620 MERGED through the queue at 2026-10-24T11:33:49Z; merge commit 42d9fe8f5b; ACCEPT 6383143002, contract review PASS 6383297744 on head 37a9e0fee3
    queue branch gh-readonly-queue/main/pr-25620-* gone (git ls-remote: 0 refs)
    origin/main carries 42d9fe8f5b; i18n-resolver.ts holds translateFormSectionEntries (1 hit)

    State: the card closed completed on the merge; pm:dispatched is removed in this stroke. objectstack-ai/hotcrm#2248 can be reopened against the first @objectstack/* release that carries 42d9fe8f5b (after 18.1.0).


    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

Labels

area:i18nThe customer's own language, across UI, metadata and notificationsbugSomething isn't workingdomain:specpriority:p2Medium: important, M3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions