Wer schon mal an WordPress oder einem der Plugins und Themes in den offiziellen Verzeichnissen mitübersetzt hat, wird translate.wordpress.org kennen – eine Plattform, auf der die Übersetzungen für (unter anderem) den WordPress-Core und viele tausend Plugins und Themes gepflegt werden.

Dahinter steht GlotPress, das es als WordPress-Plugin gibt, sodass sich theoretisch jeder und jede selbst eine solche Plattform aufsetzen kann, um zum Beispiel Übersetzungen für Plugins und Themes von Kundenprojekten zu pflegen (oder andere pflegen zu lassen), ohne für jedes Übersetzungsupdate auch ein Update des Themes oder Plugins bereitstellen zu müssen.

Für ein bisschen mehr Komfort dabei sorgt Traduttore, entwickelt von required, das zum Beispiel automatisch Strings aus Code-Repositorys extrahieren und Sprachpakete erstellen kann, auf die eine WordPress-Installation dann zugreift, als würden sie von translate.wordpress.org kommen.

Voraussetzungen

Wir brauchen eine WordPress-Installation auf einem Server mit

Außerdem ein Git-Repository bei GitHub, GitLab oder Bitbucket mit dem Code des Themes oder Plugins, das wir über unsere Übersetzungsplattform übersetzen möchten.

Installation von GlotPress und Traduttore

GlotPress lässt sich einfach über die Plugin-Suche im Backend installieren. Nach der Installation und Aktivierung findet ihr die GlotPress-Oberfläche unter https://example.com/glotpress/, wobei example.com natürlich gegen eure Website-Domain ausgetauscht werden muss. Wir werden aber zunächst noch kein Projekt anlegen, sondern noch ein paar weitere Dinge erledigen.

Erst mal kümmern wir uns um die Installation von Traduttore. Es liegt als Composer-Paket wearerequired/traduttore vor, kann aber auch von GitHub heruntergeladen, entpackt und auf dem Server geladen werden. Auch wenn wir es von GitHub herunterladen und das entpackte Verzeichnis in das wp-content/plugins-Verzeichnis hochgeladen haben, müssen wir innerhalb des Traduttore-Ordners noch composer install ausführen, um die Abhängigkeiten zu installieren. Anschließend müssen wir das Plugin noch im Backend aktivieren.

Traduttore konfigurieren

Jetzt steht die Konfiguration von Traduttore an. Wir werden später dafür sorgen, dass Traduttore automatisch den aktuellsten Stand der Strings aus dem Code-Repo ziehen kann, sobald dort neuer Code gepusht wurde. Dafür wird von dem Repo ein Webhook gesendet, der eine geheime Zeichenkette enthält, anhand der unsere Traduttore-Installation sicherstellen kann, dass es wirklich ein Request aus dem Repo ist.

Diese Zeichenkette legen wir über eine Konstante in der wp-config.php fest. Je nachdem, welchen Anbieter ihr nutzt, heißt die Konstante TRADUTTORE_BITBUCKET_SYNC_SECRET, TRADUTTORE_GITHUB_SYNC_SECRET oder TRADUTTORE_GITLAB_SYNC_SECRET. Für GitLab kann das dann so aussehen:

define( 'TRADUTTORE_GITLAB_SYNC_SECRET', 'HierDerGeheimeWert' );Code-Sprache: PHP (php)

Um einen Zufallswert für die Konstante zu erhalten, könnt ihr beispielsweise die API von WordPress für die Login-Keys nutzen, oder ein Passwort über einen Passwort-Manager erstellen und dort eintragen (oder ihr haut einfach ein bisschen auf der Tastatur rum). Wenn eure WP-CLI-Installation nicht über wp zugänglich ist, könnt ihr mit TRADUTTORE_WP_BIN den Pfad zu der WP-CLI-Datei angeben.

GlotPress-Projekt erstellen

Nun können wir unser erstes Projekt in GlotPress erstellen. Nachdem ihr /glotpress/ aufgerufen habt, könnt ihr über den entsprechenden Link ein neues Projekt erstellen. Wenn ihr mehrere Plugins/Themes aus einem Projekt übersetzbar machen wollt, bietet es sich hier eventuell an, ein Projekt als Oberprojekt zu nutzen und dem die konkreten Projekte für jedes Plugin/Theme unterzuordnen.

Für die Zusammenarbeit mit Traduttore ist das Feld URL der Quelldatei wichtig. Daraus leitet Traduttore die Repository-URL ab, von der es sich die zu übersetztenden Strings holt. Auf der Traduttore-Seite »Project Setup« ist beschrieben, wie die Links für GitLab, GitHub und Bitbucket aussehen müssen.

Für den Fall, dass ihr die GlotPress-Website mit einem Passwort schützen wollt, wird das Plugin Restricted Site Access empfohlen.

Code-Repository konfigurieren

Damit Traduttore automatisch Änderungen aus dem Repo ziehen kann, braucht es Zugang zu dem Repo. Wenn es sich um ein öffentliches Repository handelt, ist das kein Problem. Wenn es aber ein privates Repo ist, müsst ihr einen Zugang über einen SSH-Schlüssel bereitstellen, dessen privaten Teil ihr auf eurem Server eintragt und den öffentlichen Teil bei eurem GitLab-/GitHub-/Bitbucket-User hinterlegt (optimalerweise erstellt ihr dafür einen sogenannten Machine User).

Wie ein SSH-Key erstellt und dem SSH-Agent hinzugefügt werden kann, ist bei GitHub beschrieben. Den Inhalt der Datei mit der Endung .pub müsst ihr bei GitLab/GitHub/Bitbucket eintragen.

Anschließend muss noch der Webhook eingerichtet werden, der Traduttore darüber informiert, dass neuer Code gepusht wurde. Wie das für die jeweilige Plattform funktioniert, steht in den Docs von Traduttore, beispielhaft für GitLab auf der Seite »GitLab Repository Configuration«.

Wenn ihr jetzt in das Git-Repo pusht, sollten die Änderungen automatisch auch in GlotPress auftauchen. Bei mir hat das leider nicht funktioniert, weil ich mit einem privaten Repo arbeite und der aus PHP ausgeführte Kommandozeilenbefehl zum Klonen des Repos irgendwie nicht auf den SSH-Key meines Servers zugreifen kann. Ich werde da noch etwas rumprobieren, zur Not muss ich das manuell über den von Traduttore mitgelieferten WP-CLI-Befehl anstoßen, was auch kein großes Problem wäre.

Integration in das Plugin/Theme

Nachdem wir jetzt so ziemlich alles in GlotPress und Traduttore fertig konfiguriert haben, muss die Installation, auf der das zu übersetzende Produkt läuft, natürlich wissen, woher es Übersetzungen bekommt. Dafür gibt es die Bibliothek Traduttore Registry, die direkt als Teil des Themes oder Plugins genutzt werden kann, auf Multisites wird der Einsatz eines MU-Plugins empfohlen.

Installieren können wir die Bibliothek wieder über Composer oder indem wir die ZIP von GitHub herunterladen und auch hier erneut composer install in dem entpackten Verzeichnis laufen lassen.

Um die Übersetzungen für ein Projekt von unserer GlotPress-Instanz zu laden, müssen wir \Required\Traduttore_Registry\add_project() nutzen. Die Methode bekommt als ersten Parameter den Typ des Projekts übergeben: plugin oder theme. Als zweiten Parameter erwartet sie den Slug, der dem Verzeichnisnamen des Plugins oder Themes entsprechen muss. Der dritte Parameter ist die URL zur Übersetzungs-API.

Ein Beispiel mit Laden des Autoloaders von Composer sieht so aus:

// Load Composer autoloader.
$autoloader = dirname( __FILE__ ) . '/vendor/autoload.php';
if ( is_readable( $autoloader ) ) {
	require_once $autoloader;
}

add_action( 'init', function() {
	\Required\Traduttore_Registry\add_project(
		'plugin',
		'plugin-slug',
		'https://example.com/glotpress/api/translations/project-slug/'
	);
} );Code-Sprache: PHP (php)

Wichtig ist, dass die <home-url> aus dem Beispiel auf der Plugin-&-Theme-Integration-Seite der Traduttore-Doku die Startseite von GlotPress ist, nicht die Startseite der gesamten Website.

Nachdem diese Registrierung vorgenommen ist, sollten Übersetzungen von userer Übersetzungsplattform geladen werden.

Die Sache mit den JavaScript-Strings

Wenn ihr die seit WordPress 5.0 vorhandene Möglichkeit von Übersetzungs-Funktionen in JavaScript nutzt, könntet ihr – so wie ich – vor einem Problem stehen. Um das Problem zu verstehen, hier grob der Ablauf für Strings in JavaScript:

  1. Traduttore scannt die Dateien im Repo nach Strings zur Übersetzung.
  2. Für Strings aus JavaScript-Dateien wird pro JavaScript-Datei eine JSON-Datei erstellt, die im Namen neben dem Slug des Plugins/Themes noch den md5()-Hash des relativen Pfads zu der Datei enthält.
  3. Wenn WordPress eine JavaScript-Datei lädt, generiert es auch einen Hash aus dem Pfad und sucht die passende JSON-Datei.

Wenn jetzt in dem Repo aber nur die Source-Dateien liegen, die gar nicht auf den Webserver geladen werden, weil vorher beispielsweise durch Webpack Dateien in anderen Verzeichnissen entstehen, die dann von WordPress eingebunden werden, haben wir das Problem. Dann nämlich passen die generierten Hashes von Traduttore (Source-Datei) und die von WordPress (die von Webpack behandelte Datei, die wahrscheinlich in einem anderen Verzeichnis liegt und vielleicht anders heißt) nicht zusammen, und die Übersetzung wird nicht geladen.

Eine Lösung wäre, die gebauten Dateien auch in das Repo zu packen – meiner Meinung nach gehören da aber nur Source-Dateien hin (außerdem hatte ich bei dem Versuch ein Problem mit Übersetzungsfunktionen, die beim Minifizieren so sehr minifiziert wurden, dass Traduttore sie nicht mehr erkannt hat).

Die zweite Lösung ist, Traduttore vor dem Erstellen des Hashes über einen Filter den Pfad zu der gebauten Datei zu übergeben, damit der Hash dem entspricht, was WordPress später zu laden versucht.

Das kann so aussehen (im Beispiel-Plugin werden zwei Skripte mit Blöcken, die aufeinander aufbauen und deshalb in einem Plugin sind, durch Webpack aus ihren Einzeldateien in eine assets/js/editor.blocks.js verarbeitet):

/**
 * Filter the mapping.
 * 
 * @param array               $mapping The mapping of sources to translation entries.
 * @param Translation_Entry[] $entries The translation entries to map.
 * @param Project             $project The project that is exported.
 */
add_filter( 'traduttore.map_entries_to_source', function( $mapping, $entries, $project ) {
	if ( $project->get_slug() !== 'our-project-slug' ) {
		return $mapping;
	}

	$sources = [
		'blocks/block-name/index.js',
		'blocks/other-block/index.js',
	];

	foreach ( $sources as $source ) {
		if ( isset( $mapping[$source] ) ) {
			continue;
		}

		$mapping[$source] = [];
	}

	$mapping['assets/js/editor.blocks.js'] = array_merge( $mapping['blocks/block-name/index.js'], $mapping['blocks/other-block/index.js'] );
	
	unset( $mapping['blocks/block-name/index.js'], $mapping['blocks/other-block/index.js'] );
	
	return $mapping;
}, 10, 3 );Code-Sprache: PHP (php)

Wir prüfen mit dem Filter traduttore.map_entries_to_source zunächst auf den Projekt-Slug und geben alle Mappings für andere Projekte als unser gesuchtes unverändert zurück.

Ist es unser gesuchtes Projekt, legen wir in einem Array die Pfade zu den Source-Dateien ab, die wir mit assets/js/editor.blocks.js ersetzen möchten, damit Traduttore daraus den md5-Hash erzeugt, den WordPress später beim Laden der Übersetzung aus demselben Pfad erstellt.

Anschließend durchlaufen wir das Array um sicherzustellen, dass für jeden dieser Sources ein Key in $mapping vorliegt, um bei dem array_merge() -Aufruf keinen PHP-Fehler zu produzieren. Nachdem die Keys geprüft sind, führen wir mit array_merge() die Einträge aus den Arrays der Source-Pfade in einem Array zusammen, das als Key assets/js/editor.blocks.js bekommt. Danach löschen wir die ursprünglichen Keys aus dem Array und geben es zurück.

Damit sollten die Übersetzungen für Strings mit JavaScript-Übersetzungsfunktionen korrekt geladen werden.

Weitere nützliche Funktionen von Traduttore

Traduttore bringt verschiedene WP-CLI-Befehle mit, beispielsweise um die Strings für ein Projekt zu aktualisieren (sehr nützlich, wenn das mit dem automatischen Aktualisieren nach einem Push nicht funktioniert, wie bei mir) oder Sprachpakete zu erzeugen.

Zudem kann es in Verbindung mit dem inoffiziellen Slack-Plugin für WordPress Benachrichtigungen in Slack-Channel schicken. Weitere Informationen zu Traduttore, etwa zu den vorhandenen Hooks, gibt es in der Traduttore-Dokumentation.

Fazit

Die Einrichtung ist sicher alles andere als einfach, aber wenn erst mal alles läuft, ist es ne tolle Sache.

Ich werde es definitiv nutzen, allein schon wegen der Unterstützung für JavaScript-Strings. Auch die Trennung von der Übersetzung und dem Code gefällt mir gut, damit gehören Plugin- oder Theme-Updates der Vergangenheit an, die nur etwas an der Übersetzung ändern.