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:
1 parent
c7705b7083
commit
e15b2517b1
51 files changed
+3045
-219
No files matched your search
@@ -6,10 +6,12 @@ use App\Content\CustomCode;
|
||||
use App\Content\DocumentValidator;
|
||||
use App\Content\FontLibrary;
|
||||
use App\Content\FormRepository;
|
||||
use App\Content\Languages;
|
||||
use App\Content\MenuRepository;
|
||||
use App\Content\PartialRepository;
|
||||
use App\Content\StyleCompiler;
|
||||
use App\Content\Theme;
|
||||
use App\Content\Translations;
|
||||
use App\Domain\InvalidDocument;
|
||||
use App\Domain\InvalidPage;
|
||||
use App\Domain\StaleWrite;
|
||||
@@ -42,6 +44,7 @@ final class PartialController extends AbstractController
|
||||
private CsrfTokenManagerInterface $csrf,
|
||||
private PluginRegistry $plugins,
|
||||
private FormRepository $forms,
|
||||
private Languages $languages,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -125,8 +128,8 @@ final class PartialController extends AbstractController
|
||||
if (!$this->isCsrfTokenValid('editor', $request->headers->get('X-CSRF-Token'))) {
|
||||
return $this->json(['error' => 'Deine Sitzung ist abgelaufen. Bitte melde dich erneut an.'], 403);
|
||||
}
|
||||
if (strlen($request->getContent()) > 270000) {
|
||||
return $this->json(['error' => 'Der Bereich ist zu groß (maximal 256 KiB Inhalt).'], 413);
|
||||
if (strlen($request->getContent()) > 1100000) {
|
||||
return $this->json(['error' => 'Der Bereich ist zu groß (maximal 256 KiB Inhalt, mit Übersetzungen 1 MiB).'], 413);
|
||||
}
|
||||
$partial = $this->partial($id);
|
||||
try {
|
||||
@@ -137,8 +140,9 @@ final class PartialController extends AbstractController
|
||||
}
|
||||
if ('admin_partial_preview' === $request->attributes->get('_route')) {
|
||||
$this->validator->validate($document, false, true);
|
||||
$language = $body['language'] ?? null;
|
||||
|
||||
return $this->renderPreview($partial, $document);
|
||||
return $this->renderPreview($partial, $document, $request, is_string($language) && $this->languages->isExtra($language) ? $language : null);
|
||||
}
|
||||
if (!isset($body['expectedVersion']) || !is_int($body['expectedVersion'])) {
|
||||
throw new \InvalidArgumentException();
|
||||
@@ -163,18 +167,25 @@ final class PartialController extends AbstractController
|
||||
}
|
||||
|
||||
/** The area on its own, in the shell of a page, so the editor preview looks like the result. */
|
||||
private function renderPreview(array $partial, array $document): Response
|
||||
private function renderPreview(array $partial, array $document, Request $request, ?string $language = null): Response
|
||||
{
|
||||
// Ein Bereich in einer weiteren Sprache (ADR 111): seine Texte, die seiner Sektionen und
|
||||
// die Menübeschriftungen in dieser Sprache - wie auf einer übersetzten Seite.
|
||||
$localise = static fn (array $doc): array => null === $language ? Translations::strip($doc) : Translations::apply($doc, $language);
|
||||
$document = $localise($document);
|
||||
$locale = $language ?? $this->languages->main();
|
||||
$request->attributes->set('kant_locale', $locale);
|
||||
$request->attributes->set('kant_alternates', array_map(fn (string $code): array => [...$this->languages->describe($code), 'locale' => $code, 'href' => '#', 'current' => $code === $locale], $this->languages->all()));
|
||||
$theme = $this->theme->load();
|
||||
$navigation = [];
|
||||
foreach ($this->menus->all() as $slot => $menu) {
|
||||
$navigation[$slot] = $this->menus->resolve($menu['items'], '');
|
||||
$navigation[$slot] = $this->menus->resolve($menu['items'], '', language: $language);
|
||||
}
|
||||
$sections = [];
|
||||
foreach ($this->partials->referencedSlugs($document) as $slug) {
|
||||
$section = $this->partials->resolveSection($slug);
|
||||
if (null !== $section) {
|
||||
$sections[$slug] = $section['document'];
|
||||
$sections[$slug] = $localise($section['document']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,10 +195,13 @@ final class PartialController extends AbstractController
|
||||
$styles .= $this->styles->compileDocument($section).$this->code->compileDocument($section);
|
||||
}
|
||||
$forms = [];
|
||||
$walk = function (array $blocks) use (&$walk, &$forms): void {
|
||||
$walk = function (array $blocks) use (&$walk, &$forms, $language): void {
|
||||
foreach ($blocks as $block) {
|
||||
if ('form' === ($block['type'] ?? null) && '' !== ($block['props']['form'] ?? '')) {
|
||||
$form = $this->forms->findBySlug((string) $block['props']['form']);
|
||||
if (null !== $form && null !== $language) {
|
||||
$form = FormRepository::localise($form, $language);
|
||||
}
|
||||
$forms[$block['id']] = ['form' => $form, 'sent' => false, 'errors' => [], 'values' => [], 'optionImages' => [], 'blockId' => $block['id'], 'page' => ['path' => ''], 'issued' => 0, 'issuedSignature' => '', 'turnstileKey' => ''];
|
||||
if (null !== $form) {
|
||||
$styles .= $this->forms->css($form, $theme);
|
||||
@@ -202,7 +216,7 @@ final class PartialController extends AbstractController
|
||||
}
|
||||
|
||||
return $this->render('page.html.twig', [
|
||||
'page' => ['locale' => 'de', 'path' => '', 'type' => 'page', 'slug' => '', 'document' => $document],
|
||||
'page' => ['locale' => $locale, 'path' => '', 'type' => 'page', 'slug' => '', 'document' => $document],
|
||||
'canonical' => '',
|
||||
'editing' => true,
|
||||
'navigation' => $navigation,
|
||||
@@ -232,6 +246,12 @@ final class PartialController extends AbstractController
|
||||
'csrfToken' => $this->csrf->getToken('editor')->getValue(),
|
||||
'plugins' => $this->plugins->forEditor(),
|
||||
'presets' => $this->plugins->presetsForEditor(),
|
||||
// Kopf, Fuß und Sektionen werden wie Seiten übersetzt, nur ohne Adresse und Suchtitel.
|
||||
'languages' => [
|
||||
'main' => $this->languages->describe($this->languages->main()),
|
||||
'extra' => array_map(fn (string $code): array => $this->languages->describe($code), $this->languages->extra()),
|
||||
'area' => true,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user