Multilingual editor (2/3): Columns view of a translation, Translate element, element form with source values #199

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

Summary

Second 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 editing inside a translation: the Columns view of a connected translation (translated cards, placeholder rows, badges, banners, no structural controls), Translate element / Translate all, the free-translation grid, the element form with the default-language value under every translatable field, and the delete confirmation that names affected translations.

Builds on #192 (language switch, tree state, untranslated-page screen, Translate page dialog). Follow-up: #200 (page properties, page menu, pending changes, paste confirmation, Languages view, module lists).

Current behavior

After #192 a translated page opens in the ordinary grid: its elements are cards, insert rows and element operations are hidden when the page answer says translation.mode: "connected", nothing marks untranslated or changed elements, and the element form is the default-language form. The API (README "Translations on the wire") already answers everything below; nothing in the engine changes.

Desired behavior

Every rule is phrased in terms of the default language (languages[0] of GET /api/site), never "English". Mockup: Services in Deutsch, the Cards element, Home in Deutsch (free) and Français (connected), About us in Deutsch.

Columns view of a connected translation (GET /api/page in a non-default language answers translation: {mode: "connected", source: {path, lang}, changed: [uid], untranslated: [uid], synced: {<element name>: [param]}}; the server repairs the structure to the source's on open):

  • The backend layout as today. Each translated element is a card with a "↔ #" badge (tooltip: connected to the default-language element with the same uid; position, nesting and appearance follow it). The uid is the same in every language.
  • Each uid in untranslated is a placeholder row at its source position (source order within the slot: fetch the source page's elements with GET /api/page/elements?path=&lang=<default> and interleave), showing the element label, uid, the Disabled badge when the source element is disabled, and "Not translated · : ", with a Translate button and a ⋮ menu (Translate; Open element; Leave untranslated — a tooltip-level explanation, no request). Translate = POST /api/element/translate {page, lang, uid}, then open the new element's form. An element whose parent is untranslated answers 409 "translate the parent first": show the server's message.
  • Each uid in changed carries the " changed" badge (amber) on its card.
  • No insert rows in any slot, no drag handles, no drop zones; the card menu offers Edit and Enable/Disable only (insert, move, duplicate, delete and cut/copy/paste answer 409 — the UI must not offer them). A note per slot: "Connected: order and appearance follow the page. Insert, move, duplicate and delete there. Open →" (switches the language, keeps the page).
  • An inherited slot shows the inheriting page's translation in this language, or "Empty — not translated on <that page's title>".
  • Banners above the grid, in this order, each only when applicable: (1) "N elements changed in after being translated…" with Review first (opens the first changed element's form); (2) "This translation is disabled…" with Enable (POST /api/page/enabled) — this one replaces today's disabled-page banner in a non-default language; (3) "N of M elements are not translated yet…" with Translate all (POST /api/element/translate-all {page, lang}, then re-render; the answer's translated count goes into the toast). The Content tab count reads translated/total in a translation.
  • Free translation (translation.mode: "free"): the ordinary grid with insert rows and every element operation, exactly as a source page, plus a "Free translation" badge in the page docheader. No change tracking.
  • Default language: unchanged. Existing web tests pass unmodified.

Delete on a source page names the translations deleted with it: the element delete confirmation calls GET /api/element/delete-plan?page=&lang=&uid= and, when translations is non-empty, says "Its translation(s) are deleted with it: connected translations follow the structure." The delete button then reads "Delete in N languages". (The page-level delete plan is in #200.)

Element form in a translation (route /edit/<lang>/<path>/<uid>):

  • General tab: under every translatable field (general-tab params and the body — text, rich text, textarea, image, link, page, categories, number, select, bool alike) a source-value box: "" eyebrow, the source element's value rendered read-only (rich text rendered, images by file name, a page link by title, categories as a list), and a Copy button that writes the source value into the field and marks the form dirty. Source values come from the source element (GET /api/page/elements?path=&lang=<default> or the element fetch the form already uses, with lang = default). A docheader toggle Hide/Show hides all boxes (a per-session preference).
  • When the uid is in changed: an amber note at the top of General ("The element changed after this translation was saved. Compare the values under each field. Saving this form is the review: it clears the mark.") and every source box is amber with the eyebrow " — changed since this translation". Change tracking is per element (the l10nSource hash), not per field; the UI must not pretend to know which field changed.
  • Items collections in a connected translation: the rows are the source's rows (same uids, same order); each row's fields show the source item's values under them; no Add, Remove, Move up/down controls (a save that adds or removes a row answers 409, a reorder is accepted but saved in source order — hide all three). A sync note replaces the Add button: "One row per . Add, remove and reorder them on the element; the translation follows." Per-row Enable/Disable stays.
  • Appearance tab, connected: every field read-only, valued from the source, with the note "Synced from the element # on every save of it. Change it there; the translation follows." The set of synced params is translation.synced[<element name>]. Free: editable as today.
  • Access tab: Enabled is per language (label: "Shown on the site" / "Kept in the file, not shown. is unaffected."); the anchor hint says the uid is the same in every language.
  • Docheader: language badge, Hide/Show , Open (the same uid in the default language, through the unsaved-changes guard), the " changed" badge; Save's tooltip says it clears the mark. Preview opens the View module in this language scrolled to #c-<uid> — verify PreviewPane / previewmap.ts match permalinks with the language prefix (an "assumption to check" of the original brief).
  • Saving a connected translation's element: the server forces synced params and rewrites l10nSource; after save the badge disappears without a reload (re-read the page answer the save returns, which carries translation).

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 on a translation. AGENTS.md "Translations" already describes the file-level rules; add one sentence under "Editor modules" only if this slice changes what it states.

Decisions

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

  • Same uid in every language; the "↔ #uid" badge is a status, not a second id.
  • Per-element change marks, shown on every field of a marked element.
  • Connected translations show no structural controls at all; the server's 409 is the backstop, not the UI.
  • Free copies get no per-card badge, only the page badge.
  • The mockup's Source Sans tokens are not adopted; web/src/styles.css tokens and the existing ElementCard, InsertRow, LayoutGrid, ElementForm, FormScreen, PageBanner, Docheader are extended, not duplicated.

Key interfaces

  • web/src/stores/pageEditor.ts — the page answer's translation block; web/src/api/pageedit.ts / elements.ts — source elements per language; api/elementops.ts — translate, translate-all, delete-plan.
  • components/LayoutGrid.tsx, ElementCard.tsx, InsertRow.tsx, PageContent.tsx, PageBanner.tsx, ElementDeleteDialog.tsx — the Columns view; new TranslationPlaceholder (or similar) for the placeholder row.
  • components/ElementForm.tsx, FormScreen.tsx, FormLayout.tsx, the items collection component — source-value boxes, synced read-only fields, the items rules.
  • previewmap.ts, PreviewPane.tsx — language-prefixed permalinks.
  • Mockup behaviour to mirror: docs/mockups/multilingual/m/m.js — TrSlotColumn, trCard, Banners, trElementGroups, decorateItems, formDocheader, the patched elementMenu.

Acceptance criteria

  • Opening services in de on testdata/multilingual-site shows translated cards with "↔ #uid", placeholder rows at source positions with Translate, the changed badge where the fixture marks it, no insert rows or drag handles, the per-slot note, and the applicable banners; Translate creates the element (POST /api/element/translate) and opens its form; Translate all creates every missing one.
  • A free translation shows insert rows, drag and drop and the full card menu, and the "Free translation" badge.
  • The element form in a connected translation shows the source value under each General field with a working Copy, amber boxes and the note for a changed element, read-only Appearance with the synced note, per-language Enabled, the Open and Hide/Show buttons; items collections show the source item under each row and offer no add/remove/reorder.
  • Saving a changed element clears the badge in the grid without a reload; reloading agrees.
  • Deleting a source element whose translations exist names them in the confirmation (delete-plan).
  • The View module in de scrolls to the element the form's Preview names.
  • In the default language every existing editor behaviour is unchanged; existing web tests pass unmodified.
  • web unit tests cover: placeholder interleaving from untranslated and source order, the structural-control guard, banner selection, the source-value box and Copy, the items rules, the changed-mark lifecycle through a save; go test ./... and the skills drift test pass.

How to verify

  • make build, harness serve on testdata/multilingual-site, walk Services/About/Home in de against the mockup's Columns view and forms; check the URL at each step.
  • cd web && npm test; go test ./....

Out of scope

  • Page properties, the page menu's Disconnect / Delete translation / page-level delete plan, per-file pending changes, the paste confirmation, the Languages view, module lists: #200.
  • Engine changes; the theme; reconnect; machine translation.

Blocked by

## Summary Second 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 **editing inside a translation**: the Columns view of a connected translation (translated cards, placeholder rows, badges, banners, no structural controls), Translate element / Translate all, the free-translation grid, the element form with the default-language value under every translatable field, and the delete confirmation that names affected translations. Builds on #192 (language switch, tree state, untranslated-page screen, Translate page dialog). Follow-up: #200 (page properties, page menu, pending changes, paste confirmation, Languages view, module lists). ## Current behavior After #192 a translated page opens in the ordinary grid: its elements are cards, insert rows and element operations are hidden when the page answer says `translation.mode: "connected"`, nothing marks untranslated or changed elements, and the element form is the default-language form. The API (README "Translations on the wire") already answers everything below; nothing in the engine changes. ## Desired behavior Every rule is phrased in terms of the default language (`languages[0]` of `GET /api/site`), never "English". Mockup: Services in Deutsch, the Cards element, Home in Deutsch (free) and Français (connected), About us in Deutsch. **Columns view of a connected translation** (`GET /api/page` in a non-default language answers `translation: {mode: "connected", source: {path, lang}, changed: [uid], untranslated: [uid], synced: {<element name>: [param]}}`; the server repairs the structure to the source's on open): - The backend layout as today. Each translated element is a card with a "↔ #<uid>" badge (tooltip: connected to the default-language element with the same uid; position, nesting and appearance follow it). The uid is the same in every language. - Each uid in `untranslated` is a **placeholder row** at its source position (source order within the slot: fetch the source page's elements with `GET /api/page/elements?path=&lang=<default>` and interleave), showing the element label, uid, the Disabled badge when the source element is disabled, and "Not translated · <default label>: <summary of the source element>", with a **Translate** button and a ⋮ menu (Translate; Open <default label> element; Leave untranslated — a tooltip-level explanation, no request). Translate = `POST /api/element/translate {page, lang, uid}`, then open the new element's form. An element whose parent is untranslated answers 409 "translate the parent first": show the server's message. - Each uid in `changed` carries the "<default label> changed" badge (amber) on its card. - **No insert rows** in any slot, no drag handles, no drop zones; the card menu offers Edit and Enable/Disable only (insert, move, duplicate, delete and cut/copy/paste answer 409 — the UI must not offer them). A note per slot: "Connected: order and appearance follow the <default label> page. Insert, move, duplicate and delete there. Open <default label> →" (switches the language, keeps the page). - An inherited slot shows the inheriting page's translation in this language, or "Empty — not translated on <that page's title>". - **Banners** above the grid, in this order, each only when applicable: (1) "N elements changed in <default label> after being translated…" with **Review first** (opens the first changed element's form); (2) "This translation is disabled…" with **Enable** (`POST /api/page/enabled`) — this one replaces today's disabled-page banner in a non-default language; (3) "N of M elements are not translated yet…" with **Translate all** (`POST /api/element/translate-all {page, lang}`, then re-render; the answer's `translated` count goes into the toast). The Content tab count reads `translated/total` in a translation. - **Free translation** (`translation.mode: "free"`): the ordinary grid with insert rows and every element operation, exactly as a source page, plus a "Free translation" badge in the page docheader. No change tracking. - **Default language**: unchanged. Existing web tests pass unmodified. **Delete on a source page names the translations deleted with it**: the element delete confirmation calls `GET /api/element/delete-plan?page=&lang=&uid=` and, when `translations` is non-empty, says "Its <labels> translation(s) are deleted with it: connected translations follow the <default label> structure." The delete button then reads "Delete in N languages". (The page-level delete plan is in #200.) **Element form in a translation** (route `/edit/<lang>/<path>/<uid>`): - **General tab**: under every translatable field (general-tab params and the body — text, rich text, textarea, image, link, page, categories, number, select, bool alike) a **source-value box**: "<DEFAULT LABEL>" eyebrow, the source element's value rendered read-only (rich text rendered, images by file name, a page link by title, categories as a list), and a **Copy** button that writes the source value into the field and marks the form dirty. Source values come from the source element (`GET /api/page/elements?path=&lang=<default>` or the element fetch the form already uses, with `lang` = default). A docheader toggle **Hide/Show <default label>** hides all boxes (a per-session preference). - When the uid is in `changed`: an amber note at the top of General ("The <default label> element changed after this translation was saved. Compare the <default label> values under each field. Saving this form is the review: it clears the mark.") and every source box is amber with the eyebrow "<DEFAULT LABEL> — changed since this translation". Change tracking is per element (the `l10nSource` hash), not per field; the UI must not pretend to know which field changed. - **Items collections** in a connected translation: the rows are the source's rows (same uids, same order); each row's fields show the source item's values under them; no Add, Remove, Move up/down controls (a save that adds or removes a row answers 409, a reorder is accepted but saved in source order — hide all three). A sync note replaces the Add button: "One row per <default label> <item label>. Add, remove and reorder them on the <default label> element; the translation follows." Per-row Enable/Disable stays. - **Appearance tab**, connected: every field read-only, valued from the source, with the note "Synced from the <default label> element #<uid> on every save of it. Change it there; the translation follows." The set of synced params is `translation.synced[<element name>]`. Free: editable as today. - **Access tab**: Enabled is per language (label: "Shown on the <label> site" / "Kept in the <label> file, not shown. <default label> is unaffected."); the anchor hint says the uid is the same in every language. - **Docheader**: language badge, Hide/Show <default label>, **Open <default label>** (the same uid in the default language, through the unsaved-changes guard), the "<default label> changed" badge; Save's tooltip says it clears the mark. Preview opens the View module in this language scrolled to `#c-<uid>` — verify `PreviewPane` / `previewmap.ts` match permalinks with the language prefix (an "assumption to check" of the original brief). - Saving a connected translation's element: the server forces synced params and rewrites `l10nSource`; after save the badge disappears without a reload (re-read the page answer the save returns, which carries `translation`). **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 on a translation. AGENTS.md "Translations" already describes the file-level rules; add one sentence under "Editor modules" only if this slice changes what it states. ## Decisions Settled in the grilling session and the mockup review; implement, do not reopen. - Same uid in every language; the "↔ #uid" badge is a status, not a second id. - Per-element change marks, shown on every field of a marked element. - Connected translations show no structural controls at all; the server's 409 is the backstop, not the UI. - Free copies get no per-card badge, only the page badge. - The mockup's Source Sans tokens are not adopted; `web/src/styles.css` tokens and the existing `ElementCard`, `InsertRow`, `LayoutGrid`, `ElementForm`, `FormScreen`, `PageBanner`, `Docheader` are extended, not duplicated. ## Key interfaces - `web/src/stores/pageEditor.ts` — the page answer's `translation` block; `web/src/api/pageedit.ts` / `elements.ts` — source elements per language; `api/elementops.ts` — `translate`, `translate-all`, `delete-plan`. - `components/LayoutGrid.tsx`, `ElementCard.tsx`, `InsertRow.tsx`, `PageContent.tsx`, `PageBanner.tsx`, `ElementDeleteDialog.tsx` — the Columns view; new `TranslationPlaceholder` (or similar) for the placeholder row. - `components/ElementForm.tsx`, `FormScreen.tsx`, `FormLayout.tsx`, the items collection component — source-value boxes, synced read-only fields, the items rules. - `previewmap.ts`, `PreviewPane.tsx` — language-prefixed permalinks. - Mockup behaviour to mirror: `docs/mockups/multilingual/m/m.js` — `TrSlotColumn`, `trCard`, `Banners`, `trElementGroups`, `decorateItems`, `formDocheader`, the patched `elementMenu`. ## Acceptance criteria - [ ] Opening `services` in `de` on `testdata/multilingual-site` shows translated cards with "↔ #uid", placeholder rows at source positions with Translate, the changed badge where the fixture marks it, no insert rows or drag handles, the per-slot note, and the applicable banners; Translate creates the element (`POST /api/element/translate`) and opens its form; Translate all creates every missing one. - [ ] A free translation shows insert rows, drag and drop and the full card menu, and the "Free translation" badge. - [ ] The element form in a connected translation shows the source value under each General field with a working Copy, amber boxes and the note for a changed element, read-only Appearance with the synced note, per-language Enabled, the Open <default> and Hide/Show <default> buttons; items collections show the source item under each row and offer no add/remove/reorder. - [ ] Saving a changed element clears the badge in the grid without a reload; reloading agrees. - [ ] Deleting a source element whose translations exist names them in the confirmation (`delete-plan`). - [ ] The View module in `de` scrolls to the element the form's Preview names. - [ ] In the default language every existing editor behaviour is unchanged; existing web tests pass unmodified. - [ ] `web` unit tests cover: placeholder interleaving from `untranslated` and source order, the structural-control guard, banner selection, the source-value box and Copy, the items rules, the changed-mark lifecycle through a save; `go test ./...` and the skills drift test pass. ## How to verify - `make build`, `harness serve` on `testdata/multilingual-site`, walk Services/About/Home in `de` against the mockup's Columns view and forms; check the URL at each step. - `cd web && npm test`; `go test ./...`. ## Out of scope - Page properties, the page menu's Disconnect / Delete translation / page-level delete plan, per-file pending changes, the paste confirmation, the Languages view, module lists: #200. - Engine changes; the theme; reconnect; machine translation. ## Blocked by - #192
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#199
No description provided.