Skip to content

i18n: TranslationBundle has no surface for screen-flow UI — wizard screens render source-language only, even in a fully localized app #7646

Closed

Description

@yinlianghui

Context

HotCRM (main 333259a4, @objectstack 17.0.0-rc.5) completed all four locales and retired its i18n exemption ledger (hotcrm#645/#679). Yet its lead-conversion wizard (lead_conversion, type: 'screen') renders entirely in English on a zh-CN console:

  • screen title: Conversion Details
  • field labels: Create Opportunity? / Opportunity Name / Opportunity Amount
  • runner buttons: Cancel / Submit

Root cause

The TranslationBundle schema (@objectstack/spec translation.zod) exposes surfaces for objects, apps, dashboards, actions — there is no flows / screen surface at all. An app author has no place to put translations for screen titles or screen field labels: this is not a missing key, it is a missing vocabulary.

$ grep -oE '(flows?|screens?|apps|objects|views|dashboards|actions)[?:]' translation.zod-*.d.ts | sort | uniq -c
  6 actions:   4 apps:   3 dashboards:   4 objects:   # no flows, no screens

Expected

A flows surface in the bundle (per-flow: label, per-screen: title, per-field: label/placeholder/help), applied by the screen-flow runner the same way object/field labels already are. The runner chrome (Cancel/Submit) presumably belongs to the console's own message catalog instead.

Repro

zh-CN console → HotCRM → any lead → 转化线索 → wizard is English top to bottom.

Filed from HotCRM main; happy to wire the HotCRM-side bundle entries as soon as the surface exists.

Activity

  1. added
    domain:specDomain lane — seat card indexed by label:pm:seat
    needs-user-decisionBlocked on a maintainer decision — do not dispatch; decision cards only, never on a PR
    on Aug 11, 2026
  2. os-zhuang commented on Aug 11, 2026

    @os-zhuang
    Contributor

    Triage: needs-user-decision + domain:spec.

    本评论来自分诊座位 Routine(#5474 试点),不构成认领。


    Generated by Claude Code

  3. os-zhuang commented on Aug 11, 2026

    @os-zhuang
    Contributor

    Maintainer ruling recorded 2026-08-11 (spec-lane PM session chat, verbatim: 「接受你的建议,开始加速处理」, accepting the lane sweep's recommendation on this card).

    Ruling: direction accepted. TranslationBundle gains a flows surface — per-flow label; per-screen title; per-field label/placeholder/help — applied by the screen-flow runner the same way object/field labels already are. Runner chrome (Cancel/Submit) stays in the console's own message catalog, NOT the bundle. Contract-first split: this card is the spec surface; the objectui runner-application card and the HotCRM wiring are downstream, filed by their consuming seats once this lands.

    State: needs-user-decision → pm:queue (spec lane; dispatches at the next batch slot).


    Generated by Claude Code

  4. added
    pm:queueReady for the PM dispatch loop
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    needs-user-decisionBlocked on a maintainer decision — do not dispatch; decision cards only, never on a PR
    pm:queueReady for the PM dispatch loop
    on Aug 11, 2026
  5. self-assigned this
    on Aug 11, 2026
  6. os-zhuang commented on Aug 11, 2026

    @os-zhuang
    Contributor

    Claim: PM loop round 2 (spec lane — slot freed by #7657's merge)
    Session: session_01JY2Q5Xto1u8YHADgrZDTnk
    Branch: claude/issue-7646-translationbundle-flows-surface
    Worktree: cloud dev session (own container)
    Domain: domain:spec
    File surface: packages/spec translation schema family (translation.zod — locate the source; the card quotes the dts) + pins + generated closure. ⛔ The screen-flow RUNNER application (objectui) and HotCRM bundle wiring are downstream cards, NOT this one. Stop on breach; explain in the report.
    Container & model: M (new contract vocabulary surface under a direction ruling — shape design within a ruled boundary), mode:cloud, model: opus
    Serial constraints cleared: no in-flight writer on the translation schema family (open PRs + queue checked 12:55Z); in-flight lane cards touch secret-fields / i18n-resolver+protocol / component.zod / userActions / filter.zod — disjoint. Note #7634 (in flight) touches packages/spec/src/system/i18n-resolver.ts — i18n-ADJACENT but a different file face; both devs' surfaces declared, no overlap expected.


    Generated by Claude Code

  7. os-zhuang commented on Aug 11, 2026

    @os-zhuang
    Contributor

    OS-DEV-REPORT — #7646

    {
      "issue": 7646,
      "status": "done",
      "branch": "claude/issue-7646-translationbundle-flows-surface",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/7763",
      "premise_still_valid": true,
      "summary": "Premise re-verified on origin/main @2f8328c: translationDataShape() declared objects/apps/messages/globalActions/dashboards/pages/settings/metadataForms/settingsCommon and zero flows/screens keys, so a wizard's copy had no authoring slot and .strict() refused any group a translator invented. Added a `flows` group to the SHARED shape, so it lands on both doors at once (file-authored bundle + `translation` metadata item) — flows.<flow_name>.label, .screens.<node_id>.title, .screens.<node_id>.fields.<field_name>.{label,placeholder} — strictObject at all three levels with the family's surface/history/aliases/guidance conventions and every key optional (partial-locale semantics). Addressing was MEASURED, not assumed: Flow.name (flow.zod.ts:614), FlowNode.id (flow.zod.ts:263, forwarded verbatim as ScreenSpec.nodeId at contracts/automation-service.ts:138 — the same id that correlates a resume back to its pause point), ScreenFieldConfig.name (builtin-node-config.zod.ts:382, forwarded as ScreenFieldSpec.name); a test derives every bundle key from a parsed FlowSchema so the surface cannot be keyed by names nothing produces. Per-field face measured against ScreenFieldConfigSchema and one of the ruling's three keys FALSIFIED: `help` has no counterpart at all (the field is name/label/type/required/options/defaultValue/placeholder/visibleWhen), so declaring it would parse clean and translate nothing — the ADR-0078 shape #6080 kept out of the page-component face — and unlike #6080 there is no honest alias target either, since `placeholder` is the in-input hint rather than help text; it rides `guidance` instead, alongside `options` (ScreenFieldConfig.options[].value is z.unknown(), so a value-keyed map cannot address them). Runner chrome stays out of the bundle, pinned by a test. Liveness row is `planned` + authorWarn/authorHint, drilled over label/screens (app.navigation.runAction precedent), because no shipped runner reads the group yet. Changeset @objectstack/spec MINOR: the accepted authoring surface widens additively — a bundle previously rejected for carrying `flows` now parses; no existing shape changes meaning, so it is not a patch and not a break.",
      "tests": "All FOREGROUND, under the shared flock and NODE_OPTIONS=--max-old-space-size=4096. `pnpm --filter @objectstack/spec test -- --maxWorkers=2` → 'Test Files 378 passed (378) / Tests 9947 passed (9947)'. translation.test.ts alone → 110 passed (12 new, in a `screen-flow copy (#7646)` block). `pnpm --filter @objectstack/spec typecheck` → clean; test layer: 'OK — compiles under tsconfig.test.json; 57 file(s) / 265 error(s) held in test-typecheck-debt.json' (pre-existing shrink-only debt, unchanged by this PR). REVERSE VERIFICATION, direction predicted before running as PLAIN RED (the surface is new and its pins address it directly, so no inversion was possible): fix removed via patch file (`git diff > schema.patch; git checkout -- <file>`) — NEVER git stash — gave 'Tests 11 failed | 99 passed (110)', the failures being the fully-populated parse, the partial-locale parse, the addressing pin, both alias/guidance pins, the JSON-Schema describe pin, the item-door pin and the all-groups-together pin; `git apply` restored 110/110. GATES: check:liveness → first run '✗ 1 UNCLASSIFIED — translation/flows', then '✗ 1 UNDECLARED container inheritance', both closed by the ledger row + one-level drill + gen:liveness-counts, final run '✓ every governed-type property … is classified … every container inheritance is declared' with 'translation 21 classified (live 19, planned 2)'. check:generated → first run named 3 stale artifacts (gen:api-surface / gen:docs / gen:strictness-ledger), regenerated after `pnpm --filter @objectstack/spec build`, final '✓ All 13 generated artifacts are up to date'. check:authorable-surface → green, 1590 schemas (the baseRev lag line is informational, per its own text 'Not an error either way'). check:merge-driver / check:adr-anchors / check:spec-parsed-alias / check-nul-bytes → all exit 0 (verified with explicit echo EXIT=$?, not by pipe status). check:i18n → first run 'PREREQUISITE NOT MET — the workspace CLI is not built'; after `turbo run build --filter=@objectstack/cli` → 'OK (9 package(s) — all bundles in sync, no undeclared authoring keys)'. Self-scanned the touched files for control bytes beyond the gate's surface (grep -naP '[\\x00-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]') → no hits. CI on PR #7763: NOT polled — status in_progress at report time, per the draft-PR-time reporting contract (#6644 L2).",
      "open_questions": [
        {
          "question": "Three further screen-flow strings are authorable and user-visible but are OUTSIDE the recorded ruling's enumeration, so I implemented the ruled face exactly and did not widen. Should the flows surface grow to cover them? (a) `screens.<node_id>.description` — ScreenConfigSchema.description, the body text under the screen heading; (b) `flows.<name>.successMessage` / `.errorMessage` — FlowSchema declares both and AutomationResult carries them to the runner as the terminal toast; (c) `screens.<id>.fields.<name>.options` — select-option labels, which need an addressing decision because ScreenFieldConfig.options[].value is z.unknown().",
          "options": [
            "A — leave the face as ruled; each of the three is refused by name with a guidance sentence saying it is not on this surface (what this PR does).",
            "B — widen now to (a) and (b) in a follow-up commit on this PR, leaving (c) for its own addressing ruling.",
            "C — widen to all three, deciding (c)'s addressing (e.g. String(value)-keyed with a documented coercion, or an index-keyed array).",
            "D — decide nothing until the objectui runner card measures which of the three the runner actually draws."
          ],
          "recommendation": "A now, B as the follow-up card, and 裁决不可重裁 is why: the ruling enumerated per-flow label / per-screen title / per-field copy, and widening a ruled contract surface unilaterally is exactly what the escalation path exists to prevent — so this PR ships the ruled face and reports the measurement instead of acting on it. On the three axes: real business need is REAL but unproven for (a)/(b) — HotCRM's filed repro lists the screen title and three field labels and no body text or toast, so the pull is inferred rather than measured, and the honest next step is for the HotCRM seat to say whether its wizard authors them; long-term soundness favours widening in ONE further ruled step rather than key-by-key, since each addition re-cuts the same generated closure and ledger; AI-authoring safety already holds either way, because every excluded key is refused loudly with a guidance sentence rather than silently stripped — the failure mode is a visible rejection, not a translation that vanishes. (c) should not ride (a)/(b): its cost is an addressing decision on unconstrained values, not a key."
        },
        {
          "question": "The resolver-side half — a `FLOW_SCREEN_COPY_KEYS` sibling of `PAGE_COMPONENT_COPY_KEYS`, which drives both translatePage's overlay and the CLI's skeleton extraction — was deliberately NOT written here, because it lives in packages/spec/src/system/i18n-resolver.ts and #7634 is in flight on that file (flagged in the dispatch prompt). Who picks it up?",
          "options": [
            "A — fold it into the downstream objectui runner-application card, which is what will consume it.",
            "B — file a standalone spec follow-up with `Blocked-by: #7634` so it lands the moment that file is free."
          ],
          "recommendation": "B, and it is closer to ready than it looks: the pattern is fully worked out by #6080 (a const key list, a translate* overlay reading it, the CLI extractor reading the same list, and a test pinning the list against the schema so neither can drift), so it is a mechanical port rather than a design. Doing it as its own card also keeps the objectui runner card free to be about rendering. I did not file it — a `Blocked-by:` filing against an in-flight sibling is the PM's race to arbitrate, not mine."
        }
      ],
      "out_of_scope_findings": []
    }

    Notes the JSON does not carry:

    • No issues filed. Nothing off-card broke; both items above are decision-class on the surface this PR just built, so they are open_questions for adjudication rather than findings.
    • packages/lint/src/validate-translation-references.ts is untouched and correct as-is: its own header states that "an unrecognised top-level namespace is skipped, never reported", so flows is inert there rather than broken. Reference validation for flow/screen/field keys is a natural third follow-up, in the same lane as the resolver half.
    • PR-subscription posture. This cloud session was auto-subscribed to its own draft PR with the standard drive-to-green text. Per os-dev.md (maintainer ruling 2026-08-11, [finding] Cloud dev sessions get auto-subscribed to their own PR with a drive-to-green posture that contradicts os-dev L2 (report-at-draft; PM owns CI convergence) #7512), the dispatch contract outranks it: reporting at draft-PR time, no CI idle-polling, no self check-ins. Nothing beyond the standard subscription text was involved. CI convergence, the ready-flip and landing are the PM's.

    Generated by Claude Code

  8. os-zhuang commented on Aug 11, 2026

    @os-zhuang
    Contributor

    Addendum to the report above (not a second report — the JSON in that comment stands, and its <!-- os-dev-report --> marker was read back intact).

    One further commit landed on the same branch after the report, c0ebc18 — content/docs/ui/translations.mdx only, so PR #7763 is now 2 commits / 9 files. The docs-drift check fanned out over 106 pages (advisory, and mostly the @objectstack/spec dependency graph rather than this diff), but one of them is this surface's own reference page: it enumerates every translatable surface in a table and keeps a "Current boundaries" honest-limits list. Adding a group without touching it would have left the page describing a bundle that no longer exists — the drift this repo's docs-accuracy discipline is aimed at — so it gains the flows row plus a boundary bullet stating what is true today: declared and addressed the way the runner resolves it, no shipped runner reading it yet (liveness planned, compile lint warns), no help text on a screen field to translate, and runner chrome in the console's message catalog. No release-owned page touched. check:generated re-run green after the commit.

    Everything else is unchanged: still a draft PR, CI still not polled, and CI convergence / ready-flip / landing remain the PM's. Worktree torn down (unforced git worktree remove, node_modules deleted first) and no background process or monitor left running.


    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

domain:specDomain lane — seat card indexed by label:pm:seatpm:dispatchedDispatched to a dev agent by /pm-dispatch

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions