Kompetenzen Portfolio Referenzen Lebenslauf Blog Termin buchen

TYPO3 + Symfony = Übersetzungen

Gepostet von: Felix Dziekan in: Blog am 

In diesem Beitrag erkläre ich, wie du TYPO3 als Quelle für deine Symfony-Übersetzungen nutzt. Damit hast du Übersetzungen für dein Backend, ohne lokale Sprachdateien pflegen zu müssen.

Das erfordert am Anfang etwas mehr Einrichtung, aber Übersetzungen in einem CMS zu verwalten spart dir langfristig Zeit, Geld und Kopfschmerzen! Am Ende sind Übersetzungen genau das: Inhalt!

Das Beste daran, TYPO3 für Übersetzungen zu nutzen: Du kannst sie über mehrere Projekte, verschiedene Stages oder sogar verschiedene Apps, Frontends und Backends hinweg wiederverwenden und teilen, ohne irgendetwas zu duplizieren. 

In meinem anderen Blogpost habe ich bereits erklärt, wie sich TYPO3 als Quelle für react-i18next-Sprachdateien nutzen lässt, um eine App oder Website zu übersetzen. Wir nutzen dasselbe Setup, um auch unser Backend zu übersetzen. 

Warum sollten wir das tun?

Ein gutes Beispiel, warum wir dieselben Übersetzungen im Frontend und im Backend brauchen, sind die Lebenslauf-Vorlagen auf https://bewerbungshelferlein.de/ oder die Rechnungsvorlagen auf https://freelancingtoolkit.com/. Diese Vorlagen sind reines HTML/CSS und werden im Backend von Symfony über Twig gerendert. Sie brauchen aber dieselbe Wortwahl wie im Frontend.

Wichtig: Wenn ich von Übersetzungen spreche, meine ich weder nutzergenerierte Inhalte noch irgendetwas, das in der Datenbank liegt. Um Doctrine-Entities zu übersetzen, solltest du einen anderen Ansatz wählen.

Selbst für meine eigenen Projekte, in denen ich der Entwickler bin, ist das ein enormer Zeitgewinn. Ich muss zum Beispiel nie ein Deployment machen oder Zugriff auf den Code haben, nur um einen Rechtschreibfehler zu korrigieren oder eine Formulierung zu ändern. Und jetzt stell dir vor, wie glücklich deine Stakeholder oder Kunden sind, wenn sie nie wieder einen Entwickler brauchen, um die Sprache zu ändern oder Tippfehler zu beheben. Das spart eine Menge Zeit und Geld!

Grundsetup

Fangen wir mit einer kurzen Wiederholung an, wie das Setup funktioniert und aufgebaut ist. Ich habe TYPO3 mit der TYPO3-Headless-Extension aufgesetzt. Die gibt mir eine URL wie diese:

 

{{baseURL}}/translation/[language]

 

Diese URL liefert mir eine JSON-Sprachdatei. Ich kann diese Datei bequem über das TYPO3-Backend pflegen und aktualisieren. Um eine Vorstellung davon zu bekommen, wie das aussieht, kannst du dir die Übersetzungen hier für die deutsche Version und hier für die englische ansehen.

Um neue Übersetzungen für neue Features anzulegen, brauchst du allerdings weiterhin einen Entwickler, der die Übersetzungs-Platzhalter im Code ergänzt.

Und für etwas mehr Details hier ein kleiner Screenshot, wie das Backend in TYPO3 aussieht. Wenn du mehr darüber wissen willst, wie das Setup aufgebaut ist, schau dir den Blogpost über react-i18next und TYPO3 an.

Typo3 Translation Backend

Symfony auf die TYPO3-Übersetzungsdateien vorbereiten

Zeit für die Praxis — machen wir uns die Hände schmutzig. Es gibt drei große Schritte, damit das Ganze in Symfony funktioniert:

  1. die nötigen Pakete installieren und konfigurieren
  2. den eigenen Translation Provider anlegen
  3. das Setup im Backend und in einem Twig-Template nutzen

1. Installation und Konfiguration

Bevor wir anfangen, müssen wir das Symfony-Translation-Bundle installieren und konfigurieren. Das ist die Basis für alles Weitere. Die Konfiguration, die ich nutze, ist ziemlich geradlinig, und es gibt eine sehr gute Dokumentation auf der Symfony-Website, deshalb erkläre ich sie hier nicht.

Installieren:

 

composer require symfony/translation

 

Konfigurieren:

 

framework:
    default_locale: en
    translator:
        default_path: '%kernel.project_dir%/translations'
        fallbacks:
            - en

 

Übersetzungsdateien:

Ich bin mir nicht hundertprozentig sicher, ob das an meiner Konfiguration liegt, an meiner Programmierung oder ob es ein Bug ist, aber es bleibt eine wichtige Sache zu tun, damit das funktioniert. Es klingt auch sehr unlogisch, weil die Übersetzungen über das Web geladen werden, aber wir müssen die Übersetzungsdateien trotzdem in unserem Ordner '%kernel.project_dir%/translations' anlegen (falls du den Ordner nicht hast, leg ihn an). Wir brauchen eine Übersetzungsdatei pro Sprache. Sie dürfen sogar leer sein, aber sie müssen existieren. 

 

touch ./translations/messages.de.web

 

2. Den eigenen Loader-Service anlegen

Der nächste Schritt ist, einen Service anzulegen, der die Übersetzungen aus unserer Web-Ressource lädt und speichert. Da ich ein wiederverwendbares Bundle gebaut habe, das ich in allen meinen Projekten nutze, heißt mein Service:

 

serie3HelperBundle.TranslationWebLoader

 

In meiner services.yml habe ich einen neuen Eintrag ergänzt:

 

serie3HelperBundle.TranslationWebLoader:
  class: Serie3\HelperBundle\Services\TranslationWebLoader
  arguments: ["@parameter_bag"]
  tags:
    - { name: 'translation.loader', alias: 'web' }

 

Als Nächstes legen wir die eigentliche Loader-Klasse an, die das LoaderInterface implementiert:

 

class TranslationWebLoader implements LoaderInterface
{
    public function __construct(private ParameterBagInterface $params){}

    public function load(mixed $resource='', string $locale='', string $domain = 'messages'): MessageCatalogue
    {
        $baseUrl = $this->params->get('serie3_user.translation_url');
        if($locale !== 'en'){
            $baseUrl = $baseUrl.$locale;
        }
        $catalogue = new MessageCatalogue($locale);
        if ($data = @file_get_contents($baseUrl)) {

            $translations = json_decode($data, true);

            $catalogue = new MessageCatalogue($locale);

            // rekursives Hinzufügen der Übersetzungen starten
            $this->addTranslations($catalogue, $translations, [], $domain);
        } else {
            // Fehler behandeln, falls der Inhalt nicht geladen werden konnte
            $error = error_get_last();
            throw new \RuntimeException('Could not fetch translations: ' . $error['message']);
        }

        return $catalogue;
    }

    /**
     * Fügt dem Katalog rekursiv Übersetzungen hinzu.
     *
     * @param MessageCatalogue $catalogue
     * @param array $translations
     * @param array $keyPath Hält die verschachtelten Keys bis zur aktuellen Übersetzung
     * @param string $domain
     */
    private function addTranslations(MessageCatalogue $catalogue, array $translations, array $keyPath, string $domain): void
    {
        foreach ($translations as $key => $value) {
            // aktuellen Key an den Key-Pfad anhängen
            $currentKeyPath = array_merge($keyPath, [$key]);

            if (is_array($value)) {
                // ist der Wert ein Array, rekursiv hineingehen
                $this->addTranslations($catalogue, $value, $currentKeyPath, $domain);
            } else {
                // Keys mit einem Punkt zum finalen Übersetzungs-Key verbinden
                $finalKey = implode('.', $currentKeyPath);

                // Übersetzung dem Katalog hinzufügen
                $catalogue->add([$finalKey => $value], $domain);
            }
        }
    }
}

 

Gehen wir das Wichtigste durch:

 

$baseUrl = $this->params->get('serie3_user.translation_url');

 

Weil ich dieses Bundle wiederverwende, habe ich eine Umgebungsvariable angelegt, die dem Loader sagt, woher er die Übersetzungen holen soll. Wenn du deinen Übersetzungs-Service nicht in anderen Projekten wiederverwenden willst, brauchst du das nicht und kannst die Übersetzungs-URL hart verdrahten. 

Aber: Dinge hart zu verdrahten ist immer mies! Sei nicht faul und leg eine Umgebungsvariable an!

Der nächste wichtige Teil ist hier:

 

if ($data = @file_get_contents($baseUrl)) {

    $translations = json_decode($data, true);

    $catalogue = new MessageCatalogue($locale);

    // rekursives Hinzufügen der Übersetzungen starten
    $this->addTranslations($catalogue, $translations, [], $domain);
} else {
    // Fehler behandeln, falls der Inhalt nicht geladen werden konnte
    $error = error_get_last();
    throw new \RuntimeException('Could not fetch translations: ' . $error['message']);
}

 

Wir laden hier die Daten von der eben gebauten Basis-URL und fügen die Übersetzungen dem neuen Übersetzungskatalog hinzu.

3. Wie du es nutzt

In meinen Apps können die Nutzer entscheiden, welche Sprache sie verwenden wollen, Standard ist Englisch. Ändert der Nutzer das, wird die Präferenz in der Datenbank gespeichert, und bei jedem Besuch bekommt er oder sie die richtigen Übersetzungen.

Wichtig: Dir ist vielleicht aufgefallen, dass ich hier keinen Sprachwechsel in der URL nutze, sondern die Präferenz aus der Datenbank hole. Für Suchmaschinen ist das ein Problem, aber alle meine Projekte setzen stark auf Inhalte, die ohnehin nicht indexiert werden. Wenn du viel Öffentliches hast, nimm unbedingt einen Locale-Parameter in die URL auf, etwa /en/ für Englisch oder /de/ für Deutsch 

Die Spracheinstellung in der Datenbank zu speichern erlaubt mir, Cronjobs laufen zu lassen und immer die richtigen Übersetzungen zu bekommen, auch wenn der Nutzer nicht online ist. 

Ein gutes Beispiel ist mein PDF-Erzeugungsservice. Damit kann ich eine Rechnung in der bevorzugten Sprache des Nutzers erstellen, auch wenn dieser gerade nicht online ist.

 

public function __construct(private Environment $twig, private TranslatorInterface $translator, private Rights $rights){}

 

 

Ich übergebe das Translator-Interface an den PDF-Service und setze die richtige Sprache für die Übersetzungen 

 

$this->translator->setLocale($locale);

 

Im Twig-Template kann ich dann den trans-Filter nutzen, um den Text zu übersetzen:

 

{{ 'Templates.RemoteOnly'|trans }}

 

Wie der Ablauf funktioniert

Wie der Übersetzungsablauf im Backend mit Symfony und TYPO3 funktioniert

Cache-Probleme

Der Ansatz funktioniert gut, aber auf eine große Sache musst du achten: den Cache! Immer wenn eine Übersetzung aktualisiert wurde, solltest du auch deinen Symfony-Cache leeren, damit die gespeicherten Übersetzungen mitgezogen werden.

Sie brauchen einen Senior-Entwickler, der liefert?

Über 20 Jahre. Produktionssysteme. Echte Deadlines. Ob Architekt, Entwickler oder DevOps-Engineer — ich habe alle drei Rollen übernommen, oft im selben Projekt. Sparen wir uns den Agentur-Overhead und sprechen direkt.