LW IT Solutions logo LW IT Solutions logo LW IT Solutions
« Blog-Übersicht /WordPress-Plugins & Tricks/Tutorials / Plugins übersetzen: Text Domain, Sprachpakete und JavaScript
Diesen Artikel in anderen Sprachen lesen:

Plugins übersetzen: Text Domain, Sprachpakete und JavaScript

Plugins übersetzen: Text Domain, Sprachpakete und JavaScript
Inhalt
  1. Die Text Domain: eine Kennung, identisch mit dem Slug
  2. Die gettext-Funktionen in PHP
  3. Wer die Übersetzungen lädt: Just-in-time statt load_plugin_textdomain
  4. JavaScript: @wordpress/i18n, Build-Schritt und wp_set_script_translations
  5. Vom Quelltext zum Sprachpaket
  6. Eigene Übersetzungsdateien mit WP-CLI
  7. Grenzen und offene Punkte
  8. Fragen und Antworten
  9. Quellen

Ein Plugin, das nur Englisch kann, zeigt auf einer deutschen oder polnischen Website englische Schaltflächen, obwohl WordPress selbst längst die Sprache der Besucher spricht. Dieser Blog erscheint auf Englisch, Deutsch und Polnisch; jede Zeichenkette eines hier eingesetzten Plugins muss also dreifach vorliegen, auch die Texte, die JavaScript erst nachträglich in die Seite schreibt. WordPress löst das auf drei Ebenen: gettext-Aufrufe in PHP, eigene JSON-Dateien für Skripte und Sprachpakete, die translate.wordpress.org baut und ausliefert. Viele ältere Anleitungen rufen load_plugin_textdomain() noch früh in plugins_loaded auf, obwohl der Aufruf seit WordPress 6.7 nur noch den Pfad registriert; die mit 6.7 eingeführte Meldung stammt von Übersetzungsaufrufen vor after_setup_theme.

Dieser Beitrag baut ein kleines Plugin „LW Reading Time“ mit einer PHP-Datei und einem Skript, das @wordpress/i18n importiert. Er zeigt die Übersetzungsfunktionen, die Pluralregeln für Deutsch und Polnisch, was WordPress 7.1.2 (aktuelle Version am 29.09.2026) selbst lädt, wie eine Zeichenkette über GlotPress in ein Sprachpaket gelangt und wie sich Übersetzungsdateien lokal mit WP-CLI erzeugen lassen. Plugin Check und die Felder der readme.txt sind Thema eigener Beiträge dieser Reihe; hier tauchen sie nur dort auf, wo Übersetzungen sie berühren.

Die Text Domain: eine Kennung, identisch mit dem Slug

Jeder gettext-Aufruf trägt als letztes Argument die Text Domain, die eine Zeichenkette der richtigen Übersetzungsdatei zuordnet. Für Plugins im Verzeichnis gilt eine feste Regel: Die Text Domain entspricht dem Slug, ein Plugin mit dem Slug akismet nutzt also die Text Domain akismet, und mehr als eine Text Domain pro Plugin ist nicht vorgesehen. Laut Meta-Handbuch lässt sich ein Plugin sonst nicht sauber in translate.wordpress.org importieren und bekommt keine Sprachpakete. Erlaubt sind Kleinbuchstaben und Bindestriche, keine Unterstriche, keine Leerzeichen.

Außerdem muss die Text Domain als literale Zeichenkette dastehen. wp i18n make-pot und der Importer auf WordPress.org lesen den Quelltext, ohne ihn auszuführen; eine Konstante oder Variable anstelle von 'lw-reading-time' bleibt für sie unsichtbar, und das Plugin-Handbuch rät ausdrücklich davon ab. Auch Zeichenketten, die der Core bereits übersetzt, etwa „Settings“, bekommen die eigene Text Domain. Sonst verlieren sie ihre Übersetzung, sobald der Core den Wortlaut ändert.

Der Header Text Domain ist laut Handbuch seit WordPress 4.6 optional, Domain Path darf bei Plugins im Verzeichnis fehlen. Es lohnt sich trotzdem, beide zu setzen: make-pot liest die Text Domain standardmäßig aus dem Header, und seit WordPress 6.8 laden Plugins außerhalb des Verzeichnisses ihre Übersetzungen genau über dieses Paar automatisch. Plugin Check 2.1.0 (erschienen am 16.08.2026) meldet ein ungültiges Format der Text Domain als Fehler, einen vom Slug abweichenden Header als Warnung und ebenso einen Domain Path ohne führenden Schrägstrich oder mit Verweis auf einen fehlenden Ordner. Das Beispiel-Plugin bringt deshalb einen vorhandenen Ordner languages/ mit der POT-Datei mit.

Die gettext-Funktionen in PHP

Die Hauptdatei des Beispiel-Plugins zeigt die Lesezeit über einzelnen Beiträgen an und ergänzt eine Schaltfläche, die das Skript später umbeschriftet. Jede sichtbare Zeichenkette läuft durch eine Übersetzungsfunktion:

PHP
<?php
/**
 * Plugin Name:       LW Reading Time
 * Description:       Shows the estimated reading time above single posts.
 * Version:           1.0.0
 * Requires at least: 6.8
 * Requires PHP:      7.4
 * Author:            Lukas Wojcik
 * License:           GPL-2.0-or-later
 * License URI:       https://www.gnu.org/licenses/gpl-2.0.html
 * Text Domain:       lw-reading-time
 * Domain Path:       /languages
 *
 * @package LW_Reading_Time
 */

defined( 'ABSPATH' ) || exit;

const LW_READING_TIME_WORDS_PER_MINUTE = 200;

/**
 * Returns the estimated reading time of a post in whole minutes.
 *
 * @param WP_Post $post Post object.
 * @return int Minutes, at least 1.
 */
function lw_reading_time_minutes( WP_Post $post ) {
	$text  = wp_strip_all_tags( strip_shortcodes( $post->post_content ) );
	$words = preg_split( '/\s+/u', trim( $text ), -1, PREG_SPLIT_NO_EMPTY );

	return max( 1, (int) ceil( count( $words ) / LW_READING_TIME_WORDS_PER_MINUTE ) );
}

/**
 * Prepends the reading time and a "mark as read" button to single posts.
 *
 * @param string $content Post content.
 * @return string
 */
function lw_reading_time_prepend( $content ) {
	if ( ! is_singular( 'post' ) || ! in_the_loop() || ! is_main_query() ) {
		return $content;
	}

	$post    = get_post();
	$minutes = lw_reading_time_minutes( $post );

	$label = sprintf(
		/* translators: %s: estimated reading time in minutes. */
		_n( 'Reading time: %s minute', 'Reading time: %s minutes', $minutes, 'lw-reading-time' ),
		number_format_i18n( $minutes )
	);

	$button = sprintf(
		'<button type="button" class="lw-reading-time__mark" data-post="%1$d">%2$s</button>',
		(int) $post->ID,
		esc_html__( 'Mark as read', 'lw-reading-time' )
	);

	return '<p class="lw-reading-time">' . esc_html( $label ) . ' ' . $button . '</p>' . $content;
}
add_filter( 'the_content', 'lw_reading_time_prepend' );

/**
 * Enqueues the front-end script and connects it to its translation files.
 */
function lw_reading_time_enqueue_script() {
	if ( ! is_singular( 'post' ) ) {
		return;
	}

	$asset_file = plugin_dir_path( __FILE__ ) . 'build/index.asset.php';
	if ( ! file_exists( $asset_file ) ) {
		return;
	}
	$asset = require $asset_file;

	wp_enqueue_script(
		'lw-reading-time',
		plugins_url( 'build/index.js', __FILE__ ),
		$asset['dependencies'],
		$asset['version'],
		array( 'in_footer' => true )
	);

	// Looks for lw-reading-time-{locale}-{md5 of "build/index.js"}.json in wp-content/languages/plugins, without a language pack there in languages/ (Domain Path).
	wp_set_script_translations( 'lw-reading-time', 'lw-reading-time' );
}
add_action( 'wp_enqueue_scripts', 'lw_reading_time_enqueue_script' );

__() gibt die Übersetzung zurück, _e() gibt sie direkt aus. Genau diese Ausgabe ohne Escaping bemängeln die WordPress Coding Standards: Der Sniff EscapeOutput führt _e() und _ex() als unsichere Ausgabefunktionen und schlägt esc_html_e() oder esc_attr_e() vor. Die kombinierten Funktionen esc_html__(), esc_html_e(), esc_attr__(), esc_attr_e(), esc_html_x() und esc_attr_x() übersetzen und escapen in einem Schritt. Eine Übersetzung bleibt Text von außerhalb des Codes und wird wie jede andere Ausgabe behandelt. Im Beispiel umschließt esc_html() das Ergebnis von sprintf(); ein Escaping nur des Musters ließe den eingesetzten Wert ungeprüft.

Platzhalter und Übersetzerkommentare

Variablen gehören nie in die zu übersetzende Zeichenkette. Ein Aufruf wie _e( "Your city is $city." ) scheitert, weil der Extraktor den Text mit dem wörtlichen $city findet, während WordPress zur Laufzeit nach einer Zeichenkette mit dem tatsächlichen Ortsnamen sucht, die in keiner Übersetzungsdatei steht. Richtig ist ein Platzhalter mit printf() oder sprintf(). Bei mehreren Platzhaltern empfiehlt das Handbuch nummerierte Argumente wie %1$s und %2$s in einfachen Anführungszeichen, damit eine Übersetzung die Reihenfolge ändern kann.

Ein Kommentar, der mit translators: beginnt, erklärt den Übersetzenden, wofür ein Platzhalter steht. Er muss der letzte PHP-Kommentar vor dem gettext-Aufruf sein; make-pot übernimmt ihn als #.-Zeile in die POT-Datei, und GlotPress zeigt ihn neben der Zeichenkette an. Die Zahl selbst läuft durch number_format_i18n(), weil Deutsch und Polnisch andere Tausendertrennzeichen verwenden als Englisch.

_x() ergänzt einen Kontext für Wörter mit mehreren Bedeutungen; GlotPress führt Kontext und Text dann als eigenen Eintrag. Das Skript weiter unten nutzt _x( 'Read', … ) für den Zustand nach dem Klick. Ohne Kontext könnte „Read“ als Verb „Lesen“ oder „Czytaj“ übersetzt werden, die Schaltfläche braucht aber „Gelesen“ bzw. „Przeczytany“.

Pluralformen: zwei im Englischen und Deutschen, drei im Polnischen

_n() erhält Singular, Plural, Anzahl und Text Domain. Englisch und Deutsch kommen mit zwei Formen aus, Polnisch braucht drei. Die Regel steht im Header Plural-Forms jeder Sprache. GlotPress, die Software hinter translate.wordpress.org, definiert Polnisch mit nplurals=3 und dem Ausdruck (n == 1) ? 0 : ((n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 12 || n % 100 > 14)) ? 1 : 2); ein polnischer Export von translate.wordpress.org trägt denselben Header. Für die Lesezeit ergibt das:

MinutenEnglischDeutschPolnisch (Form)
1Reading time: 1 minuteLesezeit: 1 MinuteCzas czytania: 1 minuta (0)
2Reading time: 2 minutesLesezeit: 2 MinutenCzas czytania: 2 minuty (1)
5Reading time: 5 minutesLesezeit: 5 MinutenCzas czytania: 5 minut (2)
12Reading time: 12 minutesLesezeit: 12 MinutenCzas czytania: 12 minut (2)
21Reading time: 21 minutesLesezeit: 21 MinutenCzas czytania: 21 minut (2)
22Reading time: 22 minutesLesezeit: 22 MinutenCzas czytania: 22 minuty (1)

Daraus folgt eine Regel aus dem Handbuch: Auch der Singular enthält den Platzhalter, also '%s minute' statt 'One minute', denn in manchen Sprachen deckt die „Singular“-Form auch andere Zahlen als eins ab. Der Code übergibt nur die Zahl; welcher der drei polnischen Sätze erscheint, entscheidet die Übersetzungsdatei. Für Pluraltexte, deren Anzahl beim Definieren noch nicht feststeht, gibt es _n_noop() und translate_nooped_plural().

Wer die Übersetzungen lädt: Just-in-time statt load_plugin_textdomain

Mit WordPress 4.6 änderte sich das Laden grundlegend. Die Entwicklernotiz vom Juli 2016 führte das Just-in-time-Laden ein: Trifft WordPress zum ersten Mal auf eine Text Domain, sucht _load_textdomain_just_in_time() in wp-content/languages nach der passenden Datei und lädt sie. Plugins aus dem Verzeichnis brauchen seitdem kein load_plugin_textdomain() mehr. Dieselbe Version drehte die Suchreihenfolge um: Sprachpakete in wp-content/languages haben Vorrang vor Dateien im Plugin-Ordner.

WordPress 6.5 ersetzte das Übersetzungssystem. Sprachpakete enthalten seitdem neben .po und .mo auch eine .l10n.php-Datei. Ist sie vorhanden, lädt WordPress sie anstelle der .mo-Datei. Das geht schneller, benötigt weniger Speicher und nutzt OPcache. Mit dem Filter translation_file_format lässt sich wieder auf mo umstellen.

Seit WordPress 6.7 registriert load_plugin_textdomain() nur noch den Pfad; das eigentliche Laden übernimmt der Just-in-time-Mechanismus. Gleichzeitig kam eine _doing_it_wrong()-Meldung hinzu. Werden Übersetzungen einer Domain vor after_setup_theme angefordert, meldet WordPress, das Laden sei „too early“ ausgelöst worden, und nennt init als frühesten Zeitpunkt. Laut Entwicklernotiz muss WordPress beim frühen Laden den aktuellen Benutzer vorzeitig ermitteln. Andere Plugins können dann Sprache oder Dateipfad nicht mehr filtern.

Für Tests ist ein Detail aus der l10n.php des Core wichtig: Die Meldung erscheint, sobald der Core für die Domain ein Sprachverzeichnis kennt, auch wenn keine Übersetzungsdatei vorhanden ist. Seit 6.8 registriert der Core für jedes aktive Plugin den Ordner aus Domain Path oder, ohne diesen Header, den Plugin-Ordner. Deshalb löst das folgende Beispiel unter WordPress 7.1.2 die Meldung auch auf einer englischen Website ohne jede Übersetzungsdatei aus.

PHP
<?php
// Too early: runs while WordPress loads the plugin file, before after_setup_theme and init.
// WordPress 6.7+ reports (since 6.8 also without a translation file, even on en_US)
// _load_textdomain_just_in_time() via _doing_it_wrong().
define( 'LW_READING_TIME_TITLE', __( 'Reading time', 'lw-reading-time' ) );

// Better: translate at the moment the text is needed (init or later).
function lw_reading_time_title() {
	return __( 'Reading time', 'lw-reading-time' );
}

// Plugin metadata without translating it: third parameter $translate = false.
function lw_reading_time_version() {
	if ( ! function_exists( 'get_plugin_data' ) ) {
		require_once ABSPATH . 'wp-admin/includes/plugin.php';
	}
	$data = get_plugin_data( __FILE__, false, false );

	return $data['Version'];
}

Die Notiz nennt zwei typische Ursachen: get_plugin_data() ohne $translate = false und Klassen, die direkt in der Hauptdatei instanziiert werden und im Konstruktor __() aufrufen. Zum Aufspüren schlägt sie einen Callback auf doing_it_wrong_run vor, der für _load_textdomain_just_in_time einen Backtrace ausgibt, oder Query Monitor.

WordPress 6.8 dehnte das Just-in-time-Laden auf Plugins außerhalb des Verzeichnisses aus und wertet dafür die Header Text Domain und Domain Path aus. Die Entwicklernotiz fasst es in zwei Fällen zusammen:

  • Plugin auf WordPress.org, Mindestversion WordPress 4.6 oder höher: kein Aufruf von load_plugin_textdomain() nötig.
  • Plugin mit den Headern Text Domain und Domain Path, Mindestversion WordPress 6.8 oder höher: ebenfalls kein Aufruf nötig.

In allen anderen Fällen, etwa bei einem anderswo vertriebenen Plugin mit Unterstützung älterer Versionen, bleibt der Aufruf, wandert aber nach init. Plugin Check setzt das für das Verzeichnis durch: Der Sniff PluginCheck.CodeAnalysis.DiscouragedFunctions.load_plugin_textdomainFound erzeugt eine Warnung, sobald die Mindestversion aus Requires at least (Header oder Readme) über 4.6 liegt. Die Meldung ergänzt, dass WordPress die Übersetzungen unter dem Plugin-Slug automatisch lädt.

JavaScript: @wordpress/i18n, Build-Schritt und wp_set_script_translations

Seit WordPress 5.0 stellt das Skript-Handle wp-i18n dieselben Funktionen im Browser bereit: __(), _x(), _n(), _nx() und sprintf(). Das Beispiel importiert sie aus dem npm-Paket @wordpress/i18n und baut die Datei mit @wordpress/scripts:

JSON
{
	"name": "lw-reading-time",
	"version": "1.0.0",
	"private": true,
	"license": "GPL-2.0-or-later",
	"scripts": {
		"build": "wp-scripts build",
		"start": "wp-scripts start"
	},
	"devDependencies": {
		"@wordpress/scripts": "^36.0.0"
	}
}
JavaScript
// src/index.js
import { _n, _x, sprintf } from '@wordpress/i18n';

const STORAGE_KEY = 'lwReadingTimeRead';

function getReadPosts() {
	try {
		const stored = JSON.parse( window.localStorage.getItem( STORAGE_KEY ) );
		return Array.isArray( stored ) ? stored : [];
	} catch ( error ) {
		return [];
	}
}

function saveReadPosts( ids ) {
	try {
		window.localStorage.setItem( STORAGE_KEY, JSON.stringify( ids ) );
	} catch ( error ) {
		// Storage may be unavailable (e.g. private windows); the button still updates.
	}
}

document.querySelectorAll( '.lw-reading-time__mark' ).forEach( ( button ) => {
	button.addEventListener( 'click', () => {
		const postId = Number( button.dataset.post );
		const readPosts = getReadPosts();

		if ( ! readPosts.includes( postId ) ) {
			readPosts.push( postId );
			saveReadPosts( readPosts );
		}

		button.disabled = true;
		button.textContent = _x(
			'Read',
			'status of a post after it was marked as read',
			'lw-reading-time'
		);
		button.title = sprintf(
			/* translators: %d: number of posts marked as read in this browser. */
			_n(
				'%d post marked as read in this browser',
				'%d posts marked as read in this browser',
				readPosts.length,
				'lw-reading-time'
			),
			readPosts.length
		);
	} );
} );

Ohne block.json nimmt wp-scripts build die Datei src/index.js als Einstiegspunkt und schreibt build/index.js. Das Dependency-Extraction-Plugin ersetzt den Import durch das globale wp.i18n und erzeugt build/index.asset.php mit wp-i18n als Abhängigkeit und einem Versions-Hash; die PHP-Datei reicht beides an wp_enqueue_script() weiter. Die Webpack-Konfiguration behält Kommentare mit translators: in der minimierten Ausgabe, und make-pot erkennt die Aufrufformen, die Webpack erzeugt.

Die Verbindung zwischen Skript und Übersetzung stellt wp_set_script_translations() her, verfügbar seit WordPress 5.0, mit optionaler Text Domain seit 5.1. Die Funktion wirkt nur bei einem bereits registrierten Skript, erkennbar am Handle – demselben Bezeichner, mit dem der Beitrag zum selektiven Dequeuing von Plugin-Skripten arbeitet. WordPress sucht dann eine JSON-Datei im Jed-Format und legt deren Inhalt in wp.i18n ab, bevor das Skript läuft. Der Dateiname folgt dem Muster {domain}-{locale}-{md5}.json; der Hash wird aus dem Pfad des Skripts relativ zum Plugin-Ordner berechnet und lautet für build/index.js dfbff627e6c248bcb3b61d7d06da9ca9. Der Core bildet .min.js vor dem Hashen auf .js ab, und make-pot überspringt *.min.js standardmäßig. Ein optionaler dritter Parameter nennt ein Verzeichnis, das WordPress zuerst prüft, sowohl auf {domain}-{locale}-{handle}.json als auch auf den Hash-Namen, bevor es auf wp-content/languages/plugins zurückfällt.

Für Skriptmodule, die über wp_register_script_module() registriert werden, kam mit WordPress 7.0 das Gegenstück wp_set_script_module_translations() hinzu. Das Beispiel nutzt ein klassisches Skript, daher bleibt es bei diesem Hinweis.

Vom Quelltext zum Sprachpaket

Bei Plugins im Verzeichnis liest WordPress.org nach dem nächsten SVN-Commit alle PHP- und JavaScript-Dateien aus und importiert die Zeichenketten in translate.wordpress.org. Dort erscheint jedes Plugin mit vier Teilprojekten: Development, Development Readme, Stable (latest release) und Stable Readme (latest release). Vorhandene .mo-Dateien im Plugin werden einmalig übernommen; danach findet die Übersetzungsarbeit auf der Plattform statt.

Übersetzungen vorschlagen darf jede Person mit einem WordPress.org-Konto. Vorschläge starten im Status „Waiting“ und zählen erst, wenn ein General Translation Editor der Sprache oder ein für das Plugin ernannter Project Translation Editor sie auf „Current“ setzt. Ungeprüfte maschinelle Übersetzungen werden nicht angenommen. Das erste Sprachpaket einer Sprache entsteht, sobald mindestens 90 Prozent der Zeichenketten im Teilprojekt Stable freigegeben sind; Readme-Texte zählen dabei nicht mit. Danach löst jede Änderung an Zeichenketten oder Übersetzungen ein neues Sprachpaket aus, auch wenn der Anteil wieder unter 90 Prozent fällt.

Ein Sprachpaket enthält die .po– und .mo-Datei, seit WordPress 6.5 die .l10n.php-Datei und je Skript mit übersetzbaren Zeichenketten eine JSON-Datei. WordPress installiert es über die regulären Übersetzungsaktualisierungen nach wp-content/languages/plugins/. Dort finden das Just-in-time-Laden und wp_set_script_translations() die Dateien ohne weiteren Code im Plugin.

Eine Zeichenkette vom Quelltext bis zur Ausgabe: __() und _n() in PHP und @wordpress/i18n im Skript werden nach dem SVN-Commit importiert, in GlotPress vorgeschlagen und freigegeben, ab 90 % Freigabe zum Sprachpaket gebaut und für PHP und JavaScript just in time geladen; lokal baut WP-CLI die Dateien ohne GlotPress
Wie translate.wordpress.org aus einem gettext-Aufruf ein Sprachpaket macht, das WordPress für PHP und JavaScript lädt

Eigene Übersetzungsdateien mit WP-CLI

Lokale Dateien sind nötig für Plugins außerhalb des Verzeichnisses, für die Zeit vor dem ersten Sprachpaket und für Kundenprojekte mit eigenem Wortlaut. Die Befehle unter wp i18n decken die ganze Kette ab:

Shell
# Run inside wp-content/plugins/lw-reading-time/

# 1. Build the script: creates build/index.js and build/index.asset.php (dependency "wp-i18n").
npm install
npm run build

# 2. Extract all strings from PHP and from the built JavaScript into a template.
wp i18n make-pot . languages/lw-reading-time.pot --exclude=src

# 3. Copy the template to languages/lw-reading-time-pl_PL.po (and -de_DE.po),
#    translate it in Poedit or a text editor, then compile the runtime files.
wp i18n make-mo languages
wp i18n make-php languages
wp i18n make-json languages --no-purge

# Result for Polish:
# languages/lw-reading-time-pl_PL.po
# languages/lw-reading-time-pl_PL.mo
# languages/lw-reading-time-pl_PL.l10n.php
# languages/lw-reading-time-pl_PL-dfbff627e6c248bcb3b61d7d06da9ca9.json   (md5 of "build/index.js")

make-pot durchsucht PHP-, Blade- und JavaScript-Dateien sowie den Plugin-Header und schließt node_modules, vendor, Versionsverwaltungsordner und *.min.js immer aus. Im Beispiel sorgt --exclude=src dafür, dass nur die gebaute Datei als Quelle der JavaScript-Texte erscheint. make-mo kompiliert die Binärdateien, make-php erzeugt die .l10n.php-Dateien und kam mit WP-CLI 2.10.0 (i18n-command 2.6.0). make-json schreibt je JavaScript-Quelldatei eine JSON-Datei im Jed-Format und entfernt standardmäßig anschließend die JavaScript-Texte aus der PO-Datei; mit --no-purge bleibt sie unverändert. Die polnische PO-Datei enthält dann alle drei Pluralformen:

Text
# languages/lw-reading-time-pl_PL.po (excerpt)
msgid ""
msgstr ""
"Language: pl_PL\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Plural-Forms: nplurals=3; plural=(n == 1) ? 0 : ((n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 12 || n % 100 > 14)) ? 1 : 2);\n"
"X-Domain: lw-reading-time\n"

#. translators: %s: estimated reading time in minutes.
#: lw-reading-time.php:50
msgid "Reading time: %s minute"
msgid_plural "Reading time: %s minutes"
msgstr[0] "Czas czytania: %s minuta"
msgstr[1] "Czas czytania: %s minuty"
msgstr[2] "Czas czytania: %s minut"

#: lw-reading-time.php:57
msgid "Mark as read"
msgstr "Oznacz jako przeczytany"

#: build/index.js:1
msgctxt "status of a post after it was marked as read"
msgid "Read"
msgstr "Przeczytany"

#. translators: %d: number of posts marked as read in this browser.
#: build/index.js:2
msgid "%d post marked as read in this browser"
msgid_plural "%d posts marked as read in this browser"
msgstr[0] "%d wpis oznaczony jako przeczytany w tej przeglądarce"
msgstr[1] "%d wpisy oznaczone jako przeczytane w tej przeglądarce"
msgstr[2] "%d wpisów oznaczonych jako przeczytane w tej przeglądarce"

Seit 4.6 haben Sprachpakete in wp-content/languages Vorrang vor Dateien im Plugin. Soll ein Plugin trotzdem eigene Übersetzungen laden, verweist das Handbuch auf den Filter load_textdomain_mofile; seit 6.5 bietet der Core zusätzlich load_translation_file, der den Pfad der .mo– wie der .l10n.php-Datei erhält. Für Skripte legt der dritte Parameter von wp_set_script_translations() das Verzeichnis fest, das WordPress zuerst prüft.

Grenzen und offene Punkte

  • Das Beispiel-Plugin lief unter WordPress 7.1.2 mit WP-CLI 2.12.0 und @wordpress/scripts 36.0.0; das Paket verlangt Node 22.22.2 oder neuer, unter Node 20 warnte npm nur. Die Zeilennummern im PO-Auszug stammen aus diesem Build und verschieben sich mit jeder Codeänderung.
  • Ob der Importer auf WordPress.org Ordner wie src/ oder minimierte Dateien ebenso überspringt wie make-pot, ist in den genutzten Quellen nicht dokumentiert. Für das Sprachpaket zählt, dass die gebaute Datei im SVN liegt.
  • Ohne dritten Parameter findet wp_set_script_translations() unter WordPress 7.1.2 die JSON-Dateien auch im Ordner aus Domain Path, weil der Core das für die Domain registrierte Verzeichnis verwendet. Liegt in wp-content/languages/plugins eine Übersetzungsdatei derselben Sprache, bleiben die JSON-Dateien des Plugins unbeachtet.
  • _n() erwartet eine ganze Zahl; Bruchwerte wie 1,5 Minuten folgen im Polnischen anderen grammatischen Regeln. Das Beispiel rundet auf ganze Minuten auf.

Fragen und Antworten

Muss ein Plugin im Verzeichnis eigene .mo-Dateien mitliefern?

Nein. Nach dem Import in translate.wordpress.org entstehen Sprachpakete, die WordPress selbst nach wp-content/languages/plugins/ lädt. Vorhandene .mo-Dateien werden beim ersten Import einmalig übernommen; laut Meta-Handbuch lassen sie sich danach aus Paket und SVN entfernen.

Erscheint die Meldung zum zu frühen Laden nur auf einer Website mit Übersetzungsdatei?

Nein, unter WordPress 7.1.2 nicht. Die Prüfung in _load_textdomain_just_in_time() greift, sobald der Core für die Domain ein Sprachverzeichnis kennt, und seit 6.8 registriert er für jedes aktive Plugin den Plugin-Ordner oder dessen Domain Path. Ein Test auf einer Website mit en_US ganz ohne Übersetzungsdatei zeigt die Meldung deshalb ebenfalls. Sprachpakete bleiben nötig, um die übersetzte Ausgabe selbst zu prüfen.

PHP-Texte sind übersetzt, die Skripttexte bleiben englisch. Woran liegt das meist?

  • wp_set_script_translations() läuft vor der Registrierung des Skripts oder mit falschem Handle.
  • Der Hash im JSON-Dateinamen passt nicht zum relativen Pfad der ausgelieferten Datei, etwa weil die POT-Datei aus src/ statt aus build/ erzeugt wurde.
  • Die gebaute Datei fehlt im SVN, sodass WordPress.org keine JavaScript-Texte findet.

Quellen

Lukas Wojcik

Lukas Wojcik

Systems architect and technology enthusiast specializing in scalable tracking solutions, GMP Stack (GA4 & GTM), and robust backend architectures. Advocate for clean code and privacy-first design.

Kontakt

Eine Frage oder eine Idee für ein Webprojekt? Schreib mir kurz, worum es geht. Das Formular nutzt reCAPTCHA zum Schutz vor Spam.

Kommentar schreiben

Erfahrungen mit anderen Plugins oder Hosting-Umgebungen und Rückfragen zur Einrichtung sind hier willkommen.

Die E-Mail-Adresse wird nicht veröffentlicht. Pflichtfelder sind mit einem Stern versehen.