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
@@ -0,0 +1,155 @@
|
||||
<?php
|
||||
|
||||
namespace App\Twig;
|
||||
|
||||
use App\Content\Languages;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Twig\Extension\AbstractExtension;
|
||||
use Twig\TwigFilter;
|
||||
use Twig\TwigFunction;
|
||||
|
||||
/** Sprachangaben für die Vorlagen (ADR 111). */
|
||||
final class LanguageExtension extends AbstractExtension
|
||||
{
|
||||
/**
|
||||
* Die festen Texte der öffentlichen Vorlagen, die kein Redakteur pflegt: Sprunglink,
|
||||
* Beschriftungen von Navigationen, Einwilligung, Beitragslisten. Schlüssel ist der deutsche
|
||||
* Text. Für Sprachen ohne eigene Liste gilt Englisch - auf einer französischen Seite ist
|
||||
* „Skip to content“ näher am Besucher als „Zum Inhalt springen“.
|
||||
*/
|
||||
public const UI = [
|
||||
'en' => [
|
||||
'Zum Inhalt springen' => 'Skip to content',
|
||||
'Menü' => 'Menu',
|
||||
'Brotkrumen' => 'Breadcrumb',
|
||||
'Start' => 'Home',
|
||||
'Soziale Profile' => 'Social profiles',
|
||||
'Zum Blättern seitwärts scrollen' => 'Scroll sideways to browse',
|
||||
'Anbieter:' => 'Provider:',
|
||||
'Inhalt laden und einwilligen' => 'Load content and consent',
|
||||
'Einwilligung ändern' => 'Change consent',
|
||||
'Bildergalerie' => 'Image gallery',
|
||||
'Bild' => 'Image',
|
||||
'vergrößern' => 'enlarge',
|
||||
'Ansehen' => 'View',
|
||||
'Weiterlesen' => 'Read more',
|
||||
'Weitere Beiträge' => 'More posts',
|
||||
'Vorheriger Beitrag' => 'Previous post',
|
||||
'Nächster Beitrag' => 'Next post',
|
||||
'Alle akzeptieren' => 'Accept all',
|
||||
'Nur notwendige' => 'Necessary only',
|
||||
'Einzeln wählen' => 'Choose individually',
|
||||
'Auswahl speichern' => 'Save selection',
|
||||
'Datenschutz' => 'Privacy',
|
||||
'Impressum' => 'Legal notice',
|
||||
'Hauptnavigation' => 'Main navigation',
|
||||
'Zweite Navigation' => 'Secondary navigation',
|
||||
'Fußzeile' => 'Footer',
|
||||
'Rechtliches' => 'Legal',
|
||||
'Navigation' => 'Navigation',
|
||||
'Nach oben' => 'Back to top',
|
||||
'Sprache' => 'Language',
|
||||
// Formulare
|
||||
'Dieses Formular ist nicht mehr vorhanden.' => 'This form is no longer available.',
|
||||
'Bitte prüfe 1 Eingabe. Die betroffenen Felder sind unten gekennzeichnet.' => 'Please check 1 entry. The field concerned is marked below.',
|
||||
'Bitte prüfe %d Eingaben. Die betroffenen Felder sind unten gekennzeichnet.' => 'Please check %d entries. The fields concerned are marked below.',
|
||||
'Pflichtfeld' => 'required',
|
||||
'in' => 'in',
|
||||
'bitte wählen' => 'please choose',
|
||||
'Zum Absenden dieses Formulars wird JavaScript benötigt. Schreib uns sonst gern direkt eine E-Mail.' => 'Sending this form requires JavaScript. Otherwise, feel free to email us directly.',
|
||||
'Zusammenfassung' => 'Summary',
|
||||
'Bitte prüfen Sie Ihre Angaben. Über „Ändern“ kommen Sie zum jeweiligen Schritt zurück.' => 'Please check your details. “Change” takes you back to the step.',
|
||||
'Schritte des Formulars' => 'Form steps',
|
||||
'Schritt' => 'Step',
|
||||
'Zurück' => 'Back',
|
||||
'Weiter' => 'Next',
|
||||
'Ändern' => 'Change',
|
||||
'Ja' => 'Yes',
|
||||
'Bitte wähle mindestens eine Option.' => 'Please choose at least one option.',
|
||||
'Absenden' => 'Send',
|
||||
'Vielen Dank. Deine Nachricht ist angekommen.' => 'Thank you. Your message has arrived.',
|
||||
// Beiträge und Archive
|
||||
'Beiträge' => 'Posts',
|
||||
'Beitrag' => 'post',
|
||||
'Kategorie' => 'Category',
|
||||
'Schlagwort' => 'Tag',
|
||||
'Alle Beiträge, die neuesten zuerst.' => 'All posts, newest first.',
|
||||
'Beiträge in der Kategorie „%s“.' => 'Posts in the category “%s”.',
|
||||
'Beiträge zum Schlagwort „%s“.' => 'Posts tagged “%s”.',
|
||||
'von' => 'by',
|
||||
'← Neuere' => '← Newer',
|
||||
'Ältere →' => 'Older →',
|
||||
'Seite %d von %d' => 'Page %d of %d',
|
||||
'Externe Inhalte und Einwilligung' => 'External content and consent',
|
||||
'Diese Website lädt bestimmte Inhalte erst, wenn du zustimmst. Ohne Zustimmung wird die Seite vollständig angezeigt, nur eingebettete Inhalte anderer Anbieter bleiben leer.' => 'This website only loads certain content once you agree. Without consent the page is shown in full; only embedded content from other providers stays empty.',
|
||||
'Statistik' => 'Statistics',
|
||||
'Externe Inhalte' => 'External content',
|
||||
'Messung, wie die Website benutzt wird.' => 'Measuring how the website is used.',
|
||||
'Inhalte von anderen Anbietern, etwa Karten, Videos, Bewertungen oder ein Chat. Sie sehen die Adresse dieser Seite und deine IP-Adresse, sobald sie geladen werden.' => 'Content from other providers, such as maps, videos, reviews or a chat. They see the address of this page and your IP address as soon as they load.',
|
||||
// 404 und Einwilligung
|
||||
'Seite nicht gefunden' => 'Page not found',
|
||||
'Diese Seite ist nicht mehr verfügbar.' => 'This page is no longer available.',
|
||||
'Die gesuchte Seite ist nicht verfügbar. Über die Startseite findest du wieder zum richtigen Inhalt.' => 'The page you are looking for is not available. The home page will take you back to the right content.',
|
||||
'Die Adresse ist möglicherweise veraltet oder wurde falsch eingegeben. Über die Startseite findest du wieder zum richtigen Inhalt.' => 'The address may be out of date or mistyped. The home page will take you back to the right content.',
|
||||
'Zur Startseite' => 'To the home page',
|
||||
],
|
||||
'fr' => [
|
||||
'Zum Inhalt springen' => 'Aller au contenu',
|
||||
'Menü' => 'Menu',
|
||||
'Start' => 'Accueil',
|
||||
'Datenschutz' => 'Confidentialité',
|
||||
'Impressum' => 'Mentions légales',
|
||||
'Hauptnavigation' => 'Navigation principale',
|
||||
'Nach oben' => 'Haut de page',
|
||||
'Weiterlesen' => 'Lire la suite',
|
||||
'Alle akzeptieren' => 'Tout accepter',
|
||||
'Nur notwendige' => 'Uniquement nécessaires',
|
||||
'Sprache' => 'Langue',
|
||||
],
|
||||
'tr' => [
|
||||
'Zum Inhalt springen' => 'İçeriğe geç',
|
||||
'Menü' => 'Menü',
|
||||
'Start' => 'Ana sayfa',
|
||||
'Datenschutz' => 'Gizlilik',
|
||||
'Impressum' => 'Künye',
|
||||
'Hauptnavigation' => 'Ana menü',
|
||||
'Nach oben' => 'Yukarı',
|
||||
'Weiterlesen' => 'Devamını oku',
|
||||
'Alle akzeptieren' => 'Tümünü kabul et',
|
||||
'Nur notwendige' => 'Yalnızca gerekli',
|
||||
'Sprache' => 'Dil',
|
||||
],
|
||||
];
|
||||
|
||||
public function __construct(private RequestStack $requests)
|
||||
{
|
||||
}
|
||||
|
||||
public function getFunctions(): array
|
||||
{
|
||||
return [
|
||||
new TwigFunction('kant_og_locale', Languages::ogLocale(...)),
|
||||
];
|
||||
}
|
||||
|
||||
public function getFilters(): array
|
||||
{
|
||||
return [
|
||||
new TwigFilter('kant_t', $this->translate(...)),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Ein fester Text in der Sprache der Seite. Ohne weitere Sprache - oder in der Hauptsprache -
|
||||
* bleibt er, wie er ist; ebenso jeder Text, der nicht in der Liste steht.
|
||||
*/
|
||||
public function translate(string $text): string
|
||||
{
|
||||
$locale = $this->requests->getCurrentRequest()?->attributes->get('kant_locale');
|
||||
if (!is_string($locale) || 'de' === $locale) {
|
||||
return $text;
|
||||
}
|
||||
|
||||
return self::UI[$locale][$text] ?? self::UI['en'][$text] ?? $text;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user