German editor: seven uncoded strings and two small defects the review found (Enabled hints, upload refusal, at-rest required error, undo op word, digest plural, dialog overflow, first undo) #267

Closed
opened 2026-10-13 18:10:20 +02:00 by dominik.polakovics · 0 comments

Summary

A German-locale review of the editor found seven defects that read as "missing translations" but are not catalog gaps: in five of them the server hands the client English text with no message code, so the client has nothing to look up; one is a CSS overflow; one is an undo-availability gap. Fix all seven with the mechanisms the repository already has (coded messages via apimsg, the code+args field-error shape, noteCode-style code companions, a per-op catalog entry, a baseline WIP snapshot).

Each item is independent and may land as its own PR referencing this issue.

Current behavior

  1. Enabled hints stay English. The hints under the Enabled switch are plain Go constants with no code: internal/pageops/settings.go (pageEnabledOn/pageEnabledOff) and internal/server/forms.go (elementEnabledOn/elementEnabledOff). They travel as settings.on/settings.off of the switch field and SwitchView (web/src/widgets/controls.tsx) renders them verbatim. Only the translation-language case is localized, because ElementForm.accessFields overrides the hints client-side with form.enabled.onLang/offLang.

  2. Upload refusal stays English. The multipart/oversize-body errors are coded (library.upload*), but the media gate's refusals are not: writeMediaReject (internal/server/media.go) passes a media.RejectError through apimsg.FromError, which wraps it as error.unexpected with the English text in detail ("unsupported upload: detected …; allowed types are JPEG, PNG, WebP, AVIF and PDF", internal/media/process.go). The catalog's server.error.unexpected is {detail}, so the text passes through.

  3. Required-field error on a freshly inserted element stays English. A save's 422 carries {field, msg, code, args} and decodeFieldErrors translates it. The form description's validation-at-rest errors (issue #172) is map[string]string built from FieldError.Msg only (form.restErrors, internal/server/forms.go). A freshly inserted element opens with its required field already empty, so the at-rest path is what the user sees: "this field is required" instead of server.field.required's German.

  4. Undo tooltip shows the raw op word. undoStepText (web/src/undoinfo.ts) inserts info.op as the server wrote it into the snapshot subject: "Rückgängig: save auf Leistungen". The vocabulary is fixed and server-owned (snapshotWIP callers): save, create, rename, move, reorder, transfer, translate, undo, discard, stamp uids, repair translation, media upload, media delete, media rename, media move, media folder create|rename|move|delete, plus whatever op the settings/enable saves pass through snapshotPage.

  5. "1 Images" / "1 Bilder". The children digest's label is the item element's sidecar label, which sidecars write as a plural noun. element.digest.itemsLabelled.one is still {count} {label} (web/src/components/ElementCard.tsx digestCount), so the one-form cannot singularize a label it does not own.

  6. Long page paths cut off in the "Linked pages are disabled" dialog. .dialog is 380px wide; .disabled-links__path (web/src/styles.css) is a monospace inline span with no wrapping rule, and ul.disabled-links has overflow: auto, so a long path overflows horizontally and looks truncated.

  7. The first change after the editor starts cannot be undone. UndoParent resolves refs/deckle/wip^ (internal/gitrepo/snapshot.go WipParent). No baseline snapshot is taken at startup, and the first SnapshotWIP commits without a parent, so until a second snapshot exists undoAvailable is false and POST /api/page/undo has nothing to restore. Branch mode has the same gap while the draft tip equals or descends from main.

Desired behavior

  1. The four Enabled hints are apimsg codes. The switch field's settings carries onCode/offCode beside on/off (the note/noteCode pattern); SwitchView resolves the hint through the same display helper that resolves note. The client-side translation override in accessFields stays as the one place the translation-language variant is worded.

  2. The three gate reasons (RejectOversize, RejectUnsupported, RejectCorrupt) answer with their own codes and args (limit/size, detected, type), rendered through codedError like every other server refusal. error.unexpected is never the vehicle for a message the server authored.

  3. The form description's at-rest errors carry the same coded shape as a 422's errors list, and the client folds them with decodeFieldErrors (one owner for "several messages on one key joined ; ", which today lives both in restErrors and in decodeFieldErrors). Wire shape change: errors becomes a list of {field, msg, code, args}; document it in the README's form-description section.

  4. The client translates the op through a catalog key per op (undo.op.<op>), falling back to the raw word for an unknown op. A vitest/Go drift test pins the vocabulary: every op string snapshotWIP/snapshotPage/snapshotRelocation is called with has a key in en.json (the internal/pagepath/testdata fixture pattern or an exported list, not a hand-kept comment).

  5. Pick one, state it in the PR: (a) the one-form drops the sidecar label and reads like element.digest.items.one ("1 Eintrag" / "1 item"), or (b) uses the × shape the element variant already has ("1 × Bilder"). Do not add a singular label to the sidecar schema in this issue.

  6. .disabled-links__path wraps (overflow-wrap: anywhere) and the list no longer scrolls horizontally.

  7. A baseline snapshot exists before the first mutation changes the tree, so the first change is undoable in a fresh session. Take it where the evidence says it is cheapest: at server start when the repo has no wip tip (WIP mode) or lazily in snapshotWIP before the mutation when no tip exists. Branch mode: decide and document whether the same baseline applies (a draft commit equal to main) or the gap is accepted; say which in the README's undo section.

Decisions

  • Codes, not client-side copies of server text. The client never re-derives a server message (CLAUDE.md "The client and the theme do not re-derive"); every English string the server authors gets a code and a catalog entry.
  • One owner for the field-error fold. Item 3 removes restErrors' duplicate of the "; " join; the server serves the list, the client folds, as it does for the 422.
  • The op vocabulary is server-owned. Item 4's catalog keys are a translation table, and a drift test keeps them honest; no "same as snapshotWIP" comment.
  • No sidecar schema change for item 5. A singular item label is a schema and AGENTS.md change with its own cost; the digest wording is adjusted instead.

Key interfaces

  • internal/apimsg define, web/src/i18n/servermessages.json export (internal/apimsg/export_test.go) and web/src/test/i18nParity.test.ts: new codes must appear in en.json and de.json.
  • element.FormField.Settings (internal/element/form.go) and SwitchView (web/src/widgets/controls.tsx).
  • media.RejectError (internal/media/gate.go), writeMediaReject and writeError/codedError (internal/server/media.go, server.go).
  • formDescription.Errors, form.restErrors (internal/server/forms.go); decodeFieldErrors (web/src/api/client.ts); ElementForm.routeErrors, PageForm setErrors.
  • undoStepText/undoTooltip (web/src/undoinfo.ts), parseSnapshotSubject (internal/server/git.go).
  • gitrepo.SnapshotWIP, WipParent, UndoParent (internal/gitrepo/snapshot.go, branch.go); undoState (internal/server/git.go).

Documentation and skills

  • README.md: form-description errors shape (item 3), the switch field's onCode/offCode (item 1), the upload error codes (item 2), the undo section's baseline semantics (item 7).
  • scaffold/site/AGENTS.md: the element-form and page-type sections that describe the Enabled switch (around the "Element sidecar schema" and "Page types" headings) only if the documented field shape changes; the agentsdoc drift test must stay green.
  • scaffold/site/.claude/skills/: no skill walks through these behaviours; confirm with grep -rn 'Enabled\|undo\|upload' scaffold/site/.claude/skills/ in the PR and say so.
  • web/src/i18n/glossary.md: add the undo op words and gate refusal wording to the German glossary.

Acceptance criteria

  • In the German editor, a page's and an element's Enabled hints, the upload refusal for an SVG and for an oversize file, and a freshly inserted element's required-field error are German.
  • The Undo tooltip reads "Rückgängig: Speichern auf …" (or the glossary's chosen word) for every op in the vocabulary; a drift test fails when a new snapshotWIP op has no catalog key.
  • A one-item digest no longer reads "1 Images"/"1 Bilder".
  • A disabled-links entry with a 60-character path shows the whole path on two lines in a 380px dialog.
  • On a fresh clone with no refs/deckle/wip, the first save reports undoAvailable: true and POST /api/page/undo restores the pre-save file.
  • go test ./... and npm test green, including i18nParity, i18nLeak, agentsdoc_test.go, skills_test.go; servermessages.json regenerated.
  • The PR description names each rule touched and its owner (CLAUDE.md "Every rule has exactly one owner").

How to verify

  • go test ./internal/apimsg/... regenerates servermessages.json; npm test -- i18nParity lists any catalog key missing in en.json/de.json.
  • Hit GET /api/form for an element and a page with Accept-Language: de; check settings.onCode and the errors shape.
  • curl -F file=@x.svg …/api/media/upload answers 422 with a code other than error.unexpected.
  • git update-ref -d refs/deckle/wip in a test site, start the server, save once, GET /api/git/state.

Assumptions to check

  • That no other caller reads formDescription.Errors as a map (grep description.errors / desc.Errors in client and tests).
  • That settings is free-form on the wire for the switch widget (it is map[string]any), so adding onCode/offCode breaks no widget test.
  • That the gate's describe(input) text is safe as an arg (it names a sniffed type, not user text).

Out of scope

  • A singular/plural label pair in the element sidecar schema.
  • Multi-step undo.
  • Any wording change to messages that are already coded.
## Summary A German-locale review of the editor found seven defects that read as "missing translations" but are not catalog gaps: in five of them the server hands the client English text with no message code, so the client has nothing to look up; one is a CSS overflow; one is an undo-availability gap. Fix all seven with the mechanisms the repository already has (coded messages via `apimsg`, the `code`+`args` field-error shape, `noteCode`-style code companions, a per-op catalog entry, a baseline WIP snapshot). Each item is independent and may land as its own PR referencing this issue. ## Current behavior 1. **Enabled hints stay English.** The hints under the Enabled switch are plain Go constants with no code: `internal/pageops/settings.go` (`pageEnabledOn`/`pageEnabledOff`) and `internal/server/forms.go` (`elementEnabledOn`/`elementEnabledOff`). They travel as `settings.on`/`settings.off` of the `switch` field and `SwitchView` (`web/src/widgets/controls.tsx`) renders them verbatim. Only the translation-language case is localized, because `ElementForm.accessFields` overrides the hints client-side with `form.enabled.onLang`/`offLang`. 2. **Upload refusal stays English.** The multipart/oversize-body errors are coded (`library.upload*`), but the media gate's refusals are not: `writeMediaReject` (`internal/server/media.go`) passes a `media.RejectError` through `apimsg.FromError`, which wraps it as `error.unexpected` with the English text in `detail` ("unsupported upload: detected …; allowed types are JPEG, PNG, WebP, AVIF and PDF", `internal/media/process.go`). The catalog's `server.error.unexpected` is `{detail}`, so the text passes through. 3. **Required-field error on a freshly inserted element stays English.** A save's 422 carries `{field, msg, code, args}` and `decodeFieldErrors` translates it. The form description's validation-at-rest `errors` (issue #172) is `map[string]string` built from `FieldError.Msg` only (`form.restErrors`, `internal/server/forms.go`). A freshly inserted element opens with its required field already empty, so the at-rest path is what the user sees: "this field is required" instead of `server.field.required`'s German. 4. **Undo tooltip shows the raw op word.** `undoStepText` (`web/src/undoinfo.ts`) inserts `info.op` as the server wrote it into the snapshot subject: "Rückgängig: save auf Leistungen". The vocabulary is fixed and server-owned (`snapshotWIP` callers): `save`, `create`, `rename`, `move`, `reorder`, `transfer`, `translate`, `undo`, `discard`, `stamp uids`, `repair translation`, `media upload`, `media delete`, `media rename`, `media move`, `media folder create|rename|move|delete`, plus whatever `op` the settings/enable saves pass through `snapshotPage`. 5. **"1 Images" / "1 Bilder".** The children digest's `label` is the item element's sidecar label, which sidecars write as a plural noun. `element.digest.itemsLabelled.one` is still `{count} {label}` (`web/src/components/ElementCard.tsx` `digestCount`), so the one-form cannot singularize a label it does not own. 6. **Long page paths cut off in the "Linked pages are disabled" dialog.** `.dialog` is 380px wide; `.disabled-links__path` (`web/src/styles.css`) is a monospace inline span with no wrapping rule, and `ul.disabled-links` has `overflow: auto`, so a long path overflows horizontally and looks truncated. 7. **The first change after the editor starts cannot be undone.** `UndoParent` resolves `refs/deckle/wip^` (`internal/gitrepo/snapshot.go` `WipParent`). No baseline snapshot is taken at startup, and the first `SnapshotWIP` commits without a parent, so until a second snapshot exists `undoAvailable` is false and `POST /api/page/undo` has nothing to restore. Branch mode has the same gap while the draft tip equals or descends from main. ## Desired behavior 1. The four Enabled hints are `apimsg` codes. The switch field's `settings` carries `onCode`/`offCode` beside `on`/`off` (the `note`/`noteCode` pattern); `SwitchView` resolves the hint through the same display helper that resolves `note`. The client-side translation override in `accessFields` stays as the one place the translation-language variant is worded. 2. The three gate reasons (`RejectOversize`, `RejectUnsupported`, `RejectCorrupt`) answer with their own codes and args (`limit`/`size`, `detected`, `type`), rendered through `codedError` like every other server refusal. `error.unexpected` is never the vehicle for a message the server authored. 3. The form description's at-rest `errors` carry the same coded shape as a 422's `errors` list, and the client folds them with `decodeFieldErrors` (one owner for "several messages on one key joined `; `", which today lives both in `restErrors` and in `decodeFieldErrors`). Wire shape change: `errors` becomes a list of `{field, msg, code, args}`; document it in the README's form-description section. 4. The client translates the op through a catalog key per op (`undo.op.<op>`), falling back to the raw word for an unknown op. A vitest/Go drift test pins the vocabulary: every op string `snapshotWIP`/`snapshotPage`/`snapshotRelocation` is called with has a key in `en.json` (the `internal/pagepath/testdata` fixture pattern or an exported list, not a hand-kept comment). 5. Pick one, state it in the PR: (a) the one-form drops the sidecar label and reads like `element.digest.items.one` ("1 Eintrag" / "1 item"), or (b) uses the `× ` shape the element variant already has ("1 × Bilder"). Do not add a singular label to the sidecar schema in this issue. 6. `.disabled-links__path` wraps (`overflow-wrap: anywhere`) and the list no longer scrolls horizontally. 7. A baseline snapshot exists before the first mutation changes the tree, so the first change is undoable in a fresh session. Take it where the evidence says it is cheapest: at server start when the repo has no wip tip (WIP mode) or lazily in `snapshotWIP` before the mutation when no tip exists. Branch mode: decide and document whether the same baseline applies (a draft commit equal to main) or the gap is accepted; say which in the README's undo section. ## Decisions - **Codes, not client-side copies of server text.** The client never re-derives a server message (CLAUDE.md "The client and the theme do not re-derive"); every English string the server authors gets a code and a catalog entry. - **One owner for the field-error fold.** Item 3 removes `restErrors`' duplicate of the `"; "` join; the server serves the list, the client folds, as it does for the 422. - **The op vocabulary is server-owned.** Item 4's catalog keys are a translation table, and a drift test keeps them honest; no "same as snapshotWIP" comment. - **No sidecar schema change for item 5.** A singular item label is a schema and AGENTS.md change with its own cost; the digest wording is adjusted instead. ## Key interfaces - `internal/apimsg` `define`, `web/src/i18n/servermessages.json` export (`internal/apimsg/export_test.go`) and `web/src/test/i18nParity.test.ts`: new codes must appear in `en.json` and `de.json`. - `element.FormField.Settings` (`internal/element/form.go`) and `SwitchView` (`web/src/widgets/controls.tsx`). - `media.RejectError` (`internal/media/gate.go`), `writeMediaReject` and `writeError`/`codedError` (`internal/server/media.go`, `server.go`). - `formDescription.Errors`, `form.restErrors` (`internal/server/forms.go`); `decodeFieldErrors` (`web/src/api/client.ts`); `ElementForm.routeErrors`, `PageForm` `setErrors`. - `undoStepText`/`undoTooltip` (`web/src/undoinfo.ts`), `parseSnapshotSubject` (`internal/server/git.go`). - `gitrepo.SnapshotWIP`, `WipParent`, `UndoParent` (`internal/gitrepo/snapshot.go`, `branch.go`); `undoState` (`internal/server/git.go`). ## Documentation and skills - `README.md`: form-description `errors` shape (item 3), the switch field's `onCode`/`offCode` (item 1), the upload error codes (item 2), the undo section's baseline semantics (item 7). - `scaffold/site/AGENTS.md`: the element-form and page-type sections that describe the **Enabled** switch (around the "Element sidecar schema" and "Page types" headings) only if the documented field shape changes; the agentsdoc drift test must stay green. - `scaffold/site/.claude/skills/`: no skill walks through these behaviours; confirm with `grep -rn 'Enabled\|undo\|upload' scaffold/site/.claude/skills/` in the PR and say so. - `web/src/i18n/glossary.md`: add the undo op words and gate refusal wording to the German glossary. ## Acceptance criteria - [ ] In the German editor, a page's and an element's Enabled hints, the upload refusal for an SVG and for an oversize file, and a freshly inserted element's required-field error are German. - [ ] The Undo tooltip reads "Rückgängig: Speichern auf …" (or the glossary's chosen word) for every op in the vocabulary; a drift test fails when a new `snapshotWIP` op has no catalog key. - [ ] A one-item digest no longer reads "1 Images"/"1 Bilder". - [ ] A disabled-links entry with a 60-character path shows the whole path on two lines in a 380px dialog. - [ ] On a fresh clone with no `refs/deckle/wip`, the first save reports `undoAvailable: true` and `POST /api/page/undo` restores the pre-save file. - [ ] `go test ./...` and `npm test` green, including `i18nParity`, `i18nLeak`, `agentsdoc_test.go`, `skills_test.go`; `servermessages.json` regenerated. - [ ] The PR description names each rule touched and its owner (CLAUDE.md "Every rule has exactly one owner"). ## How to verify - `go test ./internal/apimsg/...` regenerates `servermessages.json`; `npm test -- i18nParity` lists any catalog key missing in `en.json`/`de.json`. - Hit `GET /api/form` for an element and a page with `Accept-Language: de`; check `settings.onCode` and the `errors` shape. - `curl -F file=@x.svg …/api/media/upload` answers 422 with a `code` other than `error.unexpected`. - `git update-ref -d refs/deckle/wip` in a test site, start the server, save once, `GET /api/git/state`. ## Assumptions to check - That no other caller reads `formDescription.Errors` as a map (grep `description.errors` / `desc.Errors` in client and tests). - That `settings` is free-form on the wire for the switch widget (it is `map[string]any`), so adding `onCode`/`offCode` breaks no widget test. - That the gate's `describe(input)` text is safe as an arg (it names a sniffed type, not user text). ## Out of scope - A singular/plural label pair in the element sidecar schema. - Multi-step undo. - Any wording change to messages that are already coded.
Sign in to join this conversation.
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#267
No description provided.