Uh oh!
There was an error while loading. Please reload this page.
fix(plugin-gantt): resolve a locale-map label in the gantt export filename - #6233
Merged
yinlianghui merged 1 commit intoAug 25, 2026
Merged
Conversation
`ObjectGantt` handed `schema.label` straight to `String(...)` when building `exportFileName`. `BaseSchema.label` is `string | I18nLabel` since #4580's revised Q1-A ruling, so a gantt whose label was authored as the spec's inline locale map exported `[object Object]-<stamp>.png`. Resolve it with `resolveI18nLabel` from `@objectstack/spec/ui` — the producer's own resolver for that vocabulary — against the display locale already in scope via `useDisplayLocale()`. The resolver answers `undefined` on a miss, so it sits inside the `??` chain and a missing label falls through exactly as before. `objectSchema?.label`, the next link, is deliberately untouched: it is the DATA object's label, declared `z.string().optional()` on the spec's `ObjectSchemaBase` (a `strictObject`), so a locale map there is rejected by the producer rather than resolved here. Part of #6052
Contributor
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
yinlianghui
marked this pull request as ready for review
August 25, 2026 03:18
Uh oh!
There was an error while loading. Please reload this page.
yinlianghui
deleted the
claude/issue-6052-gantt-export-filename-i18nlabel
branch
August 25, 2026 03:30
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes#6052
What was wrong
ObjectGanttbuilds theexportFileNameit handsGanttViewfrom a??chain whosesecond link is
schema.label, wrapped in a bareString(...).BaseSchema.labelisstring | I18nLabelsince #4580's revised Q1-A ruling —I18nLabelbeing the spec'sINLINE locale MAP — so a gantt authored as
{ "type": "object-gantt", "objectName": "task", "label": { "en": "Shift Plan", "zh-CN": "排班计划" } }exported
[object Object]-20260825-0256.png. That filename is not a reconstruction: it isthe literal string this branch's ablation run produced (below).
Premise re-verified on the merged ref, as triage asked
00f3eb5b9. Its only touch at this call site was dropping the cast:(schema as any).label→schema.label— measured withgit log -L 1504,1512, the diffis that one token. Behaviour-preserving, exactly as the card asserts; the defect is
live and pre-existing, and nothing here is already-resolved.
mainis verbatim what the card quoted.useDisplayLocale()is in scope atObjectGantt.tsx:438— feat(types,plugin-gantt): declare the ten gantt keys ObjectGantt reads through a cast #6053 did not move it.resolveI18nLabel(label, locale)is exported from@objectstack/spec/ui; verifiedagainst the installed package, not the card's prose:
resolveI18nLabel({en:'Shift Plan','zh-CN':'排班计划'},'zh-CN')→排班计划.The fix
resolveI18nLabelfrom@objectstack/spec/ui— the producer's own resolver for thatvocabulary — against the display locale already in scope. Imported aliased as
resolveInlineI18nLabel, following PR #4169's convention, because this repo has its ownresolveI18nLabelover the KEYED vocabulary and neither accepts the other's shape.The resolver answers
undefinedon an absent label or a total miss, which is why it sitsinside the
??chain rather than around it: a missing label falls through to the nextlink exactly as it always did. No lenient coercion is added — the chain resolves the
declared vocabulary, it does not accept a second one (AGENTS.md #0.1).
objectSchema?.label— measured together, deliberately treated differentlyThe card asked for the next link to be measured with this one rather than after it. It was,
and it needs the opposite treatment, which is why it is untouched:
objectSchemais the DATA object's metadata, and the spec declareslabel: z.string().optional()onObjectSchemaBase(
packages/spec/src/data/object.zod.ts:1560) — which is astrictObject. A locale map inthat slot is rejected by the producer at parse, not something this consumer should learn
to resolve. Wrapping it in
resolveI18nLabelwould be accepting a second vocabulary at aread site, i.e. the exact shape #0.1 rules out, and it would make the consumer tolerant of
metadata the producer refuses. If that slot ever needs locale maps, it widens in the spec
first and this link follows. A CONTROL test pins that the link still passes its declared
plain string through untouched.
Filename sanitisation — the chain already does it, no finding filed
The PM advisory asked whether a resolved locale map can carry filename-hostile characters.
It can, and the chain already sanitises, downstream of this call site:
GanttView.tsx:3065strips[\\/:*?"<>|\s]+from the name before appending the timestamp.A resolved map entry goes through the identical strip a plain string does, so this change
adds no sanitisation and needs none. Nothing to file.
Which assertions would still pass on a revert
Stated plainly, because most of this file is deliberately revert-green. The produced name is
BASE-yyyyMMdd-HHmm.png, so the rows below name the BASE and match the stamp:排班计划for a zh-CN audience[object Object]-20260825-0256.pngShift Planfor an en audience[object Object]-20260825-0256.pnglabelnames the file itselfexportFileNamewins the chainobjectNameobjectSchema.labelFour of the six are must-not-change pins by construction: the defect only exists for a MAP,
so a plain-string pin is a phantom assertion, green on both sides. They are here so the fix
cannot pass by breaking the string path or by collapsing another link of the chain.
Verification
All at
3bc4d5852, from the repo root (the canonical Vitest invocation the repo'svitest-invocation-guardrequires).Ablation — red measured, not asserted
The fix was committed first, then the single resolved call was reverted to
?? schema.labelunder a
trap … EXIT INT TERM. The mutation was proved on disk before the run — the injectedtext grepped present (1), the removed text grepped absent (0), landing site printed
(
ObjectGantt.tsx:1536),git diff --statshowing1 insertion(+), 1 deletion(-):Predicted before running: two map cases red, four controls green. That is what happened.
The trap restored the file and
git diff HEAD --statwas empty afterwards.Why the pin reads the produced filename, not the prop
The harm is the name of the downloaded file, and the
exportFileNameprop is oneindirection short of it. The test renders the real
GanttView, clicks its real exportbutton, and reads
downloadoff the transient anchordownloadBlobcreates — so theassertion covers
GanttView's own character strip and its-yyyyMMdd-HHmmstamp.Imageand
canvas.toBlobare stubbed to the minimum the raster path reads, because happy-domdecodes neither; the filename is under test, the pixels are not.
Scope
packages/plugin-ganttonly, plus its changeset. #6051 (24 further undeclared gantt keys)and #6050 (README
basePath) are out of scope here and remain open. No out-of-scopefindings surfaced.
Generated by Claude Code