fix(editor): German editor review — coded hints, upload refusals and at-rest errors, translated undo ops, digest one-form, dialog wrap, first undo #268

Merged
dominik.polakovics merged 5 commits from afk/267 into main 2026-10-13 18:52:35 +02:00

Fixes the seven defects of the German-locale editor review, each with a mechanism the repository already has.

Items

  1. Enabled hints: four apimsg codes (hint.pageEnabledOn/Off, hint.elementEnabledOn/Off). The switch's settings carries onCode/offCode beside on/off, the note/noteCode pattern, and SwitchView resolves them through displayLabel. ElementForm.accessFields is still the one place a translation's variant is worded, and it clears the codes so the override wins.
  2. Upload refusals: media.RejectError is apimsg.Coded. It answers with library.uploadOversize (413, limit), library.uploadUnsupported (422, detected), library.uploadUnrecognised, library.uploadCorrupt (type) and library.uploadAVIFUnavailable. error.unexpected is no longer used for them. describe now returns language-neutral format names (SVG, GIF, …) taken from magic bytes, never from user text. Oversize has no size arg because the handler reads at most MaxUploadBytes+1 bytes, so the real size is unknown.
  3. At-rest errors: formDescription.errors is now the 422's list [{field, msg, code, args}] ([]element.FieldError), folded on the client by decodeFieldErrors. The "; " join in restErrors is gone. Wire shape documented in the README.
  4. Undo op word: the op is a closed vocabulary (type snapshotOp, snapshotOps in internal/server/snapshot_ops.go). The client translates it through undo.op.<op> (undoOpWord) and shows the raw word for an unknown op. German reads "Rückgängig: Speichern auf …".
    • Drift tests: an AST test keeps the constants, the list and every call site closed. The list is exported to internal/server/testdata/undo_ops.json (regenerate with -update), and i18nParity requires an undo.op.<op> key per op in en and de.
    • transfer and the media ops now have readable English ("move element", "upload media file").
  5. Digest plural: I chose option (b). element.digest.itemsLabelled.one is {count} × {label} ("1 × Bilder"), the shape the element variant already has. The sidecar schema is unchanged.
  6. Dialog overflow: .disabled-links__path has overflow-wrap: anywhere, and the list uses overflow-x: hidden; overflow-y: auto.
  7. First undo: in WIP mode, the first mutating request on a chain with no refs/deckle/wip tip snapshots the untouched tree as deckle: baseline. It runs under the mutation lock, before the handler writes. Doing it then instead of at startup also captures hand edits made after the server started.
    • Branch mode: the gap is accepted, and in fact no gap exists. The first draft commit's parent is main, so the first change is already undoable (pinned by TestBranchModeFirstDraftSaveIsUndoable). A collapsed draft (tip equal to or behind main) has nothing to undo by design.
    • Both are documented in the README's undo section.

Rules touched and their owners

Rule Owner
Building the Enabled field and its coded hints internal/element/form.go EnabledField (the hint constants in pageops/settings.go and server/forms.go are deleted)
Showing a coded label, note or hint in the UI language web/src/api/labels.ts displayLabel
Translation-language Enabled hint wording web/src/components/ElementForm.tsx accessFields
Field-error wire shape internal/element/values.go FieldError (shared by the 422 and the at-rest errors)
Joining several messages on one field with "; " web/src/api/client.ts decodeFieldErrors (the copy in restErrors and the Go test helper foldFieldErrors are removed)
Refusal reason → code and args internal/media/gate.go (*RejectError).APIMessage
Oversize and allowlist refusal media.Gate (Process now calls it instead of repeating the check)
Refusal → HTTP status internal/server/media.go writeMediaReject (the body-limit 413 goes through it too)
The snapshot op vocabulary internal/server/snapshot_ops.go snapshotOps
Reading an op back from a subject cutSnapshotOp (parseSnapshotSubject no longer hardcodes word counts)
Media op vs page op snapshotOp.namesPages
Op → catalog key web/src/i18n/translate.ts undoOpKey/undoOpWord
Whether a fresh WIP chain needs a baseline, and taking it internal/gitrepo/snapshot.go Repo.EnsureWIPBaseline, called only from Handler.ensureWIPBaseline in ServeHTTP

Docs and skills

  • README: switch onCode/offCode, the form-description errors list, the upload error codes, the WIP baseline and branch-mode semantics, and the undo op vocabulary.
  • web/src/i18n/glossary.md: gate refusal wording and the undo op words.
  • scaffold/site/AGENTS.md: unchanged. It documents neither the switch settings, the description's errors, the refusal messages nor the undo internals.
  • Skills: grep -rn 'Enabled\|undo\|upload' scaffold/site/.claude/skills/ finds only mentions of the Enabled switch's effect and of which upload types are refused. Both are unchanged, so no skill walks through these behaviours and none were edited.

Verification

  • go vet ./... and gofmt are clean.
  • go test ./... passes, including agentsdoc_test.go, skills_test.go and the servermessages.json export (regenerated).
  • npx tsc --noEmit and npx vitest run pass: 3210 tests, including i18nParity and i18nLeak.
  • npm run build passes.
  • Acceptance tests:
    • TestFirstSaveIsUndoable: fresh site with no wip ref, one save, then undoAvailable: true, and the undo restores the pre-save file byte for byte.
    • TestMediaUploadEvilSVG422 and TestMediaUploadCorruptPNG422: coded 422s. TestMediaUploadOversize413BeforeDecode: coded 413.
    • The German switch hint and the German at-rest error are covered in widgets/PageForm/ElementForm tests.
    • The German undo tooltip is checked for every op in the fixture.
  • The drift tests were checked to fail on an unlisted op, a literal op argument, a stale fixture and a missing de.json key.

Closes #267

🤖 Generated with Claude Code

Fixes the seven defects of the German-locale editor review, each with a mechanism the repository already has. ## Items 1. **Enabled hints**: four `apimsg` codes (`hint.pageEnabledOn/Off`, `hint.elementEnabledOn/Off`). The switch's `settings` carries `onCode`/`offCode` beside `on`/`off`, the `note`/`noteCode` pattern, and `SwitchView` resolves them through `displayLabel`. `ElementForm.accessFields` is still the one place a translation's variant is worded, and it clears the codes so the override wins. 2. **Upload refusals**: `media.RejectError` is `apimsg.Coded`. It answers with `library.uploadOversize` (413, `limit`), `library.uploadUnsupported` (422, `detected`), `library.uploadUnrecognised`, `library.uploadCorrupt` (`type`) and `library.uploadAVIFUnavailable`. `error.unexpected` is no longer used for them. `describe` now returns language-neutral format names (`SVG`, `GIF`, …) taken from magic bytes, never from user text. Oversize has no `size` arg because the handler reads at most `MaxUploadBytes+1` bytes, so the real size is unknown. 3. **At-rest errors**: `formDescription.errors` is now the 422's list `[{field, msg, code, args}]` (`[]element.FieldError`), folded on the client by `decodeFieldErrors`. The `"; "` join in `restErrors` is gone. Wire shape documented in the README. 4. **Undo op word**: the op is a closed vocabulary (`type snapshotOp`, `snapshotOps` in `internal/server/snapshot_ops.go`). The client translates it through `undo.op.<op>` (`undoOpWord`) and shows the raw word for an unknown op. German reads "Rückgängig: Speichern auf …". - Drift tests: an AST test keeps the constants, the list and every call site closed. The list is exported to `internal/server/testdata/undo_ops.json` (regenerate with `-update`), and `i18nParity` requires an `undo.op.<op>` key per op in en and de. - `transfer` and the media ops now have readable English ("move element", "upload media file"). 5. **Digest plural**: I chose option **(b)**. `element.digest.itemsLabelled.one` is `{count} × {label}` ("1 × Bilder"), the shape the element variant already has. The sidecar schema is unchanged. 6. **Dialog overflow**: `.disabled-links__path` has `overflow-wrap: anywhere`, and the list uses `overflow-x: hidden; overflow-y: auto`. 7. **First undo**: in WIP mode, the first mutating request on a chain with no `refs/deckle/wip` tip snapshots the untouched tree as `deckle: baseline`. It runs under the mutation lock, before the handler writes. Doing it then instead of at startup also captures hand edits made after the server started. - **Branch mode**: the gap is accepted, and in fact no gap exists. The first draft commit's parent is main, so the first change is already undoable (pinned by `TestBranchModeFirstDraftSaveIsUndoable`). A collapsed draft (tip equal to or behind main) has nothing to undo by design. - Both are documented in the README's undo section. ## Rules touched and their owners | Rule | Owner | |---|---| | Building the Enabled field and its coded hints | `internal/element/form.go` `EnabledField` (the hint constants in `pageops/settings.go` and `server/forms.go` are deleted) | | Showing a coded label, note or hint in the UI language | `web/src/api/labels.ts` `displayLabel` | | Translation-language Enabled hint wording | `web/src/components/ElementForm.tsx` `accessFields` | | Field-error wire shape | `internal/element/values.go` `FieldError` (shared by the 422 and the at-rest `errors`) | | Joining several messages on one field with `"; "` | `web/src/api/client.ts` `decodeFieldErrors` (the copy in `restErrors` and the Go test helper `foldFieldErrors` are removed) | | Refusal reason → code and args | `internal/media/gate.go` `(*RejectError).APIMessage` | | Oversize and allowlist refusal | `media.Gate` (`Process` now calls it instead of repeating the check) | | Refusal → HTTP status | `internal/server/media.go` `writeMediaReject` (the body-limit 413 goes through it too) | | The snapshot op vocabulary | `internal/server/snapshot_ops.go` `snapshotOps` | | Reading an op back from a subject | `cutSnapshotOp` (`parseSnapshotSubject` no longer hardcodes word counts) | | Media op vs page op | `snapshotOp.namesPages` | | Op → catalog key | `web/src/i18n/translate.ts` `undoOpKey`/`undoOpWord` | | Whether a fresh WIP chain needs a baseline, and taking it | `internal/gitrepo/snapshot.go` `Repo.EnsureWIPBaseline`, called only from `Handler.ensureWIPBaseline` in `ServeHTTP` | ## Docs and skills - README: switch `onCode`/`offCode`, the form-description `errors` list, the upload error codes, the WIP baseline and branch-mode semantics, and the undo op vocabulary. - `web/src/i18n/glossary.md`: gate refusal wording and the undo op words. - `scaffold/site/AGENTS.md`: unchanged. It documents neither the switch settings, the description's `errors`, the refusal messages nor the undo internals. - Skills: `grep -rn 'Enabled\|undo\|upload' scaffold/site/.claude/skills/` finds only mentions of the Enabled switch's effect and of which upload types are refused. Both are unchanged, so no skill walks through these behaviours and none were edited. ## Verification - `go vet ./...` and gofmt are clean. - `go test ./...` passes, including `agentsdoc_test.go`, `skills_test.go` and the `servermessages.json` export (regenerated). - `npx tsc --noEmit` and `npx vitest run` pass: 3210 tests, including `i18nParity` and `i18nLeak`. - `npm run build` passes. - Acceptance tests: - `TestFirstSaveIsUndoable`: fresh site with no wip ref, one save, then `undoAvailable: true`, and the undo restores the pre-save file byte for byte. - `TestMediaUploadEvilSVG422` and `TestMediaUploadCorruptPNG422`: coded 422s. `TestMediaUploadOversize413BeforeDecode`: coded 413. - The German switch hint and the German at-rest error are covered in widgets/PageForm/ElementForm tests. - The German undo tooltip is checked for every op in the fixture. - The drift tests were checked to fail on an unlisted op, a literal op argument, a stale fixture and a missing `de.json` key. Closes #267 🤖 Generated with [Claude Code](https://claude.com/claude-code)
The one-form of element.digest.itemsLabelled uses the × shape the element
variant already has, since the sidecar label is a plural noun the catalog
cannot singularize. Long disabled-link paths wrap instead of overflowing
the 380px dialog.

Refs #267

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The four Enabled-switch hints are apimsg codes (hint.pageEnabledOn/Off,
hint.elementEnabledOn/Off), carried as onCode/offCode beside on/off in the
switch's settings and resolved by SwitchView through displayLabel, as a
note is. The form description's at-rest errors are the save's 422 list
[{field, msg, code, args}], folded on the client by decodeFieldErrors —
restErrors no longer joins messages itself.

Refs #267

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
media.RejectError is apimsg.Coded: its APIMessage maps each reason to its
own code and args (library.uploadOversize/Unsupported/Unrecognised/Corrupt/
AVIFUnavailable), so a refused upload no longer travels as error.unexpected
with English text. describe returns language-neutral format names for the
detected arg. Process runs Gate instead of re-evaluating the oversize and
allowlist refusals itself, and the body-limit 413 answers through the same
refusal.

Refs #267

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
In WIP mode the first mutating request on a chain with no refs/deckle/wip
tip snapshots the untouched working tree as "deckle: baseline" under the
mutation lock, before the handler writes, so the first change of a fresh
session has a parent and is undoable. gitrepo.Repo.EnsureWIPBaseline owns
the check and caches that a tip exists. Branch mode needs no baseline: the
first draft commit's parent is main.

Refs #267

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
fix(undo): translate the undo step's op through a server-owned vocabulary
All checks were successful
ci / build-and-test (pull_request) Successful in 3m12s
ci / release (pull_request) Has been skipped
fc245eec61
The snapshot op is a closed vocabulary owned by internal/server
snapshot_ops.go (type snapshotOp, snapshotOps); snapshotWIP and every
forwarding helper take a snapshotOp, and parseSnapshotSubject reads the op
back by that list instead of hardcoded word counts. The list is exported to
internal/server/testdata/undo_ops.json; an AST test keeps constants, list
and call sites closed, and i18nParity holds every catalog to an
undo.op.<op> key per op. The Undo tooltip translates the op (undoOpWord),
falling back to the raw word for an unknown one.

Refs #267

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Author
Owner

[autoland] verdict: pass

PASS. Signal: CI aggregate success (ci / build-and-test: npm ci + build + vitest, go vet + go test ./... + build; release skipped as expected on PRs). Branch is current with main (merge-base = origin/main d7bc3dd), no conflicts; gofmt clean on changed Go files.

Conventions: Conventional Commits title and commits (fix(editor|media|undo)); body carries Closes #267; rule owners table per CLAUDE.md; AGENTS.md unchanged with justification (it documents none of the switch settings, at-rest errors shape, refusal codes or undo internals — verified by grep); skills grep stated and verified (only mentions of the Enabled switch's effect and refused upload types).

Scope matches all seven items of #267: coded Enabled hints (onCode/offCode via displayLabel), coded media.RejectError (APIMessage owner; Process now runs Gate), at-rest errors as []element.FieldError folded by decodeFieldErrors (restErrors' '; ' join removed), closed snapshotOp vocabulary with AST + fixture drift tests and i18nParity keys, digest one-form option (b), disabled-links wrap CSS, WIP baseline under the mutation lock (branch mode documented as needing none). README and glossary updated.

[autoland] verdict: pass PASS. Signal: CI aggregate success (ci / build-and-test: npm ci + build + vitest, go vet + go test ./... + build; release skipped as expected on PRs). Branch is current with main (merge-base = origin/main d7bc3dd), no conflicts; gofmt clean on changed Go files. Conventions: Conventional Commits title and commits (fix(editor|media|undo)); body carries Closes #267; rule owners table per CLAUDE.md; AGENTS.md unchanged with justification (it documents none of the switch settings, at-rest errors shape, refusal codes or undo internals — verified by grep); skills grep stated and verified (only mentions of the Enabled switch's effect and refused upload types). Scope matches all seven items of #267: coded Enabled hints (onCode/offCode via displayLabel), coded media.RejectError (APIMessage owner; Process now runs Gate), at-rest errors as []element.FieldError folded by decodeFieldErrors (restErrors' '; ' join removed), closed snapshotOp vocabulary with AST + fixture drift tests and i18nParity keys, digest one-form option (b), disabled-links wrap CSS, WIP baseline under the mutation lock (branch mode documented as needing none). README and glossary updated.
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Cloonar/deckle!268
No description provided.