feat(i18n): German editor catalog and layout pass #237

Merged
dominik.polakovics merged 3 commits from afk/226 into main 2026-10-12 22:02:47 +02:00

Ships the editor in German: a complete de catalog, the agreed glossary terms, and a layout pass so the longer German strings fit the chrome.

Closes #226

Catalog

  • web/src/i18n/de.json: all 1171 keys — UI strings, plural forms (.one/.other; Intl.PluralRules("de") has only those two, so no third form is needed), and server.* for every exported error code, with exactly the English placeholders ({x}, {x|q}, {x|list}).
  • Register: impersonal first ("Seite löschen?", "Noch nicht veröffentlicht"), instructions as infinitives, formal "Sie" only where a sentence must address the reader, never "du". Language labels (site-authored nouns like "Deutsch") are placed as "in {label}" rather than inflected.
  • glossary.md: the Terms table now has a binding German column — the issue's terms (Veröffentlichen, Ausstehende Änderungen, Verwerfen, Rückgängig, Seite, Bereich, Element, Mediathek, Entwurf, Übersetzung, Nicht verwendete Inhalte, Live-Website, Speichern) plus ~60 more settled during a consistency pass (slot → Spalte, so it never clashes with section → Bereich; Aufklappen/Zuklappen; Bereitstellung; Verweis; …), and notes on language-label placeholders.
  • catalog.ts ships { en, de }; the parity test covers de (every used key, every server code, nothing unused, no stray placeholders).
  • Smoke test (i18nShell.test.tsx): boots with Accept-Language: de-AT,de,en, asserts locale() === "de", html[lang="de"], the rail's "Medien", and a server error translated by code; plus a bare de host choice formatting under the browser's de-CH. Tests that used de as the "no catalog" example now use fr.
  • Dates/relative times: relative times come from catalog keys ("vor 5 Minuten", "gerade eben"); absolute dates use Intl under the formatting locale (de-AT, de-DE, …) — acceptable, no custom key set needed.
  • README ("The editor's language", "Editor language catalogs") and scaffold/site/AGENTS.md say the editor ships English and German. Skills checked: setup-site and verify-site already say the editor follows the browser's language and point at AGENTS.md "Languages" — still accurate, unchanged.

Layout pass (web/src/styles.css)

Measured with a headless-Chromium script that walks the editor on a copy of testdata/multilingual-site and reports clipped/overflowing elements, at 1280px and 720px (the narrowest width the stylesheet targets: @media (max-width: 720px) / @container shell (min-width: 721px)).

  • .app's column was auto, so it grew to the body's min-content (up to 1080px in German, 952px in English) and clipped the docheader's right side — language switch, badges, Hugo pill. Now minmax(0, 1fr).
  • Docheader bar, context row and heading wrap between items; buttons, toggles, crumbs and the language label never break inside a label.
  • Dialog button rows wrap between buttons ("Speichern und fortfahren" broke over three lines).
  • Standalone header: publish chip and Publish button stay on one line; the status pill wraps inside itself; the pending panel no longer hangs past the window's left edge at narrow widths.
  • Media details panel shrinks before the browser's toolbar does.

Overflow findings German adds over English: before 0 at 1280 / 66 at 720 → after 0 / 0. English at 1280 is pixel-identical apart from the unsaved-changes dialog; at 720 English also stops clipping. Full vitest suite green (116 files, 2862 tests), make test, make build, go vet, tsc clean.

Not covered: the page tree has no filter field (filters exist only in the link/page pickers), and the rail is icon-only, so neither had German text to fit.

Follow-up

A few form labels are generated by the engine in Go, not the site ("Address segment" on translation forms, the implicit "Title", "Backend layout"), so they stay English in a German editor. The issue kept the server out of scope; filed as #236.

Screenshots

English (after) · German before · German after. The images live on the orphan branch screenshots/issue-226 (commit e0b0dd5), which is never merged; nothing on this branch adds them to the tree.

Screen English German before German after
720-page-tree
1280-page-tree
720-slug-error
720-props-de-access
720-element-form-error
720-confirm-unsaved
1280-confirm-unsaved
720-publish-running
1280-publish-running
720-pending
1280-pending
720-page-pending
720-media-detail
720-insert
1280-insert
1280-error-strip

For the German-speaking reviewer

Strings the consistency pass flagged as worth a native look: element.move.droppedContainer, element.move.media, translate.subtitle ("aus {source}"), translate.subpages.*, form.items.noSchema.

🤖 Generated with Claude Code

Ships the editor in German: a complete `de` catalog, the agreed glossary terms, and a layout pass so the longer German strings fit the chrome. Closes #226 ## Catalog - `web/src/i18n/de.json`: all 1171 keys — UI strings, plural forms (`.one`/`.other`; `Intl.PluralRules("de")` has only those two, so no third form is needed), and `server.*` for every exported error code, with exactly the English placeholders (`{x}`, `{x|q}`, `{x|list}`). - Register: impersonal first ("Seite löschen?", "Noch nicht veröffentlicht"), instructions as infinitives, formal "Sie" only where a sentence must address the reader, never "du". Language labels (site-authored nouns like "Deutsch") are placed as "in {label}" rather than inflected. - `glossary.md`: the Terms table now has a binding German column — the issue's terms (Veröffentlichen, Ausstehende Änderungen, Verwerfen, Rückgängig, Seite, Bereich, Element, Mediathek, Entwurf, Übersetzung, Nicht verwendete Inhalte, Live-Website, Speichern) plus ~60 more settled during a consistency pass (slot → Spalte, so it never clashes with section → Bereich; Aufklappen/Zuklappen; Bereitstellung; Verweis; …), and notes on language-label placeholders. - `catalog.ts` ships `{ en, de }`; the parity test covers `de` (every used key, every server code, nothing unused, no stray placeholders). - Smoke test (`i18nShell.test.tsx`): boots with `Accept-Language: de-AT,de,en`, asserts `locale() === "de"`, `html[lang="de"]`, the rail's "Medien", and a server error translated by code; plus a bare `de` host choice formatting under the browser's `de-CH`. Tests that used `de` as the "no catalog" example now use `fr`. - Dates/relative times: relative times come from catalog keys ("vor 5 Minuten", "gerade eben"); absolute dates use Intl under the formatting locale (`de-AT`, `de-DE`, …) — acceptable, no custom key set needed. - README ("The editor's language", "Editor language catalogs") and `scaffold/site/AGENTS.md` say the editor ships English and German. Skills checked: `setup-site` and `verify-site` already say the editor follows the browser's language and point at AGENTS.md "Languages" — still accurate, unchanged. ## Layout pass (`web/src/styles.css`) Measured with a headless-Chromium script that walks the editor on a copy of `testdata/multilingual-site` and reports clipped/overflowing elements, at 1280px and 720px (the narrowest width the stylesheet targets: `@media (max-width: 720px)` / `@container shell (min-width: 721px)`). - `.app`'s column was `auto`, so it grew to the body's min-content (up to 1080px in German, 952px in English) and clipped the docheader's right side — language switch, badges, Hugo pill. Now `minmax(0, 1fr)`. - Docheader bar, context row and heading wrap between items; buttons, toggles, crumbs and the language label never break inside a label. - Dialog button rows wrap between buttons ("Speichern und fortfahren" broke over three lines). - Standalone header: publish chip and Publish button stay on one line; the status pill wraps inside itself; the pending panel no longer hangs past the window's left edge at narrow widths. - Media details panel shrinks before the browser's toolbar does. Overflow findings German adds over English: **before 0 at 1280 / 66 at 720 → after 0 / 0**. English at 1280 is pixel-identical apart from the unsaved-changes dialog; at 720 English also stops clipping. Full vitest suite green (116 files, 2862 tests), `make test`, `make build`, `go vet`, `tsc` clean. Not covered: the page tree has no filter field (filters exist only in the link/page pickers), and the rail is icon-only, so neither had German text to fit. ## Follow-up A few form labels are generated by the engine in Go, not the site ("Address segment" on translation forms, the implicit "Title", "Backend layout"), so they stay English in a German editor. The issue kept the server out of scope; filed as #236. ## Screenshots English (after) · German before · German after. The images live on the orphan branch `screenshots/issue-226` (commit e0b0dd5), which is never merged; nothing on this branch adds them to the tree. | Screen | English | German before | German after | | --- | --- | --- | --- | | `720-page-tree` | ![](https://git.cloonar.com/Cloonar/deckle/raw/branch/screenshots/issue-226/issue-226/en-720-page-tree.png) | ![](https://git.cloonar.com/Cloonar/deckle/raw/branch/screenshots/issue-226/issue-226/de-before-720-page-tree.png) | ![](https://git.cloonar.com/Cloonar/deckle/raw/branch/screenshots/issue-226/issue-226/de-after-720-page-tree.png) | | `1280-page-tree` | ![](https://git.cloonar.com/Cloonar/deckle/raw/branch/screenshots/issue-226/issue-226/en-1280-page-tree.png) | ![](https://git.cloonar.com/Cloonar/deckle/raw/branch/screenshots/issue-226/issue-226/de-before-1280-page-tree.png) | ![](https://git.cloonar.com/Cloonar/deckle/raw/branch/screenshots/issue-226/issue-226/de-after-1280-page-tree.png) | | `720-slug-error` | ![](https://git.cloonar.com/Cloonar/deckle/raw/branch/screenshots/issue-226/issue-226/en-720-slug-error.png) | ![](https://git.cloonar.com/Cloonar/deckle/raw/branch/screenshots/issue-226/issue-226/de-before-720-slug-error.png) | ![](https://git.cloonar.com/Cloonar/deckle/raw/branch/screenshots/issue-226/issue-226/de-after-720-slug-error.png) | | `720-props-de-access` | ![](https://git.cloonar.com/Cloonar/deckle/raw/branch/screenshots/issue-226/issue-226/en-720-props-de-access.png) | ![](https://git.cloonar.com/Cloonar/deckle/raw/branch/screenshots/issue-226/issue-226/de-before-720-props-de-access.png) | ![](https://git.cloonar.com/Cloonar/deckle/raw/branch/screenshots/issue-226/issue-226/de-after-720-props-de-access.png) | | `720-element-form-error` | ![](https://git.cloonar.com/Cloonar/deckle/raw/branch/screenshots/issue-226/issue-226/en-720-element-form-error.png) | ![](https://git.cloonar.com/Cloonar/deckle/raw/branch/screenshots/issue-226/issue-226/de-before-720-element-form-error.png) | ![](https://git.cloonar.com/Cloonar/deckle/raw/branch/screenshots/issue-226/issue-226/de-after-720-element-form-error.png) | | `720-confirm-unsaved` | ![](https://git.cloonar.com/Cloonar/deckle/raw/branch/screenshots/issue-226/issue-226/en-720-confirm-unsaved.png) | ![](https://git.cloonar.com/Cloonar/deckle/raw/branch/screenshots/issue-226/issue-226/de-before-720-confirm-unsaved.png) | ![](https://git.cloonar.com/Cloonar/deckle/raw/branch/screenshots/issue-226/issue-226/de-after-720-confirm-unsaved.png) | | `1280-confirm-unsaved` | ![](https://git.cloonar.com/Cloonar/deckle/raw/branch/screenshots/issue-226/issue-226/en-1280-confirm-unsaved.png) | ![](https://git.cloonar.com/Cloonar/deckle/raw/branch/screenshots/issue-226/issue-226/de-before-1280-confirm-unsaved.png) | ![](https://git.cloonar.com/Cloonar/deckle/raw/branch/screenshots/issue-226/issue-226/de-after-1280-confirm-unsaved.png) | | `720-publish-running` | ![](https://git.cloonar.com/Cloonar/deckle/raw/branch/screenshots/issue-226/issue-226/en-720-publish-running.png) | ![](https://git.cloonar.com/Cloonar/deckle/raw/branch/screenshots/issue-226/issue-226/de-before-720-publish-running.png) | ![](https://git.cloonar.com/Cloonar/deckle/raw/branch/screenshots/issue-226/issue-226/de-after-720-publish-running.png) | | `1280-publish-running` | ![](https://git.cloonar.com/Cloonar/deckle/raw/branch/screenshots/issue-226/issue-226/en-1280-publish-running.png) | ![](https://git.cloonar.com/Cloonar/deckle/raw/branch/screenshots/issue-226/issue-226/de-before-1280-publish-running.png) | ![](https://git.cloonar.com/Cloonar/deckle/raw/branch/screenshots/issue-226/issue-226/de-after-1280-publish-running.png) | | `720-pending` | ![](https://git.cloonar.com/Cloonar/deckle/raw/branch/screenshots/issue-226/issue-226/en-720-pending.png) | ![](https://git.cloonar.com/Cloonar/deckle/raw/branch/screenshots/issue-226/issue-226/de-before-720-pending.png) | ![](https://git.cloonar.com/Cloonar/deckle/raw/branch/screenshots/issue-226/issue-226/de-after-720-pending.png) | | `1280-pending` | ![](https://git.cloonar.com/Cloonar/deckle/raw/branch/screenshots/issue-226/issue-226/en-1280-pending.png) | ![](https://git.cloonar.com/Cloonar/deckle/raw/branch/screenshots/issue-226/issue-226/de-before-1280-pending.png) | ![](https://git.cloonar.com/Cloonar/deckle/raw/branch/screenshots/issue-226/issue-226/de-after-1280-pending.png) | | `720-page-pending` | ![](https://git.cloonar.com/Cloonar/deckle/raw/branch/screenshots/issue-226/issue-226/en-720-page-pending.png) | ![](https://git.cloonar.com/Cloonar/deckle/raw/branch/screenshots/issue-226/issue-226/de-before-720-page-pending.png) | ![](https://git.cloonar.com/Cloonar/deckle/raw/branch/screenshots/issue-226/issue-226/de-after-720-page-pending.png) | | `720-media-detail` | ![](https://git.cloonar.com/Cloonar/deckle/raw/branch/screenshots/issue-226/issue-226/en-720-media-detail.png) | ![](https://git.cloonar.com/Cloonar/deckle/raw/branch/screenshots/issue-226/issue-226/de-before-720-media-detail.png) | ![](https://git.cloonar.com/Cloonar/deckle/raw/branch/screenshots/issue-226/issue-226/de-after-720-media-detail.png) | | `720-insert` | ![](https://git.cloonar.com/Cloonar/deckle/raw/branch/screenshots/issue-226/issue-226/en-720-insert.png) | ![](https://git.cloonar.com/Cloonar/deckle/raw/branch/screenshots/issue-226/issue-226/de-before-720-insert.png) | ![](https://git.cloonar.com/Cloonar/deckle/raw/branch/screenshots/issue-226/issue-226/de-after-720-insert.png) | | `1280-insert` | ![](https://git.cloonar.com/Cloonar/deckle/raw/branch/screenshots/issue-226/issue-226/en-1280-insert.png) | ![](https://git.cloonar.com/Cloonar/deckle/raw/branch/screenshots/issue-226/issue-226/de-before-1280-insert.png) | ![](https://git.cloonar.com/Cloonar/deckle/raw/branch/screenshots/issue-226/issue-226/de-after-1280-insert.png) | | `1280-error-strip` | ![](https://git.cloonar.com/Cloonar/deckle/raw/branch/screenshots/issue-226/issue-226/en-1280-error-strip.png) | ![](https://git.cloonar.com/Cloonar/deckle/raw/branch/screenshots/issue-226/issue-226/de-before-1280-error-strip.png) | ![](https://git.cloonar.com/Cloonar/deckle/raw/branch/screenshots/issue-226/issue-226/de-after-1280-error-strip.png) | ## For the German-speaking reviewer Strings the consistency pass flagged as worth a native look: `element.move.droppedContainer`, `element.move.media`, `translate.subtitle` ("aus {source}"), `translate.subpages.*`, `form.items.noSchema`. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Ship web/src/i18n/de.json: every UI key, plural forms and the server.*
namespace for every exported error code, in the impersonal register with
formal "Sie" where a sentence must address the reader. The glossary's
Terms table gains the binding German terms. The parity test now covers
de; the shell smoke test boots with Accept-Language de-AT and asserts
German chrome, a translated server error and html[lang=de]. README and
AGENTS.md say the editor ships English and German.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
fix(web): wrap the editor chrome instead of clipping longer labels
All checks were successful
ci / build-and-test (pull_request) Successful in 16m44s
ci / release (pull_request) Has been skipped
792977f3d2
German runs about a third longer than English. The shell's grid column
now stays the window's width instead of growing to the body's
min-content (which clipped the docheader's right side at 720px, in
English too). The docheader bar, context row and heading wrap between
items; buttons, toggles, crumbs and the language label keep their
label on one line; dialog button rows wrap between buttons; the
standalone header's publish chip and Publish button stay on one line
while the Hugo pill wraps inside itself; the pending panel stays inside
a narrow window; the media details panel shrinks before the browser's
toolbar does.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Attached to the pull request by commit link; removed again in the next
commit so they never land in the tree.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
docs: drop the #226 screenshots from the tree
All checks were successful
ci / build-and-test (pull_request) Successful in 16m20s
ci / release (pull_request) Has been skipped
c99adc5e0e
They stay reachable at the previous commit, which the pull request links.

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

[autoland] verdict: pass

Lander verdict: CONCERNS (non-blocking), approved but not auto-merged

CI: labctl pr checks 237 → success. ci / build-and-test passed in 16m20s; it runs npm ci && npm run build && npm test (vitest), then go vet ./..., go test ./... and go build. ci / release was skipped (it only runs on pushes to main). That covers every path this diff touches, so nothing was re-run locally.

Conventions: all met.

  • The title follows Conventional Commits.
  • The body has Closes #226.
  • The scope matches the issue: German catalog, glossary terms, CSS layout pass, and doc updates in README.md and scaffold/site/AGENTS.md.
  • The only runtime change is a new de export in web/src/i18n/index.ts, which doesn't change behaviour.
  • None of the scaffolded skills claim the editor is English-only, so they need no changes.
  • de.json has exactly the 1171 keys en.json has. The existing i18nParity.test.ts loops over catalogs, so it now checks German too.
  • The register is correct: there is no "du"/"dich"/"dein". Every "ihr" is the possessive "her/its", and "Sie" appears only in "wenden Sie sich an …" or as "she/it".

Concerns (worth a maintainer's eye before merging)

  1. Screenshot binaries end up permanently in main's history. e4dc1dc adds 48 PNGs under docs/screenshots/issue-226/ (~2.6 MB) and c99adc5 deletes them again. The tree on main is unchanged, but a merge commit (the style #228/#229 used) keeps both commits on main for good. The current pack is ~6.3 MB, so this adds roughly 40%, and nothing on main has done this before. A squash merge keeps the blobs out, but the screenshot links in this PR's body (raw/commit/e4dc1dc…) stop working once the branch is deleted and the objects are garbage-collected. A maintainer should pick deliberately: merge commit (keep the images, accept the bloat), squash (lose the image links), or move the images to PR attachments first.

  2. The issue expects a native German review, and it hasn't happened yet. #226's Decisions put the catalog in its own issue "so a German-speaking reviewer can review the translation in isolation", and the PR flags strings for a native look. The translation reads idiomatically and follows the glossary. One string drifts slightly in meaning:

    • web/src/i18n/de.json:87, element.move.droppedContainer: "Damit die Übersetzung erhalten bleibt" ("so that the translation is kept"). The English "to keep it" means the moved element.

    Also worth checking:

    • web/src/i18n/de.json:93, element.move.media
    • web/src/i18n/de.json:191, form.items.noSchema
    • web/src/i18n/de.json:1066-1067, translate.subpages.*
    • web/src/i18n/de.json:1070, translate.subtitle

Nit, not a concern: the reflowed doc comment in web/src/i18n/index.ts:26 runs past the file's usual wrap width.

[autoland] verdict: pass ## Lander verdict: CONCERNS (non-blocking), approved but not auto-merged **CI:** `labctl pr checks 237` → `success`. `ci / build-and-test` passed in 16m20s; it runs `npm ci && npm run build && npm test` (vitest), then `go vet ./...`, `go test ./...` and `go build`. `ci / release` was skipped (it only runs on pushes to main). That covers every path this diff touches, so nothing was re-run locally. **Conventions:** all met. - The title follows Conventional Commits. - The body has `Closes #226`. - The scope matches the issue: German catalog, glossary terms, CSS layout pass, and doc updates in `README.md` and `scaffold/site/AGENTS.md`. - The only runtime change is a new `de` export in `web/src/i18n/index.ts`, which doesn't change behaviour. - None of the scaffolded skills claim the editor is English-only, so they need no changes. - `de.json` has exactly the 1171 keys `en.json` has. The existing `i18nParity.test.ts` loops over `catalogs`, so it now checks German too. - The register is correct: there is no "du"/"dich"/"dein". Every "ihr" is the possessive "her/its", and "Sie" appears only in "wenden Sie sich an …" or as "she/it". ### Concerns (worth a maintainer's eye before merging) 1. **Screenshot binaries end up permanently in main's history.** `e4dc1dc` adds 48 PNGs under `docs/screenshots/issue-226/` (~2.6 MB) and `c99adc5` deletes them again. The tree on main is unchanged, but a merge commit (the style #228/#229 used) keeps both commits on main for good. The current pack is ~6.3 MB, so this adds roughly 40%, and nothing on main has done this before. A squash merge keeps the blobs out, but the screenshot links in this PR's body (`raw/commit/e4dc1dc…`) stop working once the branch is deleted and the objects are garbage-collected. A maintainer should pick deliberately: merge commit (keep the images, accept the bloat), squash (lose the image links), or move the images to PR attachments first. 2. **The issue expects a native German review, and it hasn't happened yet.** #226's Decisions put the catalog in its own issue "so a German-speaking reviewer can review the translation in isolation", and the PR flags strings for a native look. The translation reads idiomatically and follows the glossary. One string drifts slightly in meaning: - `web/src/i18n/de.json:87`, `element.move.droppedContainer`: "Damit die Übersetzung erhalten bleibt" ("so that the translation is kept"). The English "to keep it" means the moved element. Also worth checking: - `web/src/i18n/de.json:93`, `element.move.media` - `web/src/i18n/de.json:191`, `form.items.noSchema` - `web/src/i18n/de.json:1066-1067`, `translate.subpages.*` - `web/src/i18n/de.json:1070`, `translate.subtitle` Nit, not a concern: the reflowed doc comment in `web/src/i18n/index.ts:26` runs past the file's usual wrap width.
dominik.polakovics force-pushed afk/226 from c99adc5e0e
All checks were successful
ci / build-and-test (pull_request) Successful in 16m20s
ci / release (pull_request) Has been skipped
to 792977f3d2
All checks were successful
ci / build-and-test (pull_request) Successful in 16m44s
ci / release (pull_request) Has been skipped
2026-10-12 21:59:18 +02:00
Compare
Author
Owner

Resolves concern 1 of the lander verdict: the branch was rewritten to drop e4dc1dc (adds the 48 PNGs) and c99adc5 (removes them again). The tip is now 792977f, whose tree is identical to the previous head, so no code changed. The images now live on the orphan branch screenshots/issue-226 (commit e0b0dd5), which is never merged; the screenshot table in the description links there. Delete that branch once the images are no longer needed.

Resolves concern 1 of the lander verdict: the branch was rewritten to drop `e4dc1dc` (adds the 48 PNGs) and `c99adc5` (removes them again). The tip is now `792977f`, whose tree is identical to the previous head, so no code changed. The images now live on the orphan branch `screenshots/issue-226` (commit `e0b0dd5`), which is never merged; the screenshot table in the description links there. Delete that branch once the images are no longer needed.
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!237
No description provided.