feat(editor): multilingual editor 2/3 — Columns view of a translation, Translate element, element form with source values #202

Merged
dominik.polakovics merged 6 commits from afk/199 into main 2026-10-11 15:26:46 +02:00

Closes #199

Second editor slice of the multilingual work (PRD #192 §8, mockup docs/mockups/multilingual/). Engine untouched; everything rides the existing API ("Translations on the wire").

Columns view of a connected translation

  • Translated cards carry ↔ #uid. A card is marked amber " changed" when its uid, or the uid of one of its item rows, is in translation.changed. The engine hashes items separately, so a Cards element whose card changed is marked too.
  • Placeholder rows for untranslated elements at their source position. The source page (GET /api/page in the default language, held by the page editor as sourceData()) is walked per slot or container (web/src/translationgrid.ts, pure and unit-tested). Placeholders show for top-most untranslated elements only.
  • Each placeholder has the label, the uid, the Disabled badge and "Not translated · : ", plus Translate (POST /api/element/translate, then the form opens; a 409 shows the server's message) and a ⋮ menu (Translate / Open element / Leave untranslated).
  • No insert rows, drag or structural menu items (as in #192), plus a per-slot "Connected: … Open →" note. Inherited slots show the inheriting page's translation or "Empty — not translated on
Closes #199 Second editor slice of the multilingual work (PRD #192 §8, mockup `docs/mockups/multilingual/`). Engine untouched; everything rides the existing API ("Translations on the wire"). ## Columns view of a connected translation - Translated cards carry **↔ #uid**. A card is marked amber **"<default> changed"** when its uid, or the uid of one of its item rows, is in `translation.changed`. The engine hashes items separately, so a Cards element whose card changed is marked too. - **Placeholder rows** for untranslated elements at their source position. The source page (`GET /api/page` in the default language, held by the page editor as `sourceData()`) is walked per slot or container (`web/src/translationgrid.ts`, pure and unit-tested). Placeholders show for top-most untranslated elements only. - Each placeholder has the label, the uid, the Disabled badge and "Not translated · <default>: <summary>", plus **Translate** (`POST /api/element/translate`, then the form opens; a 409 shows the server's message) and a ⋮ menu (Translate / Open <default> element / Leave untranslated). - No insert rows, drag or structural menu items (as in #192), plus a per-slot "Connected: … Open <default> →" note. Inherited slots show the inheriting page's translation or "Empty — not translated on <title>". - Banners, in this order: changed (**Review first**), translation disabled (**Enable**, replaces the disabled-page banner in a translation), not translated (**Translate all**, with the count in the page's notice strip, since the editor has no toast). - The Content tab reads `translated/total`. The docheader shows the language badge, plus **Free translation** for a free one. A free translation keeps the full grid. ## Element form in a translation - **Connected:** a source-value box with **Copy** under every General field, item-row field and the body. - Source values come from the same `POST /api/form` description addressed to the source element, so Copy writes the widget's own value shape. - Rich text is rendered, images show by file name, links and pages by title, categories as a list. - **Hide/Show <default>** is a per-session preference. - When the element or an item changed: the amber note and amber boxes. A changed item row gets its own marker, which is honest because item hashes are per item. One save restamps the element and its items (`l10n.StampSource`). - Items follow the source rows: no add, remove or reorder, a sync note instead, per-row Enable stays. - Synced Appearance params are read-only, valued from the source. - **Any translation:** per-language Enabled wording and the anchor hint "same uid in every language". - **Connected docheader:** **Open <default>** (goes through the guard), the changed badge (cleared by the save's answer without a reload), and a Save tooltip. Duplicate and the structural ⋯ menu are not offered. - **Preview:** `previewmap.ts` keyed pages by the language-neutral `permalink`, so on any multilingual site the View module opened the wrong URL. It now maps by the node's published `address`, falling back to `permalink` for older answers. This also fixes the page View button. ## Delete confirmation - `GET /api/element/delete-plan` is asked only on a multilingual site in the default language. When it returns translations, the confirmation says "Its Deutsch translation is deleted with it: …" and the button reads "Delete in N languages", where N includes the source, as in the mockup. ## Fixture - `testdata/multilingual-site` Services DE is now a connected translation: - hero up to date; - the Cards element with an items collection (new `cards`/`card` sidecars) and one changed item; - a changed notice; - a disabled notice left untranslated. - The `l10nSource` hashes were produced by the engine. About DE (free) gained elements. - Four Go test expectations follow Services going from free to connected. ## Docs / skills - No skill walks through editor screens on a translation; `grep -ril "translat\|language" scaffold/site/.claude/skills/` lists only file-level procedures. AGENTS.md "Editor modules" states nothing this slice changes, so both are unchanged. ## Verification - `cd web && npx tsc --noEmit -p .`: clean. `npx vitest run`: 101 files, 2662 tests pass. Existing tests are unmodified; new tests cover interleaving, the guard, banners, Translate/Translate all, source boxes and Copy, the items rules, the changed-mark lifecycle through a save, the delete plan and the language-prefixed preview. - `npm run build`, `go vet ./...`, `go test ./...` (incl. the skills drift test): pass. - I ran `harness serve` on a copy of the fixture and confirmed the API side: the DE page answer's translation block, translate (path `[1]`), translate-all, delete-plan `["de"]`, and that the edit route serves. - **Not done:** a click-through of the UI in a browser (no browser in this run). The page/form walk against the mockup is still to do by hand. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Translated cards carry a "↔ #uid" badge; a card whose element or one of
  its item rows changed in the default language carries the amber
  "<default> changed" badge.
- Placeholder rows for untranslated elements at their source positions
  (interleaved from the source page's tree), with Translate, Open
  <default> element and Leave untranslated; Translate opens the new
  element's form, a refusal shows the server's message.
- Per-slot "Connected: order and appearance follow the <default> page"
  note; inherited slots show the inheriting page's translation or
  "Empty — not translated on <title>".
- Banners in order: changed (Review first), translation disabled
  (Enable), not translated (Translate all, count in the notice strip).
- Content tab reads translated/total; the docheader shows the language
  badge and, for a free translation, "Free translation".

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
feat(editor): element form in a translation, language-aware preview
All checks were successful
ci / build-and-test (pull_request) Successful in 14m20s
e54db2d761
- Connected translation: a source-value box with Copy under every
  translatable field, item-row field and the body; Hide/Show <default>
  (per session); amber boxes and the review note while the element or one
  of its items changed; items follow the source rows (no add, remove or
  reorder, sync note); synced Appearance params read-only from the source.
- Any translation: per-language Enabled wording, the anchor hint, the
  language badge; connected forms add Open <default> and the changed
  badge (cleared by the save's answer), and drop Duplicate and the
  structural ⋯ menu; free forms get the "Free translation" badge.
- Preview maps pages by their published address, so a translation (and
  every language of a multilingual site) opens at its real URL.

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

[autoland] verdict: pass

PASS — validated by the autoland lander.

Signal: labctl pr checks 202 aggregate success (ci / build-and-test, 14m20s: npm ci/build/test, go vet, go test incl. the skills and AGENTS.md drift tests, go build). Not re-run locally.

Conventions: Conventional Commits title; Closes #199 present; branch is current with main (merge-base = origin/main), no conflicts.

Scope matches #199: Columns view of a connected translation (web/src/translationgrid.ts, LayoutGrid/TranslationPlaceholder), banners (PageBanner.tsx), the element form's source-value boxes, items rules and synced Appearance (ElementForm.tsx, SourceValue.tsx, FormScreen.tsx), the delete plan (ElementDeleteDialog.tsx), and the address-based preview map (previewmap.ts), which the issue asked to be checked. The fixture change (services/_index.de.md connected, cards/card sidecars) is what the acceptance criteria need. The four Go test expectations that changed follow from that fixture. Existing web tests are unmodified apart from one widened import in translateApi.test.ts. Skills and AGENTS.md are rightly left alone: AGENTS.md "Translations" already covers Translate element/all and the change mark, and no skill walks through editor screens on a translation.

Note (non-blocking, disclosed in the PR): the walk through the UI in a browser against the mockup has not been done.

[autoland] verdict: pass PASS — validated by the autoland lander. Signal: `labctl pr checks 202` aggregate **success** (ci / build-and-test, 14m20s: npm ci/build/test, go vet, go test incl. the skills and AGENTS.md drift tests, go build). Not re-run locally. Conventions: Conventional Commits title; `Closes #199` present; branch is current with main (merge-base = origin/main), no conflicts. Scope matches #199: Columns view of a connected translation (web/src/translationgrid.ts, LayoutGrid/TranslationPlaceholder), banners (PageBanner.tsx), the element form's source-value boxes, items rules and synced Appearance (ElementForm.tsx, SourceValue.tsx, FormScreen.tsx), the delete plan (ElementDeleteDialog.tsx), and the address-based preview map (previewmap.ts), which the issue asked to be checked. The fixture change (services/_index.de.md connected, cards/card sidecars) is what the acceptance criteria need. The four Go test expectations that changed follow from that fixture. Existing web tests are unmodified apart from one widened import in translateApi.test.ts. Skills and AGENTS.md are rightly left alone: AGENTS.md "Translations" already covers Translate element/all and the change mark, and no skill walks through editor screens on a translation. Note (non-blocking, disclosed in the PR): the walk through the UI in a browser against the mockup has not been done.
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!202
No description provided.