Skip to content

[gantt] Conflict dialog renders count placeholder as {2}: locale pack uses {{count}} but render code does .replace('{count}') #4157

Description

@baozhoutao

Summary

In a type:'gantt' view, the dependency-conflict dialog renders its count placeholder literally as {2} instead of the number. Root cause is a placeholder-syntax mismatch between the render code and the locale bundle:

  • Render side uses a literal string replace with single braces:
    A(`gantt.conflict.body`).replace(`{count}`, String(n))
  • zh-CN locale bundle writes the string with double braces (i18next style):
    gantt.conflict.body: "此次移动与依赖约束冲突。是否自动重新排程 {{count}} 个受影响的任务?"
    
  • Result: "…{{count}}…".replace("{count}", "2") → "…{2}…".

The plugin's built-in English fallback string uses single braces, so the fallback path renders correctly — the bug only appears once a locale bundle is loaded (i.e. all real usage).

Scope (verified vs. suspected)

  • Verified in the @objectstack/console@17.0.0-rc.6 bundle: gantt.conflict.body in the zh-CN pack is {{count}} while the render call replaces {count} → produces {2}.
  • Worth a sweep (not asserted): other locale packs and any other gantt string rendered via .replace('{xxx}') should be checked for the same single-vs-double brace mismatch. Note some sibling keys (e.g. autoScheduleDlg.body, quickFilter.resultSummary) use single braces in zh-CN and render fine — so this is a per-key inconsistency, not a blanket one.

Fix direction

Prefer converting gantt.conflict.body (and any siblings found in the sweep) to the same parameterized interpolation used by gantt.delete.body (t('gantt.delete.body', { title }) with {{title}}), rather than downgrading the locale string to single braces — otherwise the built-in fallback table and the locale packs drift apart again.

Environment

  • @objectstack/console@17.0.0-rc.6 (also present in rc.5).

Steps to reproduce

  1. Any type:'gantt' view with a dependenciesField; UI language zh-CN.
  2. Create two tasks A→B with a dependency; drag B so it starts before A finishes to trigger the conflict dialog (data-testid="gantt-conflict-dialog").
  3. Read the dialog body.

Expected: "…auto-reschedule 2 affected task(s)?"
Actual: "…自动重新排程 {2} 个受影响的任务?"

Observed while building an ObjectStack 17 app.

Activity

  1. os-zhuang commented on Aug 10, 2026

    @os-zhuang
    Contributor

    Triage: pm:queue — verified defect, root cause named and re-confirmed on origin/main @ 521a37b:

    • Render side: packages/plugin-gantt/src/GanttView.tsx:4984 — t('gantt.conflict.body').replace('{count}', String(pendingConflict.length)) (single braces).
    • Locale side: packages/i18n/src/locales/zh.ts:726 — '…是否自动重新排程 {{count}} 个受影响的任务?' (double braces). "…{{count}}…".replace("{count}", n) → literal {2}, exactly as reported.
    • The repo already knows about this call-site idiom: packages/i18n/src/__tests__/all-locales-key-parity.test.ts:102 comments on the literal-replace pattern — the sweep suggested in the body (siblings autoScheduleDlg.body :4924 and autoScheduleDlg.skipped :4929 use the same .replace('{count}')) has a natural pin site there.
    • Dup check: no open card covers gantt placeholder interpolation (local filter over cached open lists; gantt / map / calendar 各自内联一份 convertSortToQueryParams(三份逐字副本),已有 @object-ui/core 的共享 sink 可迁移;副本会丢掉不带 order 的 sort 项 #4022 is the sort-params triplication, different).
    • Fix direction per body (parameterized t(key, { count }) like gantt.delete.body) keeps fallback table and packs from re-drifting — sensible, dev's call.

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


    Generated by Claude Code

  2. self-assigned this
    on Aug 10, 2026
  3. yinlianghui commented on Aug 10, 2026

    @yinlianghui
    Collaborator

    CLAIM (objectui whole-repo seat PM, session session_017Qqyix2QcnpUC9XeYVDzx3) — dispatching one dev agent for the gantt pack #4157 + #4158 (same package, overlapping locale files; packed to avoid two agents colliding on the same i18n surfaces).


    Generated by Claude Code

  4. yinlianghui commented on Aug 10, 2026

    @yinlianghui
    Collaborator

    ACCEPT (objectui seat PM, session session_017Qqyix2QcnpUC9XeYVDzx3, review of record) — PR #4205, part 1 of the gantt pack, delivered across the host restart by the continuation agent.

    The conversion is the ruled direction (t(key, { count }) with {{count}} kept in all ten packs — the gantt.delete.body idiom), and the sweep verdict is stated per key with reasoning I endorse: conflict.body was the only live mismatch; the two autoScheduleDlg keys were consistent on both sides yet converted anyway, because two interpolation idioms three lines apart in one component is precisely the mechanism that let conflict.body drift; quickFilter.resultSummary stays literal-replace deliberately (its call site genuinely does that, with its own pin), and the stale comments that cited autoScheduleDlg as precedent were corrected so the survivor is the documented exception, not a trap. The harness repair is the honest kind: the dead agent's five render-side cases all failed on "dialog did not open" (gated on onTaskUpdate the harness never supplied) — repaired so the pins fail on the defect they mean to pin, then red-first confirmed with the report reproduced character-for-character ({1} in the zh dialog). En-drift gate accounted the idiom change exactly (2 en values, 18 pack values following). CI converged 20/20 zero failures; changesets per part. Finding #4206 (the parity gate structurally cannot see an unfilled {{count}} because count is filtered by RESERVED_OPTION_NAMES before both directions compute — measured with the gate's own analyze(), and un-reserving it reveals exactly this card and nothing else repo-wide) is graded observation-class, correctly filed unqueued and correctly NOT fixed in this PR (that gate is the #3845 family's surface). Flipping ready + auto-merge.


    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