fix(service-analytics): translate select dimension option labels on dataset charts - #17061
Conversation
…ataset charts A dataset's select-field dimension rendered its option label straight out of authored field metadata (opt.label), which is never locale-aware — the option's label is always the author's own-language text (SelectOptionSchema.label is a plain string, never an inline locale map). A dotted cross-object dimension (field: 'contract.direction') was unaffected because a relationship-path field name never matches a key in the base object's own field map, so resolveDimensionLabels skips it entirely before either the select or lookup branch runs. DimensionLabelDeps gains one new optional capability, translateSelectOptions, wired in plugin.ts by calling the existing translateObject (@objectstack/spec/system) against the deployment's i18n bundle -- the same translator GET /meta/object/:name already uses, so a chart's category labels now match what the list grid renders for the identical field. No i18n service configured falls back to exactly today's authored-label behaviour. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012zTkyNHJ7TkuN2oXtP5x37
…lect-dimension-label-translation
📓 Docs Drift CheckThis PR changes 1 package(s): 8 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 9 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 95c586b741b72ce8533db91f944cf08acb10417c && git checkout 95c586b741b72ce8533db91f944cf08acb10417c
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin edf59e3599a3324598ae6cabbc72be322fe0d287 9b4c0af7f4164d3f6a99c7fb42c80785559995b6 && git checkout -B drift-repro edf59e3599a3324598ae6cabbc72be322fe0d287 && git merge --no-ff 9b4c0af7f4164d3f6a99c7fb42c80785559995b6
node scripts/docs-audit/affected-docs.mjs --json edf59e3599a3324598ae6cabbc72be322fe0d287
|
…ished DimensionLabelDeps DimensionLabelDeps is re-exported wholesale from index.ts, so translateSelectOptions is a new key on a published exported type -- the mechanical floor for clause 2 regardless of whether the key is optional. Backward compatible (additive, no removed/renamed key, no wire-shape change), so minor rather than major. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012zTkyNHJ7TkuN2oXtP5x37
|
Director seat adoption record — summon #20, Contract review (
|
|
Landing provenance — director seat takes the release action under the maintainer's 13:4xZ instruction 「把当前的契约复审全部处理完」 (
Generated by Claude Code |
Fixes #16773
Clause-②: yes
DimensionLabelDepsis re-exported wholesale frompackages/services/service-analytics/src/index.ts,so
translateSelectOptionsis a new key on a published exported type — the wire shape andthe accept/refuse behaviour are otherwise unchanged, and a host wiring no
i18nservice fallsback to today's authored-label behaviour, but the published-type surface itself grew and that
is the mechanical floor for this clause regardless of the key being optional. Corrected from
this PR's own provisional
no— see "Clause-② correction" below for the measurement.Reproduction
Re-driven at code level (no browser/hotclm access from this seat): a unit-level reproduction
against
resolveDimensionLabelsonorigin/mainconfirmed both halves of the correlation:field: 'direction'): the raw stored value (purchase) isunconditionally overwritten with the field's AUTHORED
options[].label(Purchase) —locale-blind, every time.
field: 'contract.direction'):resolveDimensionLabelslooksup
fields[dim.field]against the BASE object's own field map, keyed by plain field names —a dotted relationship path never matches a key there, so
metaisundefinedand the wholeper-dimension body (
if (!meta) continue) is skipped, on both strategies (the field namereaching this function is the dataset-authored string verbatim, unaffected by which strategy
resolved the query). The row's raw value passes through completely untouched.
This is measured, not assumed — see the ablation below and the new
select option i18n (#16773)describe block indimension-labels.test.ts, including adedicated control test for the dotted-field-name mismatch.
Which path the dotted arm takes
No path inside this package. The dotted arm's translated rendering (per the card) is not
produced by anything in
service-analytics— this file's select AND lookup branches bothrequire a
fields[dim.field]hit, which a dotted field name structurally cannot produceagainst the base object's field map. Whatever renders 采购/销售 for the dotted arm does so
downstream of this package, off the untouched raw machine value this file leaves behind.
Routing vs. teaching
Routing the same-object arm through "leave it raw, like the dotted arm" was rejected: this
file's own docblock states the select/lookup resolution exists because raw values are not
human-readable at all (not merely un-translated), and removing it would regress every
locale — including the plain default/English case, and any consumer that isn't a smart
client capable of its own value→label mapping. That is a wire-contract downgrade with no
verifiable safety net (objectui is a separate repo, not available from this seat to confirm),
well past "no behaviour change beyond label resolution."
Instead this PR routes the same-object arm through the repo's one existing "translate a
select option label" implementation —
translateObject(@objectstack/spec/system), thesame function
GET /meta/object/:namealready calls, which is where the console's list gridgets its translated option labels.
DimensionLabelDepsgains one new optional capability,translateSelectOptions(objectName, fieldName, options, locale), implemented inplugin.tsbybuilding a
TranslationBundlefrom the registeredi18nservice (mirroringRestServer.buildTranslationBundle,packages/rest) and callingtranslateObjecton aone-field
ObjectLikedoc. No new spec export, no new wire key:translateObject,ObjectLike,ResolveOptionsandTranslationBundleare all already-published@objectstack/spec/systemexports (confirmed againstpackages/spec/api-surface/system.json).A kernel with no
i18nservice registered — or nothing for the requested locale — degrades toexactly today's authored-label behaviour (regression tests pin this fallback).
The dotted arm is untouched by construction: it never reaches the modified branch (
if (!meta) continuefires first), not merely by intent — pinned bya dotted cross-object field name never matches the base object field map — left untouched, translateSelectOptions never consultedindimension-labels.test.ts.How #16390's future
LOOKUP_TYPESwidening inherits thisNot automatically, and that is stated plainly rather than left for the next seat to find:
lookup/master_detail labels resolve through the separate
fetchRecordLabelscapability (arelated RECORD's display name, read live off the referenced object), which this PR does not
touch.
translateSelectOptionsonly ever applies to a field's authoredoptions[]— adifferent bundle address (
objectsdot OBJECT dotfieldsdot FIELD dotoptionsdotVALUE) than a record's display name has no translation bundle entry at all today.
What #16390 does inherit for free: the i18n service bridge this PR adds to
plugin.ts(
i18nService()/buildTranslationBundle(), both private helpers scoped to this file) isalready wired to
ctx.getService('i18n'). Adding translated lookup/master_detail labels lateris a
ctx.getService('i18n')away rather than a fresh integration — but it is not free today,and #16390's card should say so when it lands.
Ablation (mutation reached disk, proven; restored, proven)
Predicted BEFORE running: stripping the
deps.translateSelectOptions?.(...)consultation outof the select branch turns exactly 2 tests red (the two asserting a translated result) and
leaves all other tests — including both explicit fallback tests and the dotted-arm control —
green, since the mutation only removes a conditional read that those tests don't exercise.
Measured exactly the predicted 2 red / 25 green. Restoration proven by blob equality AND an
empty
git diff HEAD, both under atrap ... EXIT INT TERM, never by reading an exit code.Full suite re-confirmed green after restore (98 files / 2195 tests, post-merge).
Test resolution path
packages/services/service-analytics/vitest.config.tsdeclares onlydisableConsoleIntercept: true— noresolve.alias, notest.projects[], no root-levelvitest.workspace.ts. Test files import the fix by relative path (../dimension-labels.js),which Vite/vitest transforms straight from
src/*.tson the fly. Resolution is throughsrc,not
dist— confirmed by absence of the aliasing shape a sibling delivery found elsewheretoday (
packages/qa/dogfood's inert top-levelresolve.alias), not assumed.Clause-② correction
This PR's own claim carried a provisional
Clause-②: no, reasoned from its own fence text("a new exported symbol, a new key on a published payload, or any
packages/spec/src/**path") — none of which
translateSelectOptionsis: it is a new MEMBER on an EXISTING exportedsymbol, not a wire payload key, and touches no
packages/specpath. That reading missed abroader rule this repo already ships under (#16778): a new key on a published exported
TYPE is the clause-② floor on its own, optionality included, because
DimensionLabelDepsis re-exported wholesale from
index.ts(confirmed:export type { DimensionLabelDeps, ... } from './dimension-labels.js') — so any downstream package implementing it now sees a widerpublished shape, whether or not the wire ever carries it.
Measured, not argued:
node scripts/pm/check-widening-tells.mjs --declaration no --diff -onthis PR's full diff exits 0 ("no widening tell on any declared surface") — the script's
T1/T2 tells are scoped to
packages/spec/src/**and its T3 tell topackages/spec/api-surface/**, so aservice-analyticsinterface member is outside everysurface it mechanically checks; this is a known, accepted gap in the tool (documented false
negatives), not evidence against the
yesreading. #16778's owncheck-widening-tells --declaration noexit-4 came from an unrelatedT2hit onpackages/spec/src/migrations/registry.tsin that same diff — its
DatasetCompileOptions.declaredFieldTypeprecedent was argued by handin that PR's body too, under the identical rule, never caught by this script. Declaring
yeshere is the same hand-argued rule applied consistently, not a mechanically-forced outcome.
Scope discipline
control test pins it.
AnalyticsResult's wire shape (rows,fields[]) is unchanged; only a select dimension's rendered VALUE for a row can differ, andonly when an
i18nservice is registered.packages/spec/src/**change, no new payload key, no wire-shape change — but thepublished
DimensionLabelDepstype does grow by one optional key, which is what flipsClause-② to
yes(see above).content/docs/releases/**untouched.Changeset
minoron@objectstack/service-analytics— additive, backward-compatible (no removed orrenamed key, no wire-shape change), but the published
DimensionLabelDepstype gained a key,which this repo grades at least
minorregardless of the new key being optional (#16778).Not
major: nothing an existing implementor ofDimensionLabelDepswrote stops compiling orbehaving as before. No ADR-0087 disposition: the changeset declares no breaking change.
Gate reconciliation
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 58command(s) from the diff (
.changeset/dataset-select-dimension-option-i18n.md,dimension-labels.ts,dimension-labels.test.ts,plugin.ts) against a freshly-fetchedorigin/main. All 58 ran: 55 passed; 3 (check:dual-build-cjs-loads,check:lean-entry-closure,check:type-check-debt) exited 3PREREQUISITE NOT MET— each explicitly needs a FULLmonorepo
pnpm build(dozens of unrelated packages with nodist/, e.g.@objectstack/hono,@objectstack/account,@objectstack/client), which is CI's job, not local scope. NOTMEASURED, not a finding — named rather than guessed at.
check:route-envelope(Silent-bucketper #16828) was run explicitly: PASS, unaffected (no REST route touched).
Local verification performed
pnpm --filter '@objectstack/service-analytics^...' build— green, bothbefore and after merging
origin/mainin.pnpm --filter @objectstack/service-analytics build && typecheck && test— green (98 testfiles / 2195 tests) after the merge.
DimensionLabelDeps(grepped); the six consumers of@objectstack/service-analytics(rest,runtime,qa/dogfood,cli,client,verify)reference only
AnalyticsServicePlugin's registration identity, not this interface, so noconsumer-side typecheck is owed for an additive optional member.
🤖 Generated with Claude Code
https://claude.ai/code/session_012zTkyNHJ7TkuN2oXtP5x37