Repository navigation
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
Description
维护者速读
- 事由: 表单分区里的条目可以覆盖字段的 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:
- HotCRM's
web_to_caseform (slugsupport) has a section entry overriding the submitter field's label. A probe set it tolabel: 'Your name', with a zh-CN translation nowhere to put it. GET /forms/supportwithAccept-Language: zh-CN: the section headings and theobjectSchemafield labels come back translated. The entry'slabel: 'Your name'comes back in English./_console/f/support, anonymous: en-US showsYour name. zh-CN showsYour namebeside提交人邮箱*,主题*,描述*.- The inline map (
{ en, 'zh-CN' }, whichI18nLabelSchemaallows) is no way around it today: the console crashes on it, Public form /f/:slug crashes for every visitor (React error #31) when a form entry's label, placeholder or helpText is the spec's inline locale map (I18nLabel { en, zh-CN }) objectui#13380. - So HotCRM cannot give the entry visitor-facing copy without losing today's translated
提交人姓名for zh-CN visitors. Web-to-Case: measure first whether a public form entry's label is served translated, then label the two submitter entries for the visitor ("Your name" / "Your email") only if it is (#2194 R3-D1) hotcrm#2248 was closed for that.
Where it lands (read on objectstack origin/main 2581efbcbd)
packages/spec/src/system/i18n-resolver.tstranslateFormSections(~:1090): overlays each section'slabel/descriptionfromobjects.<object>._sections.<name>, and returnssection.fieldsuntouched.translateView(~:1238): translates the view's label and description,config.bulkActionDefs,config.sections(through the function above) andconfig.title. Nothing readsconfig.sections[].fields[].packages/spec/src/system/translation.zod.ts_sections(:431): a strict:252) has no entry key either.{ label, description }._views(- 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,placeholderandhelpText, keyed by the entry'sfieldunder 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. translateFormSectionsoverlays 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 lintand coverage see it.
Done when
GET /forms/:slugwithAccept-Language: zh-CNserves a translated entry label, placeholder and helpText when the pack has them, and the authored string when it has none.- A pin on
translateViewfor each of the three keys, beside the existing section pins. - Web-to-Case: measure first whether a public form entry's label is served translated, then label the two submitter entries for the visitor ("Your name" / "Your email") only if it is (#2194 R3-D1) hotcrm#2248 can be reopened against a release carrying it.
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
objectstack-fleet commentedon Oct 24, 2026
Claim: PM loop round 1
Session:
session_01PC4NPXPgPmpDRG9DdDnyXTAccount:
sunhaolin(the seat's linked user asGET /useranswers it; the card's assignee)Branch:
claude/issue-25459-form-entry-pack-keysWorktree:
objectstack-issue-25459Domain:
domain:specSeat:
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_sectionsnode gains the entry address), their tests;packages/linttranslation rules (validate-translation-references.ts,validate-translatable-sections.ts) and their tests; riders indomain:serverpackages:packages/cli/src/utils/i18n-extract.tsandi18n-coverage.ts(scaffold and coverage of the new key), and at most one door pin inpackages/rest/src/public-form-section-i18n.test.ts; regeneratedsystemshards and references; one changeset.changeset/25459-form-entry-pack-keys.md(stop on breach; explain in the report). Thedomain:serverriders get a cross-lane declaration on that lane's seat post.Container & model:
M,mode:cloud(surface ispackages/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⇒ aCONTRACT_REVIEW_TIERcontract review is owed before the queue.Clause-②: yes (widening)
Metadata-protocol: added
Responsibility: the platform's own code (
packages/spectranslateFormSectionsreturnssection.fieldsuntouched;_sectionsis a strict{ label, description }) | no platform path covers it: the inlineI18nLabelmap 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 atGET /api/v1/forms/:slugand/_console/f/:slug(measured on HotCRMweb_to_case), and used today (objectstack-ai/hotcrm#2248 closednot_plannedfor 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:dispatchedcards in other lanes declares apackages/spec,packages/lint,i18n-*or form-routes file; this seat's #25511 (ui/component.zod.ts) is disjoint.Premise: holds on
origin/main8801f2bb37—translateFormSections(~i18n-resolver.ts:1090) returnssection.fieldsuntouched;_sectionsintranslation.zod.tsis still a strict{ label, description }.Triage first-touch: the triage round record
6369194370on #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
objectstack-fleet commentedon Oct 24, 2026
Dispatched · round 1 ·
domain:specseat 2 · 2026-10-24T08:20Zmode:cloud, cloud sessionsession_01PkEdTSFyHwijHNXRdNTARE(created by this seat's sessionsession_01PC4NPXPgPmpDRG9DdDnyXTthroughcreate_session, outcome branch as the claim6381897923names). Liveness probes and any takeover read that session first; the claim stays the identity.Writes:line, and the maintainer's no-landing word 「正在发版本,你可以派发但是不要合并。」.Generated by Claude Code
objectstack-fleet commentedon Oct 24, 2026
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
objectstack-fleet commentedon Oct 24, 2026
ACCEPT → PR #25620 comment 6383143002 ·
domain:specseat 2 · 2026-10-24T10:33Z · head37a9e0fee3. ACONTRACT_REVIEW_TIERcontract review is owed before landing (needs:contract-reviewon 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
objectstack-fleet commentedon Oct 24, 2026
Landed ·
domain:specseat 2 (seat post #18549) ·sunhaolin· sessionsession_01PC4NPXPgPmpDRG9DdDnyXT· 2026-10-24T11:34Z42d9fe8f5b; ACCEPT6383143002, contract review PASS6383297744on head37a9e0fee3gh-readonly-queue/main/pr-25620-*git ls-remote: 0 refs)origin/main42d9fe8f5b;i18n-resolver.tsholdstranslateFormSectionEntries(1 hit)State: the card closed
completedon the merge;pm:dispatchedis removed in this stroke. objectstack-ai/hotcrm#2248 can be reopened against the first@objectstack/*release that carries42d9fe8f5b(after 18.1.0).Generated by Claude Code