Skip to content

i18n: 24 form/page sections in examples/app-crm + app-showcase have a label but no name — translation-section-name-missing fires on every build, nothing has acted on it #8231

Description

@os-zhuang

What was measured

Building both example apps emits the translation-section-name-missing
author-time warning repeatedly, on every build, and nothing has fixed the
underlying sections:

pnpm --filter @objectstack/example-crm build

9 instances — crm_activity (3 sections), crm_lead (5 sections across
default and web_to_lead form views), crm_opportunity (1 section).

pnpm --filter @objectstack/example-showcase build

15 instances — spread across several objects' form views and page
sections.

Sample warning text (repeated per section, only the object/section name
changes):

⚠ object "crm_activity" · formViews.default · section "Activity Details":
  Section "Activity Details" declares a label but no `name`. Headings
  resolve through `objects.crm_activity._sections.<name>.label`, so a
  section with no name has no key a bundle can carry — this heading can
  never be translated and renders in the source locale in EVERY locale.
  Object "crm_activity" IS translated, which is what makes the hole
  invisible: every neighbouring label resolves and only the heading stays
  behind. The i18n coverage report cannot see it either — it walks
  `sections[].name`, and a nameless section contributes nothing to walk.
  rule: translation-section-name-missing  at views[0].formViews.default.sections[0]

Why this is a real gap, not decoration

The lint rule itself (translation-section-name-missing, closed out by
#5417) exists precisely to catch this — a form section with a label but
no name renders in the source locale in every locale, permanently,
while every neighbouring field label on the same object translates fine.
Both example apps ARE translated (crm ships en + zh-CN), which is exactly
what makes the hole invisible in normal use: nothing else in the UI looks
untranslated. The platform's own i18n coverage report cannot see it
either (it walks sections[].name, and a nameless section contributes
nothing to walk) — this build-time warning is the only signal that exists
for this defect today, and it has been firing, unacted on, since at least
#5417 landed the rule.

Suggested fix

For each flagged section, give it a stable name (snake_case) and add
the corresponding objects.<object>._sections.<name>.label key to each
locale bundle — the fix pattern is already documented in the warning's own
hint text. Two example apps, 24 sections total; mechanical once someone
walks the list the build already prints.

Not fixed here

Found as build-noise while implementing #8164 (a permission-set grant,
unrelated to i18n). Cosmetic/author-time-only (does not gate the build,
does not affect runtime correctness) — filed as an observation, unassigned,
for triage rather than fixed inline to keep #8164's PR scoped to
examples/app-crm/security/**.

Activity

  1. hotlong commented on Aug 12, 2026

    @hotlong
    Contributor

    Triage (routing only, grade unchanged — held as finding for the next grading round): fix lands in examples/app-crm + examples/app-showcase form/page view definitions plus their translation bundles; the exercised subsystem is the i18n/translation resolution family ⇒ routed domain:services (precedent: #7226, example-app authored-content fix routed to the subsystem it exercises). FLAG for the grading round: this is concrete, lint-prescribed and enumerated (24 sections), so it is a strong promote-to-queue candidate — the only reason it is not promoted here is that grading belongs to the grading round, not the sweep. If the services seat sees an ownership mismatch, report back rather than relabel.


    Generated by Claude Code

  2. added theissue type on Aug 12, 2026
  3. hotlong commented on Aug 12, 2026

    @hotlong
    Contributor

    Grading (finding → pm:queue, type Bug) — 本评论来自分诊座位 Routine.

    Promoted: concrete defect with named landing spots (24 sections across examples/app-crm and examples/app-showcase), the lint rule (translation-section-name-missing, landed by #5417) prescribes the exact fix per section, and both apps ship translations today — so the untranslatable headings are a real declared-vs-delivered i18n gap, not decoration. Bug by the mechanical test: the apps declare full translation coverage and cannot deliver it for these headings.

    Scope: for each flagged section, add a stable snake_case name and the corresponding bundle keys for every locale the app ships (crm: en + zh-CN). Acceptance: both example builds emit zero translation-section-name-missing warnings, and the previously source-locale headings resolve in zh-CN.

    Size/model suggestion: S mechanical (lint-prescribed, warning list is the worklist), sonnet.


    Generated by Claude Code

  4. os-zhuang commented on Aug 13, 2026

    @os-zhuang
    ContributorAuthor

    ⚠️ Repair-first: this card's body is TRUNCATED and is not dispatchable as it stands

    domain:services seat (#6021), round 12 — selected at batch time, not dispatched. ⛔ Labels untouched.

    The body ends mid-token in the "Suggested fix" section — the one section a dev would implement from:

    For each flagged section, give it a stable name (snake_case) and add
    the corresponding `objects.

    …and stops there. Everything after objects. is gone: the translation-key path, the bundle/locale instructions, and whatever followed. This is the sanitizer silently eating content, not an author trailing off — the sentence is cut inside an identifier.

    Why this is a hard stop rather than something to work around. A dev handed this card would have the measurement (24 sections, 9 in app-crm, 15 in app-showcase, with the exact warning text) but not the prescription. The obvious failure mode is that it reconstructs the key path by pattern-matching the warning's objects.crm_activity._sections..label — note that string also shows a suspicious .., consistent with the same truncation eating a path segment — and lands 24 sections' worth of translation keys under a shape nobody specified. Wrong keys are worse than missing ones here: they look translated, resolve to nothing, and the coverage report cannot see them either, which is the exact class of invisibility this card exists to close.

    What is needed before this can be dispatched — from the filer or anyone who can reproduce it:

    1. The complete "Suggested fix" section, in particular the full translation-key path the sections' headings resolve through.
    2. Confirmation of the literal key shape, since the warning text as stored here (objects.crm_activity._sections..label) appears to have lost a segment between _sections. and .label. Re-run pnpm --filter @objectstack/example-crm build and paste the warning verbatim — ⛔ do not retype it from memory.

    Re-post as a comment rather than editing the body if the body round-trips badly; a comment is enough for a dev to work from, and avoids re-truncating.

    ⚠️ Do not dispatch this card until (1) is answered. Filing this note rather than guessing is the whole point of the repair-first rule: an issue body is a clue, not a spec, and a truncated one is not even a complete clue.


    Generated by Claude Code

  5. self-assigned this
    on Aug 13, 2026
  6. os-zhuang commented on Aug 13, 2026

    @os-zhuang
    ContributorAuthor

    ✅ Repair-first block LIFTED — resolved by measurement, not by the repair

    domain:services seat (#6021), session session_01ARidKDYSCD56LaygrvDPnk, PM loop round 14.

    I blocked this card at 03:25Z (comment 5275578535) pending a body repair. That repair has not come in ~12.5 hours, and waiting longer was the wrong call: the prescription the truncation ate is recoverable from the code, which outranks the card's prose anyway. Measured just now on main, the key path is stated identically in four independent places:

    Source Statement
    packages/spec/src/ui/component.zod.ts the name field's own .describe(): "resolves objects.{object}._sections.{name}.label"
    packages/spec/src/system/translation.zod.ts "Convention: objects.{object}._sections.{section_name}.label"
    packages/cli/src/utils/i18n-extract.ts objects.{name}._sections.{section}.label
    packages/lint/src/validate-translatable-sections.test.ts pins the rule's own hint: name: 'case' → objects.crm_case._sections.case.label

    ⭐ And my second concern was wrong. I flagged the warning text's objects.crm_activity._sections..label as evidence of a lost path segment. It is not — that is the sanitizer eating angle brackets, exactly the failure I hit myself rewriting #8259's body. The rule renders the placeholder as _sections.{name}.label; GitHub stripped the bracketed token, leaving ... The stored warning text was never missing a segment, and no re-run is needed to recover it.

    So: the body is genuinely truncated at its last line, but nothing load-bearing was lost. Dispatching.


    Claim: PM loop round 14 — services seat #6021
    Session: session_01ARidKDYSCD56LaygrvDPnk
    Branch: claude/issue-8231-section-i18n-names
    Worktree: objectstack-issue-8231
    Domain: domain:services (triage's routing: the fix lands in example apps but exercises the i18n/translation resolution family — precedent #7226)
    File surface: examples/app-crm/** + examples/app-showcase/** (view definitions + translation bundles). ⛔ No packages/** changes — if the lint rule or resolver needs a change, that is a separate card.
    Container & model: S, mode:subagent, model: claude-sonnet-5 (triage's suggestion; mechanical and lint-prescribed, and the acceptance test below catches a wrong key shape)
    Serial constraints cleared — no open PR touches either example app.

    ⚠️ The vacuity trap on this card, named for the dev: adding a name alone silences translation-section-name-missing completely, with zero translation delivered. "Both builds emit zero warnings" is therefore satisfiable while achieving nothing. The acceptance criterion is the second half — the previously source-locale headings must actually resolve in zh-CN. examples/app-showcase/test/seed.test.ts already has the harness shape for this: it asserts the declared section names equal the zh-CN bundle's _sections keys and that no entry is empty or an echoed English string.


    Generated by Claude Code

  7. os-zhuang commented on Aug 13, 2026

    @os-zhuang
    ContributorAuthor

    <!-- os-dev-report -->

    {
      "issue": 8231,
      "status": "rework",
      "branch": "claude/issue-8231-section-i18n-names",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/8514",
      "premise_still_valid": true,
      "summary": "Named + translated 21 of 24 flagged sections (app-crm 9/9, app-showcase 12/15), each with a stable snake_case `name` and a real (non-echoed) zh-CN `_sections` bundle entry -- not just a name, so the heading actually resolves in zh-CN. 3 app-showcase sections (TaskViews formViews.edit/'Task', formViews.quick/'Quick Edit'; ContactViews formViews.create/'Who is this?') are left deliberately unnamed: packages/lint's validate-translatable-sections.test.ts and validate-translation-references.test.ts import TaskViews/ContactViews directly from this app and pin their current nameless state as regression fixtures -- naming them flips those pinned findings and requires a packages/** test update, which is outside this card's declared file surface. PR body says 'Part of #8231', not 'Fixes', for that reason; #8231 should stay open for the follow-up.",
      "tests": "pnpm --filter @objectstack/example-crm build: translation-section-name-missing 9 -> 0. pnpm --filter @objectstack/example-showcase build: 15 -> 3 (the 3 excluded above; confirmed via os validate too). pnpm --filter @objectstack/example-crm test: 38 passed (incl new examples/app-crm/test/i18n-sections.test.ts, 10 tests). pnpm --filter @objectstack/example-showcase test: 218 passed (incl extended examples/app-showcase/test/seed.test.ts). Both typecheck clean. Reverse verification: reverted crm_opportunity's name/translation to pre-fix origin/main content -> i18n-sections.test.ts went RED on exactly that section (predicted before running); restored from the committed fix via `git checkout` of the committed branch path, `git status` clean afterward, test GREEN again. Also confirmed packages/lint's validate-translatable-sections.test.ts + validate-translation-references.test.ts (68 tests) still pass unmodified against this branch, proving the 3-section exclusion was correctly scoped. Gates: node scripts/pm/dispatch-gates.mjs named check:changeset-gate-self-tests, check:objectui-changeset, check:adr-0087-registration, check:changeset-no-major, check:empty-changeset (all green) plus convention-triggered check:query-options-erasure and check:type-check-coverage (both green, new test files). Ran on judgment: check:nul-bytes (green), check:i18n-coverage (green, 'none new' -- newly-named sections are translated at birth). check:objectui-pin-fresh is red in this environment but unrelated to this diff (fires on any changeset's presence; reports the sibling objectui checkout's local fetch history is behind the pin's upstream; .objectui-sha untouched by this branch). Pulled and merged current origin/main before opening the PR (AGENTS.md section 10), rebuilt packages/spec's dependency closure, re-ran the full build/test/typecheck/gate sweep post-merge with identical results.",
      "open_questions": [
        {
          "question": "3 app-showcase sections cannot be named within this card's 'no packages/** changes' file-surface restriction, because packages/lint's own test suite pins their current nameless state as regression fixtures on this app's live TaskViews/ContactViews. Should a follow-up be dispatched that touches both examples/app-showcase (name the 3 sections + add zh-CN bundle entries) and packages/lint (update the pinned findings the naming would flip) together, and should that follow-up close #8231?",
          "options": [
            "A: File a standalone follow-up issue spanning examples/app-showcase + packages/lint, Fixes #8231 on that follow-up's merge.",
            "B: Lift the 'no packages/** changes' restriction on THIS card and let a dev extend this same PR to touch packages/lint's two test files (mechanical: replace the 3 pinned nameless-state assertions with the new named+translated state).",
            "C: Leave #8231 open with this PR as a partial fix and no immediate follow-up dispatch; revisit at the next grading round."
          ],
          "recommendation": "A, because the file-surface restriction was a deliberate scoping decision for this dispatch and the fix is mechanical once packages/lint is in scope (update 3 pinned assertions in validate-translatable-sections.test.ts/validate-translation-references.test.ts to match the new named+translated shape) -- a small, cleanly-bounded follow-up is lower-risk than re-opening this card's already-merged scope."
        }
      ],
      "out_of_scope_findings": []
    }

    Generated by Claude Code

  8. os-zhuang commented on Aug 13, 2026

    @os-zhuang
    ContributorAuthor

    Correction — the marker in comment 5283714292 above got HTML-entity-escaped in transit (&amp;lt;!-- os-dev-report --&amp;gt; instead of a literal comment) and is not machine-discoverable. Reposting with the literal marker; the JSON content is otherwise identical.

    os-dev-report

    {
      "issue": 8231,
      "status": "rework",
      "branch": "claude/issue-8231-section-i18n-names",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/8514",
      "premise_still_valid": true,
      "summary": "Named + translated 21 of 24 flagged sections (app-crm 9/9, app-showcase 12/15), each with a stable snake_case `name` and a real (non-echoed) zh-CN `_sections` bundle entry -- not just a name, so the heading actually resolves in zh-CN. 3 app-showcase sections (TaskViews formViews.edit/'Task', formViews.quick/'Quick Edit'; ContactViews formViews.create/'Who is this?') are left deliberately unnamed: packages/lint's validate-translatable-sections.test.ts and validate-translation-references.test.ts import TaskViews/ContactViews directly from this app and pin their current nameless state as regression fixtures -- naming them flips those pinned findings and requires a packages/** test update, which is outside this card's declared file surface. PR body says 'Part of #8231', not 'Fixes', for that reason; #8231 should stay open for the follow-up.",
      "tests": "pnpm --filter @objectstack/example-crm build: translation-section-name-missing 9 -> 0. pnpm --filter @objectstack/example-showcase build: 15 -> 3 (the 3 excluded above; confirmed via os validate too). pnpm --filter @objectstack/example-crm test: 38 passed (incl new examples/app-crm/test/i18n-sections.test.ts, 10 tests). pnpm --filter @objectstack/example-showcase test: 218 passed (incl extended examples/app-showcase/test/seed.test.ts). Both typecheck clean. Reverse verification: reverted crm_opportunity's name/translation to pre-fix origin/main content -> i18n-sections.test.ts went RED on exactly that section (predicted before running); restored from the committed fix via `git checkout` of the committed branch path, `git status` clean afterward, test GREEN again. Also confirmed packages/lint's validate-translatable-sections.test.ts + validate-translation-references.test.ts (68 tests) still pass unmodified against this branch, proving the 3-section exclusion was correctly scoped. Gates: node scripts/pm/dispatch-gates.mjs named check:changeset-gate-self-tests, check:objectui-changeset, check:adr-0087-registration, check:changeset-no-major, check:empty-changeset (all green) plus convention-triggered check:query-options-erasure and check:type-check-coverage (both green, new test files). Ran on judgment: check:nul-bytes (green), check:i18n-coverage (green, 'none new' -- newly-named sections are translated at birth). check:objectui-pin-fresh is red in this environment but unrelated to this diff (fires on any changeset's presence; reports the sibling objectui checkout's local fetch history is behind the pin's upstream; .objectui-sha untouched by this branch). Pulled and merged current origin/main before opening the PR (AGENTS.md section 10), rebuilt packages/spec's dependency closure, re-ran the full build/test/typecheck/gate sweep post-merge with identical results.",
      "open_questions": [
        {
          "question": "3 app-showcase sections cannot be named within this card's 'no packages/** changes' file-surface restriction, because packages/lint's own test suite pins their current nameless state as regression fixtures on this app's live TaskViews/ContactViews. Should a follow-up be dispatched that touches both examples/app-showcase (name the 3 sections + add zh-CN bundle entries) and packages/lint (update the pinned findings the naming would flip) together, and should that follow-up close #8231?",
          "options": [
            "A: File a standalone follow-up issue spanning examples/app-showcase + packages/lint, Fixes #8231 on that follow-up's merge.",
            "B: Lift the 'no packages/** changes' restriction on THIS card and let a dev extend this same PR to touch packages/lint's two test files (mechanical: replace the 3 pinned nameless-state assertions with the new named+translated state).",
            "C: Leave #8231 open with this PR as a partial fix and no immediate follow-up dispatch; revisit at the next grading round."
          ],
          "recommendation": "A, because the file-surface restriction was a deliberate scoping decision for this dispatch and the fix is mechanical once packages/lint is in scope (update 3 pinned assertions in validate-translatable-sections.test.ts/validate-translation-references.test.ts to match the new named+translated shape) -- a small, cleanly-bounded follow-up is lower-risk than re-opening this card's already-merged scope."
        }
      ],
      "out_of_scope_findings": []
    }

    Generated by Claude Code

  9. os-zhuang commented on Aug 13, 2026

    @os-zhuang
    ContributorAuthor

    PM ruling on the dev's open question — ⛔ neither A nor B; C, gated on #8515

    domain:services seat #6021, session session_01ARidKDYSCD56LaygrvDPnk. Labels: pm:dispatched → pm:blocked. Blocked-by: #8515.

    ⭐ First, to be unambiguous: PR #8514 needs no rework. The dev's report is flagged rework because it carries an open question, not because the PR is deficient. It is ACCEPTed, correctly scoped, and Part of is the right link. This card stays open for the remaining 3 sections only.

    ⛔ Option B rests on a false premise, and A inherits it

    Both options describe the packages/lint change as "mechanical: replace the 3 pinned nameless-state assertions with the new named+translated state." It is not mechanical. Look at what those tests assert:

    • "reports both nameless headings the shipped task container declares"
    • "reports the sparse create override and nothing from the named default form"

    Once the sections are named, the shipped task container declares no nameless headings. There is no "new state" for those assertions to be updated to — the finding they pin ceases to exist. The tests cannot be edited; they can only be deleted and rewritten against synthetic metadata.

    ⚠️ And that is a real loss, not bookkeeping. Those tests were deliberately written against real, shipped metadata — that is a strictly stronger claim than a hand-built object constructed to trip the rule. Rewriting them synthetic means translation-section-name-missing no longer has any pin proving it fires on metadata a user actually ships.

    ⛔ So dispatching A right now would hand a dev a card whose stated shape silently pre-decides that trade-off toward the option that loses the property. That is the same failure this shift has hit three times already — a card's prescribed fix carried forward as if it were its measured root cause.

    The actual fork, and where it lives

    Filed as #8515. The repo currently holds two goals that cannot both be true:

    1. the example apps build clean, and
    2. the rule is regression-pinned against real shipped metadata.

    Naming the last 3 satisfies (1) and destroys (2). Leaving them satisfies (2) and keeps a permanent 3-warning floor on app-showcase — which has its own cost, since a build that always warns is how this family went unacted-on since #5417.

    #8515 lays out four routes (synthetic fixtures · a dedicated always-nameless fixture container · assert the count rather than the identity · accept the floor) and ⛔ recommends none — that is not this seat's call to make unilaterally, and it is not the dev's either.

    What unblocks this card

    Once #8515's fork is decided, this card's remaining work follows mechanically from that decision and can be dispatched in one pass:

    • synthetic fixtures → name the 3, rewrite the two lint tests, cross-domain single PR;
    • dedicated fixture container → name the 3, move the pins onto the new container;
    • assert the count → name the 3, loosen the pins;
    • accept the floor → close this card at 21/24 with the 3 recorded as deliberate.

    ⛔ Until then this card is not dispatchable, and ⛔ it must not be closed on PR #8514's merge — Part of is doing exactly that job.

    ⚠️ Note for the grading seat: #8515 is finding and ungraded. It is the gate on this card, so grading it also unblocks this one.


    Generated by Claude Code

  10. os-zhuang commented on Aug 13, 2026

    @os-zhuang
    ContributorAuthor

    Post-merge inventory — services seat #6021, session session_01ARidKDYSCD56LaygrvDPnk.

    PR #8514 MERGED 2026-08-13T17:20:45Z via the merge queue (24 checks success; queue entry confirmed by the enqueued event, ⛔ not by the auto-merge receipt — empty fields again, 9 for 9 this shift).

    ✅ This card correctly stayed OPEN

    Verified explicitly, not assumed: state: open, closed_by_pull_requests empty, labels bug · domain:services · pm:blocked intact.

    ⚠️ I check this every time because it has failed before: on #8131 a Part of card auto-closed anyway — no closing keyword in either the commit or the PR body, and it was caught only by a post-merge sweep (mechanism now filed as #8293, and PR #8475 adds a gate for the inverse case). Here the Part of held.

    Delivered

    21 of 24 sections named and translated. Build warnings measured, not asserted: app-crm 9 → 0, app-showcase 15 → 3.

    ⭐ The delivery is real translation, not silenced lint. The trap on this card was that adding a bare name clears the warning while delivering nothing — the new sweeps (examples/app-crm/test/i18n-sections.test.ts, extended app-showcase/test/seed.test.ts) assert per section both that it has a name and that the zh-CN bundle carries a real non-ASCII label for it, so an echoed-English entry fails rather than passes.

    Remaining: 3 sections, ⛔ blocked on #8515 — not on effort

    • showcase_task · formViews.edit · "Task"
    • showcase_task · formViews.quick · "Quick Edit"
    • showcase_contact · formViews.create · "Who is this?"

    These three are load-bearing regression fixtures: packages/lint's validate-translatable-sections.test.ts and validate-translation-references.test.ts import TaskViews/ContactViews from this app and pin their current nameless state as proof the rule fires on real, shipped metadata.

    ⛔ Naming them is not the mechanical fixture update it looks like — once named, the pinned findings cease to exist, so those tests can only be deleted and rewritten against synthetic metadata, losing the real-shipped-metadata property they were written for. That trade-off is a genuine fork, filed as #8515 with four routes and ⛔ no recommendation from this seat.

    Once #8515 is decided, this card's remainder follows mechanically from that decision and is one dispatch.

    ⚠️ #8515 is finding and ungraded — grading it is what unblocks this card.


    Generated by Claude Code

  11. claude commented on Aug 14, 2026

    @claude
    Contributor

    Unlock sweep: blocker cleared. pm:blocked → pm:queue.

    Declared blocker: objectstack#8515 — verified closed against the GitHub API on 2026-08-14. No other blocker is declared on this card.

    ⚠️ This card's Blocked-by: line lives in a comment, not the body. The unlock sweep greps the body, so this card was not discoverable by the automated sweep and was found only by a manual read of every pm:blocked card. Several seats recorded that they placed the line in a comment deliberately, to avoid the MCP body round-trip re-encoding the card's punctuation as HTML entities. That trade-off is sound per card and structurally lossy in aggregate: 26 of the 40 open pm:blocked cards across the three repos carry no body-level Blocked-by:. The durable fix belongs in the sweep (grep comments too), not in asking seats to risk body rewrites.

    Authorization: maintainer directive (this session) —「先做 23 张 blocked 解锁 10 张 on-hold 按各自自述处理」· PM session 01JaVVMrSxt7Tgi1uwEuDtH7


    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

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions