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
@@ -3,6 +3,7 @@
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Content\FormRepository;
|
||||
use App\Content\Languages;
|
||||
use App\Content\MenuRepository;
|
||||
use App\Content\PageRepository;
|
||||
use App\Content\SiteSettings;
|
||||
@@ -32,6 +33,7 @@ final class FormController extends AbstractController
|
||||
private Turnstile $turnstile,
|
||||
private string $secret,
|
||||
private string $siteOrigin,
|
||||
private Languages $languages,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -56,10 +58,19 @@ final class FormController extends AbstractController
|
||||
// noch einmal.
|
||||
|| !$this->turnstile->passed($request->request->getString('cf-turnstile-response'), $request->getClientIp());
|
||||
|
||||
$checked = $this->forms->check($form, $request->request->all());
|
||||
// Abgeschickt von einer übersetzten Seite (ADR 111): Meldungen in dieser Sprache, und der
|
||||
// Weg zurück führt auf die übersetzte Seite - sofern es sie gibt.
|
||||
$language = $request->request->getString('language');
|
||||
$translatedPath = null;
|
||||
if ($this->languages->isExtra($language)) {
|
||||
$translatedPath = $this->pages->translatedPaths($page['id'])[$language] ?? null;
|
||||
}
|
||||
$language = null === $translatedPath ? null : $language;
|
||||
|
||||
$checked = $this->forms->check($form, $request->request->all(), $language);
|
||||
if (!$silentlyDropped && [] !== $checked['errors']) {
|
||||
// Re-render the page the form sits on, with the entered values and the messages.
|
||||
return $this->renderPage($request, $page, $form, $checked);
|
||||
return $this->renderPage($request, $page, $form, $checked, null === $language ? null : ($page['path'] === $this->settings->homePath() ? $language : $language.'/'.$translatedPath));
|
||||
}
|
||||
if (!$silentlyDropped) {
|
||||
$submission = $this->forms->store($form['id'], $checked['values'], $checked['consent'], $senderHash);
|
||||
@@ -69,7 +80,9 @@ final class FormController extends AbstractController
|
||||
}
|
||||
|
||||
// Post/Redirect/Get so a reload does not send the message twice.
|
||||
return $this->redirect($this->settings->publicPath($path).'?gesendet='.rawurlencode($slug).'#'.rawurlencode($request->request->getString('block')), 303);
|
||||
$back = null === $language ? $this->settings->publicPath($path) : '/'.$language.($page['path'] === $this->settings->homePath() ? '' : '/'.$translatedPath);
|
||||
|
||||
return $this->redirect($back.'?gesendet='.rawurlencode($slug).'#'.rawurlencode($request->request->getString('block')), 303);
|
||||
}
|
||||
|
||||
/** The signed timestamp makes the age of the form checkable without storing anything. */
|
||||
@@ -128,7 +141,7 @@ final class FormController extends AbstractController
|
||||
* vergass, sah eine Seite ohne Gestaltung. Jetzt geht der Weg durch dieselbe Stelle wie jeder
|
||||
* andere Aufruf, und die Formularzustaende werden als Anfrageattribut mitgereicht.
|
||||
*/
|
||||
private function renderPage(Request $request, array $page, array $form, array $checked): Response
|
||||
private function renderPage(Request $request, array $page, array $form, array $checked, ?string $translatedAddress = null): Response
|
||||
{
|
||||
// Nur das abgeschickte Formular zeigt Meldungen und behaelt seine Eingaben - wo auch immer
|
||||
// es auf der Seite steht: im Inhalt, im Kopf, im Fuss oder in einer verknuepften Sektion.
|
||||
@@ -137,7 +150,7 @@ final class FormController extends AbstractController
|
||||
|
||||
// Die Startseite antwortet unter "/" - mit ihrem eigenen Pfad wuerde sie dorthin umleiten,
|
||||
// und aus der Fehlermeldung wuerde eine Weiterleitung ohne Meldung.
|
||||
$adresse = $page['path'] === $this->settings->homePath() ? '' : (string) $page['path'];
|
||||
$adresse = $translatedAddress ?? ($page['path'] === $this->settings->homePath() ? '' : (string) $page['path']);
|
||||
$antwort = $this->forward('App\Controller\PageController::page', [
|
||||
'path' => $adresse,
|
||||
'request' => $request->duplicate(null, null, ['kant_form_state' => $zustaende, '_route' => 'page']),
|
||||
|
||||
Reference in new issue
Block a user