Multilingual editor (3/3): page properties, page menu, pending changes, paste confirmation, Languages view, module lists #200

Closed
opened 2026-10-10 23:49:36 +02:00 by dominik.polakovics · 0 comments

Summary

Third of three editor slices of the multilingual work (contract: the PRD in #192, section 8; visual reference: docs/mockups/multilingual/, artifact https://claude.ai/artifact/6RYbRG7y4yMyeGxYbFsh1n). This slice is everything around the page: page properties of a translation, the page menu (Translate to…, Disconnect, Delete translation, the page delete plan), per-file pending changes and badges, the cut-and-paste confirmation that names dropped translations, the Languages view, the module and category lists with a translation-state column, and the link picker's language note.

Builds on #192 and #199. The API (README "Translations on the wire") already answers everything below; nothing in the engine changes.

Current behavior

After #199 a translation is editable, but its properties form is the default-language form, the page menu has no translation items, pending changes still send lang and show one row per page, fetchMoveRefs sends no toParentUid and ignores the translations block, there is no Languages view, and module lists ignore the entries' translation state.

Desired behavior

Every rule is phrased in terms of the default language (languages[0] of GET /api/site). Mockup: Properties on Leistungen, the page ⋯ menu in EN and DE, cut Hero on Services → paste on Hosting, the "changes" chip, the globe toggle, the News module in DE.

Page properties in a translation (POST /api/form kind properties in a non-default language already describes the translation's own fields — title, slug with placeholder = the source segment (absent on the home page, the categories section and a category), menuTitle / description where the page type has them — then the source's other fields readOnly with a note, and the translation's own enabled; POST /api/page/settings/save writes only the own keys):

  • General: title, address segment, menu title, description, each with the source value under it (the same source-value box as #199, source = the default-language properties form of the page) and Copy. The segment's hint shows the resulting address live: the parent's address in this language (tree node) + the typed segment (or the placeholder when empty) + /. The home page shows no segment field, with a hint that a language's home is always /<code>/.
  • A 422 field error on slug (segment already a sibling's address in this language) shows on the field like every other field error and writes nothing.
  • Appearance: the read-only fields with their note (layout, type and weight follow the source).
  • Access: Enabled per language (built into / not built in the site; the default language is unaffected), Hidden read-only (follows the source), the address (the tree node's address in this language) and the file (content/<path> with the language suffix).
  • The docheader shows the language badge and Hide/Show .

Page menu:

  • Source page (default language): "Translate to …" for every language in which the page is untranslated (per-language state from the cached per-language trees of #192), opening the Translate dialog of #192. "Delete…" calls GET /api/page/delete-plan and names the translations deleted with the page (translations): "a translation cannot exist without its source".
  • Translation, connected: "Disconnect from " → confirmation ("becomes a free translation… there is no reconnect") → POST /api/page/disconnect {page, lang}; the page re-renders as free.
  • Translation, any mode: "Delete translation…" → confirmation naming the file (content/<path>/_index.<code>.md), that only that file goes, that subpages keep their translations → POST /api/page/delete with lang (the plan answers {deleted: 1, changed: 0}); afterwards the page shows the untranslated-page screen of #192.
  • "New subpage…" disabled outside the default language with the hint; "Rename…" renames the title of the current language's file (POST /api/page/rename already does); "Open in " and the other items unchanged.

Cut and paste across pages: the confirmation before pasting a cut element calls GET /api/element/move-refs with toParentUid (the uid of the container the paste lands in; empty for a slot) and reads translations: {moved: [code], dropped: [code]}. The dialog says which languages' translations move with the element and names the dropped ones in a warning (": the translation is deleted. has no connected translation to move it into. Translate first to keep it."); the confirm button reads "Move and drop " (danger) when anything is dropped. Ask for a same-page move into a container too (toPage = page), since a container untranslated in a language drops the copy there. Today web/src/api/links.ts sends no toParentUid and ignores the block.

Pending changes: GET /api/publish/pending, POST /api/publish-all and /api/publish/discard-all lose their lang argument (the server ignores a sent one). The panel lists one row per file: a translation is its source page's row in that language (page: {path, lang}) with a language badge; clicking a row opens that page in that language. The panel header says publishing takes every language together. Publish all and Discard all act site-wide. Page Undo and Discard… stay per file; their tooltips name the file. Tree badges are already per language (GET /api/badges?lang=); confirm they read per file. The publish link check's linkedFrom entries with lang are shown with the language badge in DisabledLinksDialog.

Languages view: the Columns / Languages toggle in the page docheader (introduced as a disabled control or omitted in #192; enabled here). For each slot of the layout (inherited slots skipped) a row of columns, one per language in languages order: the default language first as "source", each translation's cards in a dense variant of ElementCard (placeholders for untranslated uids as a dense row with an icon-only Translate), or "Not translated to " with Translate page…, and an Open button per column that switches to that language's Columns view. Data: one page fetch per language, cached with the per-language trees of #192. The view is UI state, not part of the URL.

Modules and categories: in a non-default language the entry list (GET /api/module/entries?lang=) shows the translation's title, an italic default-language title with the default code tag for an untranslated entry, and a Translation column (translated · changed since · unpublished changes · not translated, with the switch's dots; "changed" and "unpublished" from the entry's translation and badge state where the API provides them, else only translated / not translated) with an inline Translate… on untranslated entries. New is disabled outside the default language with a hint. An entry's ⋮ menu offers "Translate to …" for each language in which it is untranslated (in the default language: per-language entry lists, cached like the trees) and "Delete translation…" on a translated entry in a non-default language. The Categories module behaves the same on GET /api/categories?lang= (translation: "translated"|"missing").

Link picker and /api/link/resolve: the resolved target shows the address in the current language; when the answer carries fallback: true a note says the page is untranslated in this language and the link goes to the default-language page.

Skills and AGENTS.md (CLAUDE.md rule): no skill walks through editor screens; confirm with grep -ril "translat\|language" scaffold/site/.claude/skills/ and leave them untouched unless a step describes the editor's behaviour. AGENTS.md "Editor modules": add that a module's list shows the selected language's entries and that "New" creates in the default language, if the section's wording does not already say so.

Decisions

Settled in the grilling session and the mockup review; implement, do not reopen.

  • One publish for all languages; a half-published site is never valid.
  • Home's address is never editable; a language's home is /<code>/.
  • No reconnect. Disconnect and Delete translation always confirm.
  • The paste confirmation names dropped languages before the paste, never after.
  • The mockup's tokens are not adopted; existing PageForm, PendingChanges, PublishChip, ModuleScreen, Menu, Dialog, LinkPicker are extended.

Key interfaces

  • web/src/api/links.ts (fetchMoveRefs) — toParentUid, translations; api/publish.ts — drop lang, rows carry page.lang; api/pagemanage.ts / pagesettings.ts — disconnect, delete with lang, delete-plan translations, the properties form of a translation; api/modules.ts, api/categories.ts — translation per entry.
  • components/PageForm.tsx, PageScreen.tsx, PageEditorChrome.tsx, Docheader.tsx, Menu.tsx, DeleteDialog.tsx, ClipboardBar.tsx / the paste confirmation, PendingChanges.tsx, PublishChip.tsx, DisabledLinksDialog.tsx, ModuleScreen.tsx, LinkPicker.tsx; a new LanguagesView.
  • Stores: pendingChanges.ts, clipboard.ts, pageEditor.ts, modules.ts.
  • Mockup behaviour to mirror: docs/mockups/multilingual/m/m.js — TrPageForm, savePageProps, pageMenu, disconnect, the patched CLIP.paste, openPendingChanges, LanguagesView, ModuleMain.

Acceptance criteria

  • Page properties of services in de edit title, segment, menu title, description with source values and Copy; the address hint updates while typing; a segment that is a sibling's address shows the server's field error and writes nothing; Appearance is read-only; Access shows per-language Enabled, the address and the file; the home page has no segment field.
  • Page menu items as specified in both languages; Disconnect turns the page free; Delete translation removes only that file and lands on the untranslated-page screen; a source-page delete names its translations.
  • Pasting a cut element into a page or container that a connected translation lacks names that language as dropped in the confirmation; the move-refs request carries toParentUid; a same-page move into an untranslated container asks too.
  • Pending changes list per-file rows with language badges; Publish publishes every language with no lang sent; tree badges are per file; row clicks open the file's language.
  • The Languages view renders one column per language per slot with the specified contents and actions.
  • The News and Categories modules show the selected language's entries, the translation-state column outside the default language, New disabled with a hint, and Translate to … in the entry menu; translating an entry lands in its translation.
  • The link picker shows the current-language address and the fallback note.
  • In the default language every existing editor behaviour is unchanged; existing web tests pass unmodified apart from the pending-changes request shape.
  • web unit tests cover: the properties form of a translation and the slug error, the page menu per state, the move-refs request and dialog text for moved/dropped, the pending-changes shape, the Languages view, the module list column; go test ./... and the skills drift test pass.

How to verify

  • make build, harness serve on testdata/multilingual-site, walk every screen above against the mockup; check the URL at each step.
  • cd web && npm test; go test ./....

Out of scope

  • Engine changes; the theme; reconnect; machine translation; a settings screen; translating the editor UI.

Blocked by

## Summary Third of three editor slices of the multilingual work (contract: the PRD in #192, section 8; visual reference: `docs/mockups/multilingual/`, artifact https://claude.ai/artifact/6RYbRG7y4yMyeGxYbFsh1n). This slice is **everything around the page**: page properties of a translation, the page menu (Translate to…, Disconnect, Delete translation, the page delete plan), per-file pending changes and badges, the cut-and-paste confirmation that names dropped translations, the Languages view, the module and category lists with a translation-state column, and the link picker's language note. Builds on #192 and #199. The API (README "Translations on the wire") already answers everything below; nothing in the engine changes. ## Current behavior After #199 a translation is editable, but its properties form is the default-language form, the page menu has no translation items, pending changes still send `lang` and show one row per page, `fetchMoveRefs` sends no `toParentUid` and ignores the `translations` block, there is no Languages view, and module lists ignore the entries' `translation` state. ## Desired behavior Every rule is phrased in terms of the default language (`languages[0]` of `GET /api/site`). Mockup: Properties on Leistungen, the page ⋯ menu in EN and DE, cut Hero on Services → paste on Hosting, the "changes" chip, the globe toggle, the News module in DE. **Page properties in a translation** (`POST /api/form` kind `properties` in a non-default language already describes the translation's own fields — `title`, `slug` with `placeholder` = the source segment (absent on the home page, the categories section and a category), `menuTitle` / `description` where the page type has them — then the source's other fields `readOnly` with a `note`, and the translation's own `enabled`; `POST /api/page/settings/save` writes only the own keys): - General: title, address segment, menu title, description, each with the source value under it (the same source-value box as #199, source = the default-language properties form of the page) and Copy. The segment's hint shows the resulting address live: the parent's `address` in this language (tree node) + the typed segment (or the placeholder when empty) + `/`. The home page shows no segment field, with a hint that a language's home is always `/<code>/`. - A 422 field error on `slug` (segment already a sibling's address in this language) shows on the field like every other field error and writes nothing. - Appearance: the read-only fields with their `note` (layout, type and weight follow the source). - Access: Enabled per language (built into / not built in the <label> site; the default language is unaffected), Hidden read-only (follows the source), the address (the tree node's `address` in this language) and the file (`content/<path>` with the language suffix). - The docheader shows the language badge and Hide/Show <default label>. **Page menu**: - Source page (default language): "Translate to <label>…" for every language in which the page is untranslated (per-language state from the cached per-language trees of #192), opening the Translate dialog of #192. "Delete…" calls `GET /api/page/delete-plan` and names the translations deleted with the page (`translations`): "a translation cannot exist without its source". - Translation, connected: "Disconnect from <default label>" → confirmation ("becomes a free translation… there is no reconnect") → `POST /api/page/disconnect {page, lang}`; the page re-renders as free. - Translation, any mode: "Delete <label> translation…" → confirmation naming the file (`content/<path>/_index.<code>.md`), that only that file goes, that subpages keep their translations → `POST /api/page/delete` with `lang` (the plan answers `{deleted: 1, changed: 0}`); afterwards the page shows the untranslated-page screen of #192. - "New subpage…" disabled outside the default language with the hint; "Rename…" renames the title of the current language's file (`POST /api/page/rename` already does); "Open in <module>" and the other items unchanged. **Cut and paste across pages**: the confirmation before pasting a cut element calls `GET /api/element/move-refs` **with `toParentUid`** (the uid of the container the paste lands in; empty for a slot) and reads `translations: {moved: [code], dropped: [code]}`. The dialog says which languages' translations move with the element and names the dropped ones in a warning ("<labels>: the translation is deleted. <target title> has no connected <labels> translation to move it into. Translate <target title> first to keep it."); the confirm button reads "Move and drop <labels>" (danger) when anything is dropped. Ask for a same-page move into a container too (`toPage` = `page`), since a container untranslated in a language drops the copy there. Today `web/src/api/links.ts` sends no `toParentUid` and ignores the block. **Pending changes**: `GET /api/publish/pending`, `POST /api/publish-all` and `/api/publish/discard-all` lose their `lang` argument (the server ignores a sent one). The panel lists one row per file: a translation is its source page's row in that language (`page: {path, lang}`) with a language badge; clicking a row opens that page in that language. The panel header says publishing takes every language together. Publish all and Discard all act site-wide. Page Undo and Discard… stay per file; their tooltips name the file. Tree badges are already per language (`GET /api/badges?lang=`); confirm they read per file. The publish link check's `linkedFrom` entries with `lang` are shown with the language badge in `DisabledLinksDialog`. **Languages view**: the Columns / Languages toggle in the page docheader (introduced as a disabled control or omitted in #192; enabled here). For each slot of the layout (inherited slots skipped) a row of columns, one per language in `languages` order: the default language first as "source", each translation's cards in a dense variant of `ElementCard` (placeholders for untranslated uids as a dense row with an icon-only Translate), or "Not translated to <label>" with **Translate page…**, and an **Open** button per column that switches to that language's Columns view. Data: one page fetch per language, cached with the per-language trees of #192. The view is UI state, not part of the URL. **Modules and categories**: in a non-default language the entry list (`GET /api/module/entries?lang=`) shows the translation's title, an italic default-language title with the default code tag for an untranslated entry, and a **Translation** column (translated · <default label> changed since · unpublished changes · not translated, with the switch's dots; "changed" and "unpublished" from the entry's `translation` and badge state where the API provides them, else only translated / not translated) with an inline **Translate…** on untranslated entries. **New** is disabled outside the default language with a hint. An entry's ⋮ menu offers "Translate to <label>…" for each language in which it is untranslated (in the default language: per-language entry lists, cached like the trees) and "Delete <label> translation…" on a translated entry in a non-default language. The Categories module behaves the same on `GET /api/categories?lang=` (`translation: "translated"|"missing"`). **Link picker and `/api/link/resolve`**: the resolved target shows the `address` in the current language; when the answer carries `fallback: true` a note says the page is untranslated in this language and the link goes to the default-language page. **Skills and AGENTS.md** (CLAUDE.md rule): no skill walks through editor screens; confirm with `grep -ril "translat\|language" scaffold/site/.claude/skills/` and leave them untouched unless a step describes the editor's behaviour. AGENTS.md "Editor modules": add that a module's list shows the selected language's entries and that "New" creates in the default language, if the section's wording does not already say so. ## Decisions Settled in the grilling session and the mockup review; implement, do not reopen. - One publish for all languages; a half-published site is never valid. - Home's address is never editable; a language's home is `/<code>/`. - No reconnect. Disconnect and Delete translation always confirm. - The paste confirmation names dropped languages before the paste, never after. - The mockup's tokens are not adopted; existing `PageForm`, `PendingChanges`, `PublishChip`, `ModuleScreen`, `Menu`, `Dialog`, `LinkPicker` are extended. ## Key interfaces - `web/src/api/links.ts` (`fetchMoveRefs`) — `toParentUid`, `translations`; `api/publish.ts` — drop `lang`, rows carry `page.lang`; `api/pagemanage.ts` / `pagesettings.ts` — disconnect, delete with lang, delete-plan `translations`, the properties form of a translation; `api/modules.ts`, `api/categories.ts` — `translation` per entry. - `components/PageForm.tsx`, `PageScreen.tsx`, `PageEditorChrome.tsx`, `Docheader.tsx`, `Menu.tsx`, `DeleteDialog.tsx`, `ClipboardBar.tsx` / the paste confirmation, `PendingChanges.tsx`, `PublishChip.tsx`, `DisabledLinksDialog.tsx`, `ModuleScreen.tsx`, `LinkPicker.tsx`; a new `LanguagesView`. - Stores: `pendingChanges.ts`, `clipboard.ts`, `pageEditor.ts`, `modules.ts`. - Mockup behaviour to mirror: `docs/mockups/multilingual/m/m.js` — `TrPageForm`, `savePageProps`, `pageMenu`, `disconnect`, the patched `CLIP.paste`, `openPendingChanges`, `LanguagesView`, `ModuleMain`. ## Acceptance criteria - [ ] Page properties of `services` in `de` edit title, segment, menu title, description with source values and Copy; the address hint updates while typing; a segment that is a sibling's address shows the server's field error and writes nothing; Appearance is read-only; Access shows per-language Enabled, the address and the file; the home page has no segment field. - [ ] Page menu items as specified in both languages; Disconnect turns the page free; Delete translation removes only that file and lands on the untranslated-page screen; a source-page delete names its translations. - [ ] Pasting a cut element into a page or container that a connected translation lacks names that language as dropped in the confirmation; the move-refs request carries `toParentUid`; a same-page move into an untranslated container asks too. - [ ] Pending changes list per-file rows with language badges; Publish publishes every language with no `lang` sent; tree badges are per file; row clicks open the file's language. - [ ] The Languages view renders one column per language per slot with the specified contents and actions. - [ ] The News and Categories modules show the selected language's entries, the translation-state column outside the default language, New disabled with a hint, and Translate to … in the entry menu; translating an entry lands in its translation. - [ ] The link picker shows the current-language address and the fallback note. - [ ] In the default language every existing editor behaviour is unchanged; existing web tests pass unmodified apart from the pending-changes request shape. - [ ] `web` unit tests cover: the properties form of a translation and the slug error, the page menu per state, the move-refs request and dialog text for moved/dropped, the pending-changes shape, the Languages view, the module list column; `go test ./...` and the skills drift test pass. ## How to verify - `make build`, `harness serve` on `testdata/multilingual-site`, walk every screen above against the mockup; check the URL at each step. - `cd web && npm test`; `go test ./...`. ## Out of scope - Engine changes; the theme; reconnect; machine translation; a settings screen; translating the editor UI. ## Blocked by - #199
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#200
No description provided.