Skip to content
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
This repository was archived by the owner on Nov 4, 2026. It is now read-only.

仪表盘聚合图表轴标题 "Count" 未本地化(类别标签已本地化) #7258

Closed

Description

@hotlong

现象

AI 搭建的客户仪表盘「按状态统计客户数量」柱状图:三根柱子的类别标签已本地化(合作中 / 已流失 / 潜在),但 X 轴标题显示英文 "Count"。中文界面下终端用户可见。

修复要求

聚合度量的轴标题/图例走 i18n(count → 数量,或按 measure label),并核对 sum/avg 等其它聚合名的同类字符串。

Activity

  1. self-assigned this
    on Sep 2, 2026
  2. hotlong commented on Sep 2, 2026

    @hotlong
    ContributorAuthor

    Claim: PM loop round 1 (epic objectstack-ai/cloud#1841)
    Session: session_c5c0ce54-bb9c-478c-9e5b-cf44b80d4569
    Branch: claude/issue-7258-chart-axis-count-i18n
    Worktree: objectui-issue-7258
    Domain: domain:ui
    File surface: packages/plugin-dashboard/** and/or the chart renderer that prints the aggregate axis title, plus packages/i18n/** zh entry (stop on breach; explain in the report)
    Container & model: S, mode:subagent, model: sonnet (PM judgement)
    Clause-②: no
    Serial constraints cleared: only dependabot bumps touch plugin-dashboard package.json; #7254 also adds i18n zh entries — different keys, rebase conflicts are mechanical

  3. hotlong commented on Sep 2, 2026

    @hotlong
    ContributorAuthor

    Claim: dev
    Session: session_c5c0ce54-bb9c-478c-9e5b-cf44b80d4569
    Branch: claude/issue-7258-chart-axis-count-i18n
    Worktree: objectui-issue-7258
    Base: origin/main @ 67dadd6

  4. hotlong commented on Sep 2, 2026

    @hotlong
    ContributorAuthor

    os-dev-report
    {
    "issue": 7258,
    "status": "needs_decision",
    "branch": "claude/issue-7258-chart-axis-count-i18n",
    "pr": null,
    "premise_still_valid": true,
    "summary": "The bug is real, but not fixable inside objectui without violating AGENTS.md #0.1 (contract-first). The literal 'Count' does NOT originate in the renderer: for a dataset-bound chart, DatasetWidget/ObjectChart pass queryDataset()'s res.fields straight into buildChartSeries() (packages/core/src/utils/chart-series.ts:441-442, labelOf), which trusts fields[].label as already-resolved author content -- exactly the behavior objectui#4106/ObjectChart.datasetMeasureLabel.test.tsx pins for a REAL custom measure label ('Tasks'). The English 'Count' is minted server-side in objectstack's service-analytics as a hardcoded default for the built-in count aggregate, with no i18n hook, at 5 sites: packages/services/service-analytics/src/analytics-service.ts:2045 and :2300 (inferMeasure), packages/services/service-analytics/src/cube-registry.ts:80 (inferFromObject), packages/spec/src/kernel/metadata-create-seeds.ts:98, mirrored in packages/qa/downstream-contract/src/additional-domains.fixtures.ts:116. None of these are in objectui.",
    "tests": "No source changes made (diagnostic-only; no build/test run applicable). Evidence trail: grep -rn "'Count'" across objectui packages/ found only Studio config-panel enum labels (plugin-dashboard/src/index.tsx:242,318 -- authoring UI, not the rendered axis) and test fixtures -- no renderer-side literal that could produce a capitalized 'Count' axis title. Traced DatasetWidget.tsx:1390 (buildChartSeries(chartRows, dimensions, values, state.fields, ...)) and ObjectChart.tsx:848-856 (same helper) to core/src/utils/chart-series.ts:441-442, whose labelOf() falls back to the raw field NAME (lowercase 'count'), never a capitalized word -- ruling out a client-side humanizeLabel/humanizeFieldKey source (grep confirmed humanizeLabel is applied only to groupBy VALUES in ObjectChart.tsx:143/165, never to measure keys). Cross-referenced against objectstack (sibling checkout) and found the literal 'Count' hardcoded at the 5 producer sites listed in summary, all returning {name:'count', label:'Count', type:'count'} with no locale parameter. Confirmed existing CLIENT-side precedent for localizing this exact closed vocabulary purely by a structural discriminator: packages/i18n/src/locales/{en,zh}.ts 'report.aggregate.{count,sum,avg,min,max,...}' (zh: 'Count'->'计数') consumed by the pivot/matrix report, and 'grid.summary.{count,sum,avg,...}' (zh: 'Count'->'计数') consumed by packages/plugin-grid/src/useColumnSummary.ts -- both keyed by an explicit TYPE discriminator (ColumnSummaryType / report aggregate type), never by matching label text. Searched gh issue list on both repos for an existing duplicate (search: 'aggregate label i18n', 'measure label locale', 'aggregate label Count') -- objectui#4020/objectstack#5141 (axis showed raw NAME instead of label -- opposite direction, already fixed) and objectstack#4330/objectui#4575/#4566 (dimension-value and number-FORMAT locale, not aggregate-name TEXT) are related but distinct; no existing card covers this exact gap.",
    "mcp_calls": "0 -- all GitHub reads/writes used the gh CLI, not MCP GitHub tools",
    "open_questions": [
    {
    "question": "Where should the fix for the unlocalized default aggregate label ('Count'/'Sum'/'Avg'/...) land: a renderer-side heuristic in objectui, or a contract change in objectstack (service-analytics + spec) that objectui then consumes through its existing i18n seam?",
    "options": [
    "A. objectui infers 'this is a built-in aggregate' from the field NAME (e.g. name==='count') or from LABEL-TEXT matching ('Count'), then substitutes a locale string client-side, with no objectstack change.",
    "B. Extend the wire contract: add a structural discriminator to AnalyticsResult.fields[] in packages/spec/src/contracts/analytics-service.ts (objectstack) -- e.g. a builtinAggregate?: 'count'|'sum'|'avg'|'min'|'max'|'count_distinct' field -- populate it ONLY at the objectstack producer sites that currently hardcode the English default (analytics-service.ts:2045/2300, cube-registry.ts inferFromObject), leave it absent for any author-declared custom measure so 'Tasks'-style labels are untouched; queryDataset's REST response threads it through; objectui's buildChartSeries()/labelOf() then prefers a locale lookup keyed by that discriminator (reusing/mirroring the existing report.aggregate.* i18n keys) over the raw label, falling back to label when the discriminator is absent.",
    "C. Localize server-side: objectstack resolves the aggregate's display label by the request's exec.userLanguage (the channel already used for other server-generated user-visible strings per this repo's own doctrine) instead of a bare English literal, so the wire 'label' is already correct and objectui needs no change at all."
    ],
    "recommendation": "B, because long-term soundness (>=50% weight) rules out A outright: name-based inference ('count') risks silently clobbering a real author-named measure literally called 'count' with its OWN custom label, and label-text matching ('Count') breaks the instant the string is anything but that exact English literal (already-resolved non-English text, a renamed default, etc.) -- both are exactly the consumer-side leniency around a producer defect that AGENTS.md #0.1 forbids, and both duplicate a translation responsibility the codebase has already assigned to a STRUCTURAL discriminator twice (ColumnSummaryType for grid.summary., the report's own aggregate type for report.aggregate.), never to label text or naming convention. B extends that exact, already-paid-for doctrine with one optional wire field and reuses an existing FE i18n namespace -- small, structurally sound, and hard for future AI-authored dataset/measure metadata to get wrong (real business need is confirmed by this issue's own repro; startup-scope discipline is satisfied because the diff is one optional field plus about three producer call sites plus one FE consumer, not new capability surface). C is the second choice if the maintainer prefers zero wire-contract change, but it duplicates the FE's is-this-my-language logic on the server and provides no discriminator for a future FE need (e.g. an author wanting to override just the built-in label per-widget); it is viable but a strictly worse fit for the existing architecture than B."
    }
    ],
    "out_of_scope_findings": []
    }

  5. hotlong commented on Sep 2, 2026

    @hotlong
    ContributorAuthor

    PM ruling (maintainer 2026-09-02, in-session): B — structural discriminator on the analytics contract. The producer half is filed as objectstack-ai/objectstack#14492 (spec seat, Clause-②: yes). This card is the consumer half and stays parked until that lands.

    Blocked-by: objectstack-ai/objectstack#14492

    Triage of the dev report: premise valid, no PR (diagnostic-only run) — a good run, not a failed dispatch. Branch claude/issue-7254-style claim stays; re-dispatch once the contract field exists.

    Generated by Claude Code

  6. hotlong commented on Sep 3, 2026

    @hotlong
    ContributorAuthor

    Claim: epic cloud#1841 lane — consumer half re-dispatched in parallel with the producer (objectstack#14492, now in-seat cross-lane per the owner's 2026-09-04 authorization)
    Session: wonderful-driscoll-ac0895 (cloud PM session, Fable 5.1)
    Branch: fix/chart-builtin-aggregate-label-7258
    Worktree: ~/Documents/GitHub/objectui-issue-7258 (linked worktree off origin/main; primary checkout untouched)
    Domain: core chart-series label resolution + i18n locale keys
    File surface: packages/core/src/utils/chart-series.ts (labelOf / buildChartSeries), the i18n locale bundles that already carry report.aggregate.* keys (en + zh), tests beside chart-series; spec dependency bump ONLY if the repo consumes @objectstack/spec by pin and the field is needed at type level
    Container & model: general-purpose subagent reading objectstack/.claude/agents/os-dev.md, inherit tier
    Clause-②: no (consumer side; reads an optional field already ruled in #14492)
    Serial constraints: PR stays draft until objectstack#14492 is merged so the contract exists upstream; code against the optional field structurally so the branch compiles before the spec bump

    Generated by Claude Code

  7. hotlong commented on Sep 3, 2026

    @hotlong
    ContributorAuthor

    os-dev-report
    {
    "issue": 7258,
    "status": "done",
    "branch": "fix/chart-builtin-aggregate-label-7258",
    "pr": "#7535",
    "premise_still_valid": true,
    "summary": "Consumer half of ruling B, DRAFT PR #7535 at 799c84a (kept draft: Depends on objectstack#14492 landing upstream for the field to be populated; inert on spec 17.2.0 by construction). buildChartSeries()/labelOf() now resolves a result field that carries the optional wire discriminator builtinAggregate (closed enum count|sum|avg|min|max|count_distinct, validated at the lookup by isBuiltinAggregate) through a caller-supplied locale map, and otherwise keeps fields[].label verbatim (objectui#4106) then the name; no name/label-text inference. Core stays React/i18n-free, so the map arrives as ChartSeriesOptions.builtinAggregateLabels (the nullCategoryLabel division); a new i18n seam builtinAggregateLabels(tt) reads the existing report.aggregate.* keys (all ten packs already carry them, bundles untouched), and the three chart callers (ObjectChart, DatasetWidget, DatasetReportRenderer) pass it. Type level: @objectstack/spec is consumed by npm RANGE (core ^17.2.0, lockfile 17.2.0; no workspace link, no SHA pin); adding the key ON ChartResultField turned the compile-time pin keyof ChartResultField subset-of spec fields[] (dataset-result-field-spec-parity, TS2344 line 121) red, so the key lives BESIDE it as BuiltinAggregateCarrier and buildChartSeries takes ChartMeasureField[] (ChartResultField intersected with the carrier) so a spec-typed DatasetResultField[] flows in unchanged today and carries the key once the spec does; NO pin bump needed for this PR, follow-up TODO(objectstack#14492) in chart-series.ts to fold the key into ChartResultField after the spec release ships. File-surface breach declared in the PR body: 3 caller edits (one option each), the i18n seam module + index export, and a DatasetWidget surface pin under a real I18nProvider, each necessary so the seam is wired rather than declared. Two standard-text deviations, both from the dispatch word: branch name is the Claim's (fix/chart-builtin-aggregate-label-7258, not the claude/issue-N form) and the PR body ends with the dispatch-mandated footer with the session id carried in prose. CI: green — all 32 check runs on 799c84a completed with 0 failures (29 success incl. Build & E2E, Lint, Type Check, Test shards 1-4, README Export Check, Changeset Declaration, Bundle Analysis; 3 skipped: coverage shards and dependabot), read via REST check-runs (17 polls) after one gh pr checks --watch burst tripped the GraphQL abuse limiter. PR left as DRAFT, not marked ready, not merged.",
    "tests": "Union at 799c84a (final commit; ablation restored to that exact blob first). type-check (tsc --noEmit + tsc -p tsconfig.test.json) for core, i18n, plugin-charts, plugin-dashboard, plugin-report: all five Done, exit 0, after building the plugins' dependency closure; --listFiles shows each new test file in its package's test project (1/1/1). vitest run --maxWorkers=2 over 16 files (3 new + chart-series/dataset-format/spec-parity/label-net siblings + ObjectChart.datasetMeasureLabel + two DatasetWidget label suites): Test Files 16 passed (16), Tests 256 passed (256), exit 0. Ablation after commit: labelOf mutated back to field?.label ?? name (on-disk: marker count 1, removed call 0), core+dashboard pins ran 4 failed | 14 passed (18) — red exactly at zh built-in count (core), every-member, multi-measure, zh built-in count (surface); observed direction note: the en pins stayed green under the mutation because the en locale label IS the wire Count, so they are controls; restore via git checkout HEAD -- path, git hash-object equals HEAD blob bcae7ca08, git diff HEAD empty, marker 0; no dist involved (vitest aliases @object-ui/* to src). ESLint --format json over the 9 ts/tsx files of the diff: exit 0, 0 errors, 74 warnings, 0 on any added line (74 pre-existing: ObjectChart 58, DatasetWidget 11, DatasetReportRenderer 5); narrowing proof: universe = eslint files glob, count = 9 JSON entries, grep -cE projectService|parserOptions|project: eslint.config.js = 0 (control files:|ignores: = 9). check-changeset-presence (9 source files / 5 released packages / 1 changeset), -fixed, -no-major, -overwrite: exit 0 each. check-control-bytes OK (6206 files) + targeted perl scan of diff files 0. check-i18n-call-site-keys, check-i18n-en-drift (no en value changed), check-phantom-dependencies: exit 0. NOT MEASURED locally: check:readme-exports (257 type-entry-not-on-disk, 22 unbuilt packages) and check:spec-floors (12 findings all no-artifact on unbuilt packages; the 19 built ones incl. all five touched judged 185 pairs clean) — CI measures both on a full build. os-verify-lock.sh ran in DECLARED UNLOCKED MODE (no flock on this macOS host), nothing serialized.",
    "mcp_calls": "0 — every GitHub read/write went through the gh CLI; one gh pr checks --watch burst tripped the GraphQL abuse limiter, after which CI was read via REST check-runs only",
    "open_questions": [],
    "out_of_scope_findings": [
    "filed as #7534: buildDatasetFieldHelpers().headerLabel (KPI caption, table/pivot headers, report metric caption, app-shell DatasetPreview) still prints the wire Count — same class one seam over, mechanical follow-up with resolveMeasureLabel across five call sites, Blocked-by objectstack#14492"
    ]
    }

  8. hotlong commented on Sep 3, 2026

    @hotlong
    ContributorAuthor

    ACCEPT (pending producer) — PR #7535 verified in-seat: 10 files, all inside the declared surface plus the three one-option caller edits and the i18n seam the PR body declares; 29 checks success / 3 skipped / 0 failed at 799c84a; consumer reads the optional builtinAggregate structurally (no spec pin bump needed — @objectstack/spec is an npm range here and the cloud image links the pinned framework at build time, so the field arrives via the cloud pin bump). Order: objectstack#15017 merges first → this PR goes ready + queue → cloud .objectui-sha bump. #7534 (KPI / table / pivot captions, same class) is tracked for the follow-up batch.

    Generated by Claude Code

  9. hotlong commented on Sep 3, 2026

    @hotlong
    ContributorAuthor

    Producer objectstack#15017 is ready and in the objectstack merge queue (clause-② PASS, all checks green), so #7535 is now marked ready and enqueued as well. The behaviour switches on once cloud's .objectstack-sha and .objectui-sha bumps carry both sides (epic cloud#1841).

    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