Translate pages, areas, menus, forms and posts field by field instead of rebuilding them

A site can have further languages (Settings → Languages). A page is built once; each language
adds only its texts, keyed by the stable block IDs, plus its own address under /en/… (ADR 111,
document schema v27, migration 20261009000100 for page_translation).

- Editor: DE | EN switch, a list of all texts with original, state (missing, translated,
  original changed) and filter; structure stays with the main language. Undo, drafts,
  publishing and revisions cover all languages at once.
- Website: /en and translated slugs, lang, canonical, hreflang with x-default, og:locale,
  sitemap; links to translated pages follow the language; a language switch block.
- Header, footer and linked sections translate the same way; menu entries carry labels per
  language; fixed template texts come from a small dictionary.
- Forms: labels, help, options, button and success message per language; messages and the
  step script speak the language, the submission and the email stay in the main language.
- Posts, archives, category names, consent text and the 404 page follow the language.
- Single-language sites see none of it and never touch the new table.

The forms administration becomes an overview with one row per form and its own edit page;
fields are one line each with details collapsed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
kantarciandClaude Opus 5.5 committed 2026-10-09 16:56:52 +02:00
1 parent c7705b7083
commit e15b2517b1
51 files changed
+3045 -219

No files matched your search

+12 -1
View File
@@ -3,6 +3,7 @@
namespace App\Controller;
use App\Content\ButtonDesign;
use App\Content\Languages;
use App\Content\MenuButton;
use App\Content\MenuRepository;
use App\Content\PageRepository;
@@ -17,7 +18,7 @@ use Symfony\Component\Routing\Attribute\Route;
final class MenuController extends AbstractController
{
public function __construct(private MenuRepository $menus, private PageRepository $pages, private Theme $theme)
public function __construct(private MenuRepository $menus, private PageRepository $pages, private Theme $theme, private Languages $languages)
{
}
@@ -85,6 +86,7 @@ final class MenuController extends AbstractController
'slots' => MenuRepository::SLOTS, 'slot' => $slot, 'slotName' => MenuRepository::SLOTS[$slot],
'menu' => $menu, 'rows' => $rows,
'menuIcons' => \App\Content\DocumentValidator::ICONS,
'languages' => array_map(fn (string $code): array => $this->languages->describe($code), $this->languages->extra()),
// Fuer die Button-Vorschau und die Farbwahl am Eintrag: dieselben Daten wie im Editor.
'buttonSchema' => ['buttons' => $theme['buttons'], 'colors' => $theme['colors'], 'fonts' => $theme['fonts'], 'buttonOptions' => ButtonDesign::OPTIONS, 'mobile' => MenuButton::MOBILE],
]);
@@ -130,6 +132,7 @@ final class MenuController extends AbstractController
'newTab' => $item['newTab'] ?? false,
'icon' => $item['icon'] ?? '', 'iconPosition' => $item['iconPosition'] ?? 'before',
'button' => $item['button'] ?? null,
'labels' => $item['labels'] ?? [],
];
array_push($rows, ...$this->flatten($item['children'] ?? [], $level + 1));
}
@@ -160,6 +163,11 @@ final class MenuController extends AbstractController
'level' => max(1, min(MenuRepository::MAX_DEPTH, (int) ($row['level'] ?? 1))),
'position' => (int) ($row['position'] ?? 0),
'button' => MenuButton::fromForm($row),
// Je weitere Sprache ein Feld label_<sprache>; leer heißt: wie in der Hauptsprache.
'labels' => array_filter(
array_combine($this->languages->extra(), array_map(static fn (string $code): string => mb_substr(trim((string) ($row['label_'.$code] ?? '')), 0, 80), $this->languages->extra())) ?: [],
static fn (string $text): bool => '' !== $text,
),
];
}
usort($flat, static fn (array $a, array $b): int => $a['position'] <=> $b['position']);
@@ -177,6 +185,9 @@ final class MenuController extends AbstractController
if (null !== $row['button']) {
$item['button'] = $row['button'];
}
if ([] !== $row['labels']) {
$item['labels'] = $row['labels'];
}
if ('url' === $row['type']) {
$item['url'] = $row['url'];
} else {