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
@@ -7,6 +7,7 @@ use App\Content\CustomCode;
|
||||
use App\Content\DocumentValidator;
|
||||
use App\Content\FontLibrary;
|
||||
use App\Content\FormRepository;
|
||||
use App\Content\Languages;
|
||||
use App\Content\MediaLibrary;
|
||||
use App\Content\MenuRepository;
|
||||
use App\Content\NotFoundPage;
|
||||
@@ -18,6 +19,7 @@ use App\Content\SiteSettings;
|
||||
use App\Content\StyleBundle;
|
||||
use App\Content\StyleCompiler;
|
||||
use App\Content\Theme;
|
||||
use App\Content\Translations;
|
||||
use App\Content\Turnstile;
|
||||
use App\Plugin\PluginRegistry;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
@@ -27,21 +29,40 @@ use Symfony\Component\Routing\Attribute\Route;
|
||||
|
||||
final class PageController extends AbstractController
|
||||
{
|
||||
public function __construct(private PageRepository $pages, private MenuRepository $menus, private PostRepository $posts, private FormRepository $forms, private RedirectRepository $redirects, private SiteSettings $settings, private PartialRepository $partials, private CustomCode $code, private Consent $consent, private Theme $theme, private StyleCompiler $styles, private MediaLibrary $media, private FontLibrary $fonts, private StyleBundle $bundle, private Turnstile $turnstile, private string $secret, private string $siteOrigin, private NotFoundPage $notFoundPage, private PluginRegistry $plugins)
|
||||
public function __construct(private PageRepository $pages, private MenuRepository $menus, private PostRepository $posts, private FormRepository $forms, private RedirectRepository $redirects, private SiteSettings $settings, private PartialRepository $partials, private CustomCode $code, private Consent $consent, private Theme $theme, private StyleCompiler $styles, private MediaLibrary $media, private FontLibrary $fonts, private StyleBundle $bundle, private Turnstile $turnstile, private string $secret, private string $siteOrigin, private NotFoundPage $notFoundPage, private PluginRegistry $plugins, private Languages $languages)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Die Sprache dieser Anfrage, wenn es nicht die Hauptsprache ist: Kennung und die Zuordnung
|
||||
* Pfad der Hauptsprache => übersetzter Pfad (ADR 111). Null für die Hauptsprache.
|
||||
*
|
||||
* @var array{locale: string, map: array<string, string>}|null
|
||||
*/
|
||||
private ?array $translation = null;
|
||||
|
||||
/** Dieselbe Groessenangabe, die `blocks/block.html.twig` einem Bild ohne feste Breite gibt. */
|
||||
private const IMAGE_SIZES = '(max-width: 650px) 100vw, (max-width: 1024px) 50vw, 800px';
|
||||
|
||||
// A segment is a slug; up to four of them form the public path. The leading lookahead keeps
|
||||
// the CMS routes (/admin, /media, /assets) out of the catch-all, which matches before them.
|
||||
private const PATH = '(?!admin(?:/|$)|media/|assets/|kategorie/|schlagwort/|beitraege(?:/|$)|einwilligung$|feed\.xml$|sitemap\.xml$|robots\.txt$|llms\.txt$)[a-z0-9]+(?:-[a-z0-9]+)*(?:/[a-z0-9]+(?:-[a-z0-9]+)*){0,3}';
|
||||
private const PATH = '(?!admin(?:/|$)|media/|assets/|kategorie/|schlagwort/|beitraege(?:/|$)|einwilligung$|feed\.xml$|sitemap\.xml$|robots\.txt$|llms\.txt$)[a-z0-9]+(?:-[a-z0-9]+)*(?:/[a-z0-9]+(?:-[a-z0-9]+)*){0,4}';
|
||||
|
||||
#[Route('/', name: 'home', methods: ['GET'])]
|
||||
#[Route('/{path}', name: 'page', requirements: ['path' => self::PATH], methods: ['GET'])]
|
||||
public function page(Request $request, string $path = ''): Response
|
||||
{
|
||||
$this->translation = null;
|
||||
// Eine weitere Sprache steht vorn in der Adresse: /en ist ihre Startseite, /en/services
|
||||
// eine übersetzte Seite. Ohne weitere Sprache gibt es diesen Weg gar nicht.
|
||||
$segments = explode('/', $path, 2);
|
||||
if ('' !== $path && $this->languages->isExtra($segments[0])) {
|
||||
return $this->translatedPage($request, $segments[0], $segments[1] ?? '');
|
||||
}
|
||||
// Mehr als vier Ebenen gibt es nur mit einer Sprache davor.
|
||||
if (substr_count($path, '/') >= PageRepository::MAX_DEPTH) {
|
||||
return $this->notFound($request);
|
||||
}
|
||||
$home = $this->settings->homePath();
|
||||
$path = '' === $path ? $home : $path;
|
||||
// One canonical address for the root page: its own path redirects to "/".
|
||||
@@ -71,6 +92,134 @@ final class PageController extends AbstractController
|
||||
return $this->renderPage($request, $page, $path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Eine Seite in einer weiteren Sprache. Gefunden wird sie über ihre übersetzte Adresse; ihre
|
||||
* Startseite antwortet unter /en und nicht zusätzlich unter /en/home.
|
||||
*/
|
||||
private function translatedPage(Request $request, string $locale, string $rest): Response
|
||||
{
|
||||
$map = $this->pages->translationMap($locale);
|
||||
// Schon hier: auch eine 404 unter /en/ antwortet auf Englisch.
|
||||
$this->translation = ['locale' => $locale, 'map' => $map];
|
||||
$homeTranslated = $map[$this->settings->homePath()] ?? null;
|
||||
if ('' !== $rest && $rest === $homeTranslated && null === $request->attributes->get('kant_form_state')) {
|
||||
return $this->shell($this->redirect('/'.$locale, 301));
|
||||
}
|
||||
// Die Archive gibt es in jeder Sprache unter derselben Adresse mit vorangestellter Sprache.
|
||||
if (1 === preg_match('#^(beitraege|kategorie/([a-z0-9-]{1,80})|schlagwort/([a-z0-9-]{1,80}))$#D', $rest, $archiv)) {
|
||||
$this->translation = ['locale' => $locale, 'map' => $map];
|
||||
|
||||
return $this->renderArchive($request, str_starts_with($rest, 'kategorie/') ? 'category' : 'tag', '' !== ($archiv[2] ?? '') ? $archiv[2] : (($archiv[3] ?? '') ?: null));
|
||||
}
|
||||
$target = '' === $rest ? $homeTranslated : $rest;
|
||||
$page = null === $target ? null : $this->pages->findPublishedTranslation($target, $locale);
|
||||
if (null === $page) {
|
||||
$redirect = '' === $rest ? null : $this->redirects->find($locale.'/'.$rest);
|
||||
if (null !== $redirect && 301 === $redirect['status']) {
|
||||
return $this->shell($this->redirect('/'.$redirect['to'], 301));
|
||||
}
|
||||
|
||||
return $this->notFound($request, null !== $redirect ? 410 : 404);
|
||||
}
|
||||
if ($page['id'] === $this->settings->load()['notFoundPageId']) {
|
||||
return $this->notFound($request);
|
||||
}
|
||||
|
||||
return $this->renderPage($request, $page, $page['path']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Kopf, Fuß und Sektionen in der Sprache dieser Anfrage: übersetzt, wo es Übersetzungen gibt,
|
||||
* und ohne die Übersetzungen selbst in der Hauptsprache.
|
||||
*/
|
||||
private function localise(array $document): array
|
||||
{
|
||||
return null === $this->translation ? Translations::strip($document) : Translations::apply($document, $this->translation['locale']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Die öffentliche Adresse eines Pfades der Hauptsprache in der Sprache dieser Anfrage - oder
|
||||
* unverändert, wenn die Seite dort nicht übersetzt ist.
|
||||
*/
|
||||
private function localHref(string $href): string
|
||||
{
|
||||
if (null === $this->translation || !str_starts_with($href, '/') || str_starts_with($href, '//')) {
|
||||
return $href;
|
||||
}
|
||||
$suffix = '';
|
||||
if (false !== ($cut = strcspn($href, '?#')) && $cut < strlen($href)) {
|
||||
$suffix = substr($href, $cut);
|
||||
$href = substr($href, 0, $cut);
|
||||
}
|
||||
$path = trim($href, '/');
|
||||
$home = $this->settings->homePath();
|
||||
$locale = $this->translation['locale'];
|
||||
if ('' === $path || $path === $home) {
|
||||
return isset($this->translation['map'][$home]) ? '/'.$locale.$suffix : $href.$suffix;
|
||||
}
|
||||
if ('beitraege' === $path || str_starts_with($path, 'kategorie/') || str_starts_with($path, 'schlagwort/')) {
|
||||
return '/'.$locale.'/'.$path.$suffix;
|
||||
}
|
||||
|
||||
return isset($this->translation['map'][$path]) ? '/'.$locale.'/'.$this->translation['map'][$path].$suffix : $href.$suffix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Auf einer übersetzten Seite zeigen Links auf Seiten, die es in dieser Sprache gibt, auf
|
||||
* deren übersetzte Fassung - in Buttons, Menüs, Fließtext und Brotkrumen gleichermaßen.
|
||||
* Ein Link auf eine nicht übersetzte Seite bleibt, wie er ist. Ausgenommen sind die Links
|
||||
* der Sprachwahl, die mit hreflang bewusst auf eine bestimmte Sprache zeigen.
|
||||
*/
|
||||
private function localiseLinks(string $html): string
|
||||
{
|
||||
if (null === $this->translation) {
|
||||
return $html;
|
||||
}
|
||||
|
||||
return preg_replace_callback('/<a\b[^>]*>/i', function (array $tag): string {
|
||||
if (str_contains($tag[0], 'hreflang=')) {
|
||||
return $tag[0];
|
||||
}
|
||||
|
||||
return preg_replace_callback('/\bhref="(\/[^"]*)"/', fn (array $href): string => 'href="'.$this->localHref($href[1]).'"', $tag[0]) ?? $tag[0];
|
||||
}, $html) ?? $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unter welchen Adressen diese Seite in welcher Sprache steht - für hreflang und die
|
||||
* Sprachwahl. Leer, solange die Website nur eine Sprache hat.
|
||||
*
|
||||
* @return list<array{locale: string, native: string, href: string, url: string, current: bool}>
|
||||
*/
|
||||
private function alternates(array $page, int $status): array
|
||||
{
|
||||
$extra = $this->languages->extra();
|
||||
if ([] === $extra || $status >= 400 || !isset($page['id']) || 'archive' === ($page['type'] ?? null)) {
|
||||
return [];
|
||||
}
|
||||
$origin = rtrim($this->siteOrigin, '/');
|
||||
$home = $this->settings->homePath();
|
||||
$current = $this->translation['locale'] ?? $this->languages->main();
|
||||
$main = $this->languages->main();
|
||||
$list = [['locale' => $main, 'href' => $this->settings->publicPath($page['path'], $home)]];
|
||||
$translated = $this->pages->translatedPaths($page['id']);
|
||||
foreach ($extra as $locale) {
|
||||
if (isset($translated[$locale])) {
|
||||
$list[] = ['locale' => $locale, 'href' => $page['path'] === $home ? '/'.$locale : '/'.$locale.'/'.$translated[$locale]];
|
||||
}
|
||||
}
|
||||
if (count($list) < 2) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return array_map(fn (array $entry): array => [
|
||||
...$entry,
|
||||
'native' => $this->languages->describe($entry['locale'])['native'],
|
||||
'url' => $origin.$entry['href'],
|
||||
'current' => $entry['locale'] === $current,
|
||||
], $list);
|
||||
}
|
||||
|
||||
/**
|
||||
* Eine eigene Adresse eines Plugins: die Vorlage bekommt die Einstellungen des Plugins und die
|
||||
* Abfrageparameter, sonst nichts. Dieselben Schutzkopfzeilen wie jede Seite, kein Skript.
|
||||
@@ -94,7 +243,45 @@ final class PageController extends AbstractController
|
||||
|
||||
public function notFound(Request $request, int $status = 404): Response
|
||||
{
|
||||
return $this->renderPage($request, $this->notFoundPage->page($status), '', $status);
|
||||
$page = $this->notFoundPage->page($status);
|
||||
$locale = $this->translation['locale'] ?? null;
|
||||
// Die eigene 404-Seite ist eine Seite wie jede andere und wird im Editor übersetzt; die
|
||||
// mitgelieferte bekommt ihre Texte aus dem Wörterbuch der festen Texte (ADR 111).
|
||||
if (null !== $locale && in_array($locale, Translations::locales($page['document']), true)) {
|
||||
$page['document'] = Translations::apply($page['document'], $locale);
|
||||
} else {
|
||||
$page['document'] = Translations::strip($page['document']);
|
||||
if (null !== $locale && str_starts_with((string) $page['id'], 'error-')) {
|
||||
$page['document'] = $this->translateFixedTexts($page['document'], $locale);
|
||||
}
|
||||
}
|
||||
if (null !== $locale) {
|
||||
$page['locale'] = $locale;
|
||||
}
|
||||
|
||||
return $this->renderPage($request, $page, '', $status);
|
||||
}
|
||||
|
||||
/** Titel, Beschreibung und Texte eines mitgelieferten Dokuments über das Wörterbuch. */
|
||||
private function translateFixedTexts(array $document, string $locale): array
|
||||
{
|
||||
$t = static fn (string $text): string => \App\Twig\LanguageExtension::UI[$locale][$text] ?? \App\Twig\LanguageExtension::UI['en'][$text] ?? $text;
|
||||
foreach (['title', 'description'] as $field) {
|
||||
$document[$field] = $t((string) ($document[$field] ?? ''));
|
||||
}
|
||||
$walk = function (array $blocks) use (&$walk, $t): array {
|
||||
foreach ($blocks as $index => $block) {
|
||||
if (is_string($block['props']['text'] ?? null)) {
|
||||
$blocks[$index]['props']['text'] = $t($block['props']['text']);
|
||||
}
|
||||
$blocks[$index]['children'] = $walk($block['children'] ?? []);
|
||||
}
|
||||
|
||||
return $blocks;
|
||||
};
|
||||
$document['blocks'] = $walk($document['blocks'] ?? []);
|
||||
|
||||
return $document;
|
||||
}
|
||||
|
||||
/** Published pages and error content share theme, areas, block rendering and CSP. */
|
||||
@@ -102,10 +289,12 @@ final class PageController extends AbstractController
|
||||
{
|
||||
$styles = '';
|
||||
// The page decides which header and footer it gets - including none.
|
||||
$areas = $this->areas($status >= 400 ? null : $page);
|
||||
$areas = array_map($this->localise(...), $this->areas($status >= 400 ? null : $page));
|
||||
$theme = $this->theme->load();
|
||||
// Theme first, block styles after, so a block always wins over the site defaults.
|
||||
$sections = $this->sections($page['document'], $areas);
|
||||
$sections = array_map($this->localise(...), $this->sections($page['document'], $areas));
|
||||
// Die festen Texte der Vorlagen folgen der Sprache der Seite (LanguageExtension).
|
||||
$request->attributes->set('kant_locale', $this->translation['locale'] ?? $this->languages->main());
|
||||
// Dieselbe Sektion zweimal auf der Seite: ab der zweiten Verknüpfung eine Kopie mit eigenen
|
||||
// Kennungen, in Renderreihenfolge Kopf, Inhalt, Fuß.
|
||||
$pristine = $sections;
|
||||
@@ -176,20 +365,25 @@ final class PageController extends AbstractController
|
||||
break;
|
||||
}
|
||||
}
|
||||
$adresse = $status >= 400 ? null : rtrim($this->siteOrigin, '/').$this->settings->publicPath($path, $this->settings->homePath());
|
||||
$adresse = $status >= 400 ? null : rtrim($this->siteOrigin, '/').$this->localHref($this->settings->publicPath($path, $this->settings->homePath()));
|
||||
$alternates = $this->alternates($page, $status);
|
||||
// Die Sprachwahl ist ein Baustein und kann im Kopf, im Fuß oder im Inhalt stehen; die
|
||||
// Vorlage holt die Liste von hier, statt sie durch jede Einbindung zu reichen.
|
||||
$request->attributes->set('kant_alternates', $alternates);
|
||||
$canonical = null === $adresse ? null : $adresse.(null !== $blaettern && $blaettern['page'] > 1 ? '?seite='.$blaettern['page'] : '');
|
||||
$response = $this->render('page.html.twig', [
|
||||
'page' => $page,
|
||||
'canonical' => $canonical,
|
||||
'alternates' => $alternates,
|
||||
'pagination' => null === $blaettern || null === $adresse ? null : ['page' => $blaettern['page'], 'pages' => $blaettern['pages'], 'base' => $adresse],
|
||||
'navigation' => $this->navigation($path),
|
||||
'postLists' => $postLists,
|
||||
// Ohne Beiträge gibt es keinen Feed, auf den der Kopf verweisen könnte.
|
||||
'hasPosts' => $this->posts->anyPublished(),
|
||||
'postMeta' => 'post' === $page['type'] ? $this->posts->termsFor($page['id']) : null,
|
||||
'postMeta' => 'post' === $page['type'] ? $this->posts->termsFor($page['id'], $this->translation['locale'] ?? null) : null,
|
||||
// Die Nachbarn kosten eine Abfrage; sie fällt nur an, wenn die Seite den
|
||||
// Baustein wirklich enthält.
|
||||
'neighbours' => 'post' === $page['type'] && $this->hasType($page['document'], 'postnav') ? $this->posts->neighbours($page['id']) : [],
|
||||
'neighbours' => 'post' === $page['type'] && $this->hasType($page['document'], 'postnav') ? $this->posts->neighbours($page['id'], language: $this->translation['locale'] ?? null) : [],
|
||||
'needsTabs' => $needsTabs,
|
||||
'needsFormScript' => $needsFormScript,
|
||||
'pluginStyles' => $pluginAssets['styles'],
|
||||
@@ -244,6 +438,9 @@ final class PageController extends AbstractController
|
||||
if ($status >= 400) {
|
||||
$response->headers->set('X-Robots-Tag', 'noindex, follow');
|
||||
}
|
||||
if (null !== $this->translation) {
|
||||
$response->setContent($this->localiseLinks((string) $response->getContent()));
|
||||
}
|
||||
|
||||
return $this->shell($response, $wantsScript, $styles, $frames, $needsDocument);
|
||||
}
|
||||
@@ -317,6 +514,10 @@ final class PageController extends AbstractController
|
||||
if ('form' === ($block['type'] ?? null)) {
|
||||
$slug = $block['props']['form'] ?? '';
|
||||
$form = $this->forms->findBySlug($slug);
|
||||
// In einer weiteren Sprache mit übersetzten Beschriftungen (ADR 111).
|
||||
if (null !== $form && null !== $this->translation) {
|
||||
$form = FormRepository::localise($form, $this->translation['locale']);
|
||||
}
|
||||
$states[$block['id']] = [
|
||||
'form' => $form,
|
||||
// Die Bilder einer Bildauswahl, einmal fuer alle Optionen aller Felder geholt.
|
||||
@@ -420,9 +621,12 @@ final class PageController extends AbstractController
|
||||
$prefix = '';
|
||||
foreach (explode('/', (string) $page['path']) as $segment) {
|
||||
$prefix = '' === $prefix ? $segment : $prefix.'/'.$segment;
|
||||
$ancestor = $this->pages->findPublished($prefix);
|
||||
// In einer weiteren Sprache heißt jede Stufe so, wie sie dort heißt - sofern es sie
|
||||
// dort gibt. Sonst bleibt sie in der Hauptsprache stehen, mit ihrer Adresse.
|
||||
$translated = null === $this->translation || !isset($this->translation['map'][$prefix]) ? null : $this->pages->findPublishedTranslation($this->translation['map'][$prefix], $this->translation['locale']);
|
||||
$ancestor = $translated ?? $this->pages->findPublished($prefix);
|
||||
if (null !== $ancestor) {
|
||||
$trail[] = ['name' => $ancestor['document']['title'] ?? $prefix, 'href' => '/'.$prefix];
|
||||
$trail[] = ['name' => $ancestor['document']['title'] ?? $prefix, 'href' => $this->localHref('/'.$prefix)];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -505,7 +709,7 @@ final class PageController extends AbstractController
|
||||
'@context' => 'https://schema.org',
|
||||
'@type' => 'Article',
|
||||
'headline' => mb_substr((string) ($page['document']['title'] ?? ''), 0, 110),
|
||||
'mainEntityOfPage' => $origin.'/'.$page['path'],
|
||||
'mainEntityOfPage' => $origin.$this->localHref('/'.$page['path']),
|
||||
];
|
||||
foreach ([['description', $page['excerpt'] ?? $page['document']['description'] ?? ''], ['datePublished', $page['published_at'] ?? '']] as [$key, $value]) {
|
||||
if ('' !== (string) $value && null !== $value) {
|
||||
@@ -618,23 +822,32 @@ final class PageController extends AbstractController
|
||||
#[Route('/schlagwort/{slug}', name: 'tag', requirements: ['slug' => '[a-z0-9-]{1,80}'], methods: ['GET'])]
|
||||
public function archive(Request $request, ?string $slug = null): Response
|
||||
{
|
||||
$kind = 'category' === $request->attributes->get('_route') ? 'category' : 'tag';
|
||||
$this->translation = null;
|
||||
|
||||
return $this->renderArchive($request, 'category' === $request->attributes->get('_route') ? 'category' : 'tag', $slug);
|
||||
}
|
||||
|
||||
private function renderArchive(Request $request, string $kind, ?string $slug): Response
|
||||
{
|
||||
$language = $this->translation['locale'] ?? null;
|
||||
$t = static fn (string $text): string => null === $language ? $text : (\App\Twig\LanguageExtension::UI[$language][$text] ?? \App\Twig\LanguageExtension::UI['en'][$text] ?? $text);
|
||||
$term = null;
|
||||
if (null !== $slug) {
|
||||
$term = $this->posts->findTerm($kind, $slug);
|
||||
if (null === $term) {
|
||||
return $this->notFound($request);
|
||||
}
|
||||
$term['name'] = $this->posts->termName($term, $language);
|
||||
}
|
||||
$page = max(1, $request->query->getInt('seite', 1));
|
||||
$result = $this->posts->published($page, $term['id'] ?? null);
|
||||
$result = $this->posts->published($page, $term['id'] ?? null, language: $language);
|
||||
// Ein Archiv ohne Beitrag ist kein Inhalt, sondern eine Baustelle mit eigener Adresse -
|
||||
// egal ob die Website noch gar keinen Blog hat oder ob diese eine Kategorie leer ist.
|
||||
if (0 === $result['total']) {
|
||||
return $this->notFound($request);
|
||||
}
|
||||
$base = null === $term ? '/beitraege' : '/'.PostRepository::ARCHIVE_PREFIX[$kind].'/'.$term['slug'];
|
||||
$heading = null === $term ? 'Beiträge' : $term['name'];
|
||||
$heading = null === $term ? $t('Beiträge') : $term['name'];
|
||||
|
||||
// Das Archiv ist eine Seite wie jede andere: ein Dokument aus Bausteinen, das durch dieselbe
|
||||
// Aufbaukette läuft - Kopf und Fuß mit ihren Stilen, verknüpfte Sektionen, Schriften,
|
||||
@@ -642,15 +855,15 @@ final class PageController extends AbstractController
|
||||
// eigene Vorlage, die von alledem nur die Palette kannte.
|
||||
$kopf = [
|
||||
['id' => 'archiv-titel', 'type' => 'heading', 'props' => ['text' => $heading, 'level' => 1]],
|
||||
['id' => 'archiv-zahl', 'type' => 'text', 'props' => ['text' => sprintf('%d %s', $result['total'], 1 === $result['total'] ? 'Beitrag' : 'Beiträge')]],
|
||||
['id' => 'archiv-zahl', 'type' => 'text', 'props' => ['text' => sprintf('%d %s', $result['total'], 1 === $result['total'] ? $t('Beitrag') : $t('Beiträge'))]],
|
||||
];
|
||||
if (null !== $term) {
|
||||
array_unshift($kopf, ['id' => 'archiv-art', 'type' => 'text', 'props' => ['text' => 'category' === $kind ? 'Kategorie' : 'Schlagwort'], 'style' => ['desktop' => ['fontSize' => '13px', 'fontWeight' => '700', 'letterSpacing' => '0.08em', 'textTransform' => 'uppercase', 'margin' => ['bottom' => '8px']]]]);
|
||||
array_unshift($kopf, ['id' => 'archiv-art', 'type' => 'text', 'props' => ['text' => 'category' === $kind ? $t('Kategorie') : $t('Schlagwort')], 'style' => ['desktop' => ['fontSize' => '13px', 'fontWeight' => '700', 'letterSpacing' => '0.08em', 'textTransform' => 'uppercase', 'margin' => ['bottom' => '8px']]]]);
|
||||
}
|
||||
$document = [
|
||||
'schemaVersion' => DocumentValidator::SCHEMA_VERSION,
|
||||
'title' => $heading,
|
||||
'description' => null === $term ? 'Alle Beiträge, die neuesten zuerst.' : sprintf('Beiträge %s „%s“.', 'category' === $kind ? 'in der Kategorie' : 'zum Schlagwort', $term['name']),
|
||||
'description' => null === $term ? $t('Alle Beiträge, die neuesten zuerst.') : sprintf($t('category' === $kind ? 'Beiträge in der Kategorie „%s“.' : 'Beiträge zum Schlagwort „%s“.'), $term['name']),
|
||||
'blocks' => [
|
||||
['id' => 'archiv-kopf', 'type' => 'section', 'layout' => ['tone' => 'mint', 'width' => 'normal'], 'children' => $kopf],
|
||||
['id' => 'archiv', 'type' => 'section', 'layout' => ['tone' => 'paper', 'width' => 'normal'], 'children' => [
|
||||
@@ -659,7 +872,7 @@ final class PageController extends AbstractController
|
||||
],
|
||||
];
|
||||
$seite = [
|
||||
'id' => 'archiv-'.(null === $term ? 'beitraege' : $kind.'-'.$term['slug']), 'type' => 'archive', 'path' => ltrim($base, '/'), 'locale' => 'de',
|
||||
'id' => 'archiv-'.(null === $term ? 'beitraege' : $kind.'-'.$term['slug']), 'type' => 'archive', 'path' => ltrim($base, '/'), 'locale' => $language ?? $this->languages->main(),
|
||||
'website_name' => '', 'seo_title' => '', 'robots' => 'index', 'published_at' => null, 'author' => null, 'document' => $document,
|
||||
];
|
||||
|
||||
@@ -705,7 +918,7 @@ final class PageController extends AbstractController
|
||||
// immer die erste - sonst würden zwei Listen auf einer Seite einander
|
||||
// umblättern.
|
||||
$paginate = (bool) ($props['paginate'] ?? false);
|
||||
$result = $this->posts->published($paginate ? $page : 1, $term['id'] ?? null, $props['count']);
|
||||
$result = $this->posts->published($paginate ? $page : 1, $term['id'] ?? null, $props['count'], language: $this->translation['locale'] ?? null);
|
||||
$lists[$block['id']] = $paginate ? $result : $result['posts'];
|
||||
}
|
||||
$walk($block['children'] ?? []);
|
||||
@@ -986,7 +1199,7 @@ final class PageController extends AbstractController
|
||||
{
|
||||
$links = [];
|
||||
foreach ($this->menus->all() as $slot => $menu) {
|
||||
$links[$slot] = $this->menus->resolve($menu['items'], $currentPath);
|
||||
$links[$slot] = $this->menus->resolve($menu['items'], $currentPath, language: $this->translation['locale'] ?? null);
|
||||
}
|
||||
|
||||
return $links;
|
||||
|
||||
Reference in new issue
Block a user