Tłumaczenie wtyczek: text domain, paczki językowe i JavaScript

Spis treści
- Text domain: jeden identyfikator, zgodny ze slugiem
- Funkcje gettext w PHP
- Kto ładuje tłumaczenia: just-in-time zamiast load_plugin_textdomain
- JavaScript: @wordpress/i18n, budowanie i wp_set_script_translations
- Od kodu źródłowego do paczki językowej
- Własne pliki tłumaczeń z WP-CLI
- Ograniczenia i otwarte kwestie
- Pytania i odpowiedzi
- Źródła
Wtyczka napisana wyłącznie po angielsku wyświetla angielskie przyciski także na niemieckiej czy polskiej stronie, choć sam WordPress od dawna mówi językiem odwiedzających. Ten blog ukazuje się po angielsku, niemiecku i polsku, więc każdy tekst używanej tu wtyczki musi istnieć w trzech wersjach, łącznie z komunikatami, które JavaScript dopisuje do strony dopiero po jej załadowaniu. WordPress rozwiązuje to na trzech poziomach: wywołania gettext w PHP, osobne pliki JSON dla skryptów oraz paczki językowe budowane i rozsyłane przez translate.wordpress.org. Wiele starszych poradników wciąż wywołuje load_plugin_textdomain() wcześnie, w plugins_loaded, choć od WordPressa 6.7 to wywołanie tylko rejestruje ścieżkę; komunikat wprowadzony w 6.7 pochodzi z wywołań tłumaczeń przed after_setup_theme.
W tym artykule zbudujemy małą wtyczkę „LW Reading Time” z jednym plikiem PHP i skryptem importującym @wordpress/i18n. Przyjrzymy się funkcjom tłumaczeń i regułom liczby mnogiej dla niemieckiego i polskiego. Pokażemy też, co WordPress 7.1.2 (aktualne wydanie na dzień 29.09.2026) ładuje samodzielnie, jak tekst trafia przez GlotPress do paczki językowej i jak lokalnie budować pliki tłumaczeń za pomocą WP-CLI. Plugin Check i pola pliku readme.txt mają w tej serii osobne artykuły; tutaj omawiamy je tylko w związku z tłumaczeniami.
Text domain: jeden identyfikator, zgodny ze slugiem
Każde wywołanie gettext przyjmuje jako ostatni argument text domain, czyli identyfikator wskazujący, do którego pliku tłumaczeń należy dany tekst. Dla wtyczek z katalogu WordPress.org reguła jest ścisła: text domain musi być identyczny ze slugiem. Wtyczka o slugu akismet używa więc text domain akismet i nie powinna mieć drugiego. Według podręcznika Meta wtyczka łamiąca tę zasadę nie zostanie poprawnie zaimportowana do translate.wordpress.org i nie dostanie paczek językowych. Dozwolone są małe litery i myślniki, bez podkreśleń i spacji.
Text domain musi też być dosłownym ciągiem znaków. wp i18n make-pot i importer na WordPress.org czytają kod źródłowy bez jego wykonywania, dlatego stała lub zmienna w miejscu 'lw-reading-time' pozostaje dla nich niewidoczna; podręcznik wtyczek wprost odradza taki skrót. Nawet teksty, które rdzeń już tłumaczy, na przykład „Settings”, dostają własny text domain wtyczki. W przeciwnym razie tracą tłumaczenie, gdy tylko rdzeń zmieni brzmienie.
Nagłówek Text Domain jest według podręcznika opcjonalny od WordPressa 4.6, a Domain Path wtyczki z katalogu mogą pominąć. Mimo to warto ustawiać oba: make-pot domyślnie odczytuje text domain z nagłówka, a od WordPressa 6.8 właśnie ta para pozwala wtyczkom spoza katalogu automatycznie ładować tłumaczenia. Plugin Check 2.1.0 (wydany 16.08.2026) zgłasza nieprawidłowy format text domain jako błąd, a nagłówek niezgodny ze slugiem jako ostrzeżenie. Ostrzeżenie pojawia się też wtedy, gdy Domain Path nie zaczyna się od ukośnika albo wskazuje nieistniejący katalog. Dlatego przykładowa wtyczka zawiera katalog languages/ z plikiem POT.
Funkcje gettext w PHP
Główny plik przykładowej wtyczki pokazuje nad pojedynczymi wpisami szacowany czas czytania i dodaje przycisk, którego etykietę później zmienia skrypt. Każdy widoczny tekst przechodzi przez funkcję tłumaczącą:
/**
* 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' );__() zwraca tłumaczenie, _e() od razu je wypisuje. Właśnie takie wypisywanie bez escapowania wytykają WordPress Coding Standards: sniff EscapeOutput traktuje _e() i _ex() jako niebezpieczne funkcje wyjścia i proponuje w zamian esc_html_e() lub esc_attr_e(). Funkcje łączone esc_html__(), esc_html_e(), esc_attr__(), esc_attr_e(), esc_html_x() i esc_attr_x() tłumaczą i escapują w jednym kroku. Tłumaczenie to wciąż tekst spoza kodu, więc traktuje się je jak każde inne wyjście. W przykładzie esc_html() obejmuje wynik sprintf(); escapowanie samego wzorca pozostawiłoby wstawioną wartość bez kontroli.
Symbole zastępcze i komentarze dla tłumaczy
Zmienne nigdy nie trafiają do tłumaczonego tekstu. Wywołanie _e( "Your city is $city." ) nie zadziała, bo ekstraktor znajdzie tekst z dosłownym $city, a WordPress w czasie działania będzie szukał tekstu z faktyczną nazwą miasta, której nie ma w żadnym pliku tłumaczeń. Poprawna forma to symbol zastępczy z printf() lub sprintf(). Przy kilku symbolach podręcznik zaleca numerowane argumenty, takie jak %1$s i %2$s, w pojedynczych cudzysłowach, aby tłumaczenie mogło zmienić ich kolejność.
Komentarz zaczynający się od translators: wyjaśnia tłumaczom, co oznacza symbol zastępczy. Musi być ostatnim komentarzem PHP przed wywołaniem gettext; make-pot przenosi go do pliku POT jako wiersz #., a GlotPress wyświetla go obok tekstu. Samą liczbę przepuszcza się przez number_format_i18n(), bo niemiecki i polski stosują inne separatory tysięcy niż angielski.
_x() dodaje kontekst dla słów o wielu znaczeniach; GlotPress traktuje parę kontekst plus tekst jako osobny wpis. Skrypt poniżej używa _x( 'Read', … ) dla stanu po kliknięciu. Bez kontekstu „Read” mogłoby zostać przetłumaczone jako czasownik „Czytaj” albo „Lesen”, a przycisk potrzebuje formy „Przeczytany” lub „Gelesen”.
Formy liczby mnogiej: dwie po angielsku i niemiecku, trzy po polsku
_n() przyjmuje liczbę pojedynczą, mnogą, liczbę elementów i text domain. Angielskiemu i niemieckiemu wystarczają dwie formy, polski potrzebuje trzech. Regułę zapisuje nagłówek Plural-Forms danego języka. GlotPress, oprogramowanie stojące za translate.wordpress.org, definiuje polski jako nplurals=3 z wyrażeniem (n == 1) ? 0 : ((n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 12 || n % 100 > 14)) ? 1 : 2); polski eksport z translate.wordpress.org ma identyczny nagłówek. Dla czasu czytania daje to:
| Minuty | Angielski | Niemiecki | Polski (forma) |
|---|---|---|---|
| 1 | Reading time: 1 minute | Lesezeit: 1 Minute | Czas czytania: 1 minuta (0) |
| 2 | Reading time: 2 minutes | Lesezeit: 2 Minuten | Czas czytania: 2 minuty (1) |
| 5 | Reading time: 5 minutes | Lesezeit: 5 Minuten | Czas czytania: 5 minut (2) |
| 12 | Reading time: 12 minutes | Lesezeit: 12 Minuten | Czas czytania: 12 minut (2) |
| 21 | Reading time: 21 minutes | Lesezeit: 21 Minuten | Czas czytania: 21 minut (2) |
| 22 | Reading time: 22 minutes | Lesezeit: 22 Minuten | Czas czytania: 22 minuty (1) |
Stąd bierze się zasada z podręcznika: również liczba pojedyncza zawiera symbol zastępczy, czyli '%s minute', a nie 'One minute', ponieważ w niektórych językach „pojedyncza” forma obejmuje także inne liczby niż jeden. Kod przekazuje tylko liczbę, a o tym, które z trzech polskich zdań się pojawi, decyduje plik tłumaczeń. Dla tekstów w liczbie mnogiej, których liczba nie jest jeszcze znana w chwili definicji, istnieją _n_noop() i translate_nooped_plural().
Kto ładuje tłumaczenia: just-in-time zamiast load_plugin_textdomain
WordPress 4.6 zmienił sposób ładowania od podstaw. Notatka dla deweloperów z lipca 2016 roku wprowadziła ładowanie just-in-time: gdy WordPress po raz pierwszy natrafi na dany text domain, _load_textdomain_just_in_time() szuka pasującego pliku w wp-content/languages i go wczytuje. Od tego czasu wtyczki z katalogu nie potrzebują load_plugin_textdomain(). W tej samej wersji odwrócono kolejność wyszukiwania: paczki językowe w wp-content/languages mają pierwszeństwo przed plikami w katalogu wtyczki.
WordPress 6.5 zmienił mechanizm tłumaczeń. Paczki językowe zawierają odtąd obok .po i .mo także plik .l10n.php. Jeśli ten plik istnieje, WordPress wczytuje go zamiast .mo. Dzięki temu ładowanie jest szybsze, zużywa mniej pamięci i korzysta z OPcache. Filtr translation_file_format pozwala wrócić do formatu mo.
WordPress 6.7 zamienił load_plugin_textdomain() w samą rejestrację: funkcja zapamiętuje tylko ścieżkę, a właściwe ładowanie zostawia mechanizmowi just-in-time. Jednocześnie pojawił się komunikat _doing_it_wrong(). Gdy tłumaczenia danej domeny zostaną zażądane przed after_setup_theme, WordPress informuje, że ładowanie uruchomiono „too early”, i wskazuje init jako najwcześniejszy właściwy moment. Według notatki tak wczesne ładowanie zmusza WordPressa do przedwczesnego ustalenia bieżącego użytkownika i odbiera innym wtyczkom możliwość filtrowania języka lub ścieżki pliku. Dla testów istotny jest szczegół z pliku l10n.php w rdzeniu: komunikat pojawia się, gdy rdzeń zna katalog językowy dla danej domeny, a nie dopiero wtedy, gdy istnieje plik tłumaczeń. Od wersji 6.8 rdzeń rejestruje dla każdej aktywnej wtyczki katalog z nagłówka Domain Path (bez nagłówka katalog wtyczki), dlatego w WordPressie 7.1.2 poniższy przykład wywołuje komunikat także na stronie po angielsku bez żadnego pliku tłumaczeń.
// 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'];
}Notatka wymienia dwie typowe przyczyny: get_plugin_data() bez $translate = false oraz klasy tworzone bezpośrednio w pliku głównym, których konstruktor wywołuje __(). Do namierzenia źródła proponuje callback na akcji doing_it_wrong_run, który dla _load_textdomain_just_in_time wypisuje backtrace, albo wtyczkę Query Monitor.
WordPress 6.8 rozszerzył ładowanie just-in-time na wtyczki spoza katalogu i korzysta przy tym z nagłówków Text Domain i Domain Path. Notatka streszcza to w dwóch przypadkach:
- Wtyczka hostowana na WordPress.org, wymagająca co najmniej WordPressa 4.6: wywołanie
load_plugin_textdomain()nie jest potrzebne. - Wtyczka z nagłówkami
Text DomainiDomain Path, wymagająca co najmniej WordPressa 6.8: wywołanie również nie jest potrzebne.
W pozostałych przypadkach, na przykład przy wtyczce rozpowszechnianej poza katalogiem i wspierającej starsze wersje, wywołanie zostaje, ale należy je przenieść do init. Plugin Check egzekwuje to dla katalogu: sniff PluginCheck.CodeAnalysis.DiscouragedFunctions.load_plugin_textdomainFound zgłasza ostrzeżenie, gdy minimalna wersja z Requires at least (nagłówek lub readme) jest wyższa niż 4.6. Komunikat dodaje, że WordPress sam ładuje tłumaczenia pod slugiem wtyczki.
JavaScript: @wordpress/i18n, budowanie i wp_set_script_translations
Od WordPressa 5.0 uchwyt skryptu wp-i18n udostępnia w przeglądarce te same funkcje: __(), _x(), _n(), _nx() i sprintf(). Przykład importuje je z pakietu npm @wordpress/i18n i buduje plik za pomocą @wordpress/scripts:
{
"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"
}
}// 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
);
} );
} );Bez pliku block.json polecenie wp-scripts build przyjmuje src/index.js jako punkt wejścia i zapisuje build/index.js. Wtyczka webpacka Dependency Extraction zastępuje import globalnym obiektem wp.i18n i tworzy build/index.asset.php z zależnością wp-i18n oraz hashem wersji; plik PHP przekazuje oba do wp_enqueue_script(). Konfiguracja webpacka zachowuje w zminifikowanym wyniku komentarze z translators:, a make-pot rozpoznaje formy wywołań generowane przez webpack.
Połączenie skryptu z tłumaczeniem tworzy funkcja wp_set_script_translations(), dostępna od WordPressa 5.0, z opcjonalnym text domain od wersji 5.1. Działa tylko dla skryptu już zarejestrowanego, rozpoznawanego po uchwycie – tym samym identyfikatorze, na którym opiera się artykuł o selektywnym wyłączaniu skryptów wtyczek. WordPress szuka wtedy pliku JSON w formacie Jed i umieszcza jego zawartość w wp.i18n, zanim skrypt się uruchomi. Nazwa pliku ma postać {domain}-{locale}-{md5}.json; hash liczony jest ze ścieżki skryptu względem katalogu wtyczki i dla build/index.js wynosi dfbff627e6c248bcb3b61d7d06da9ca9. Rdzeń przed haszowaniem zamienia .min.js na .js, a make-pot domyślnie pomija pliki *.min.js. Opcjonalny trzeci parametr wskazuje katalog, który WordPress sprawdza jako pierwszy, zarówno pod kątem {domain}-{locale}-{handle}.json, jak i nazwy z hashem, zanim sięgnie do wp-content/languages/plugins.
Dla modułów skryptów rejestrowanych przez wp_register_script_module() WordPress 7.0 dodał odpowiednik wp_set_script_module_translations(). Przykład korzysta z klasycznego skryptu, więc funkcja pojawia się tu tylko jako wzmianka.
Od kodu źródłowego do paczki językowej
Dla wtyczek z katalogu serwis WordPress.org po kolejnym commicie SVN analizuje wszystkie pliki PHP i JavaScript, a teksty importuje do translate.wordpress.org. Każda wtyczka ma tam cztery podprojekty: Development, Development Readme, Stable (latest release) i Stable Readme (latest release). Istniejące pliki .mo z wtyczki zostają przejęte jednorazowo; dalsza praca nad tłumaczeniem odbywa się już na platformie.
Tłumaczenia może zgłaszać każda osoba z kontem WordPress.org. Propozycje mają najpierw status „Waiting” i liczą się dopiero wtedy, gdy General Translation Editor danego języka albo Project Translation Editor wyznaczony dla wtyczki nada im status „Current”. Niezweryfikowane tłumaczenia maszynowe nie są przyjmowane. Pierwsza paczka językowa dla danego języka powstaje, gdy zatwierdzonych jest co najmniej 90 procent tekstów podprojektu Stable; teksty z readme się do tego progu nie wliczają. Później każda zmiana tekstów lub tłumaczeń generuje nową paczkę, nawet jeśli pokrycie znów spadnie poniżej 90 procent.
Paczka językowa zawiera pliki .po i .mo, od WordPressa 6.5 plik .l10n.php oraz po jednym pliku JSON na każdy skrypt z tekstami do tłumaczenia. WordPress instaluje ją w ramach zwykłych aktualizacji tłumaczeń w katalogu wp-content/languages/plugins/. Tam ładowanie just-in-time i wp_set_script_translations() odnajdują pliki bez dodatkowego kodu we wtyczce.
Własne pliki tłumaczeń z WP-CLI
Lokalne pliki są potrzebne dla wtyczek spoza katalogu, na czas przed pojawieniem się pierwszej paczki językowej i w projektach dla klientów z własnym słownictwem. Polecenia wp i18n obejmują cały łańcuch:
# 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 przeszukuje pliki PHP, Blade i JavaScript oraz nagłówek wtyczki, zawsze pomijając node_modules, vendor, katalogi systemów kontroli wersji i *.min.js. W przykładzie --exclude=src sprawia, że jako źródło tekstów JavaScript występuje wyłącznie zbudowany plik. make-mo kompiluje pliki binarne, polecenie make-php tworzy pliki .l10n.php i jest dostępne od WP-CLI 2.10.0 (i18n-command 2.6.0). make-json zapisuje po jednym pliku JSON w formacie Jed na każdy plik źródłowy JavaScript i domyślnie usuwa potem teksty JavaScript z pliku PO; z opcją --no-purge plik PO pozostaje nietknięty. Polski plik PO zawiera wtedy wszystkie trzy formy liczby mnogiej:
# 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"Od wersji 4.6 paczki językowe w wp-content/languages mają pierwszeństwo przed plikami we wtyczce. Jeśli wtyczka mimo to ma ładować własne tłumaczenia, podręcznik wskazuje filtr load_textdomain_mofile; od wersji 6.5 rdzeń oferuje dodatkowo load_translation_file, który otrzymuje ścieżkę zarówno do pliku .mo, jak i .l10n.php. Dla skryptów trzeci parametr wp_set_script_translations() określa katalog sprawdzany jako pierwszy.
Ograniczenia i otwarte kwestie
- Przykładową wtyczkę uruchomiono w WordPressie 7.1.2 z WP-CLI 2.12.0 i
@wordpress/scripts36.0.0; pakiet wymaga Node 22.22.2 lub nowszego, pod Node 20 npm zgłaszał jedynie ostrzeżenie. Numery wierszy w wycinku pliku PO pochodzą z tej kompilacji i przesuwają się przy każdej zmianie kodu. - Wykorzystane źródła nie dokumentują, czy importer na WordPress.org pomija katalogi takie jak
src/albo pliki zminifikowane tak samo jakmake-pot. Dla paczki językowej liczy się to, że zbudowany plik trafia do SVN. - Bez trzeciego parametru
wp_set_script_translations()w WordPressie 7.1.2 znajduje pliki JSON także w katalogu z nagłówkaDomain Path, bo rdzeń korzysta z katalogu zarejestrowanego dla domeny. Gdy wwp-content/languages/pluginsleży plik tłumaczeń dla tego samego języka, pliki JSON wtyczki zostają pominięte. _n()oczekuje liczby całkowitej; wartości ułamkowe, jak 1,5 minuty, podlegają w polszczyźnie innym regułom gramatycznym. Przykład zaokrągla w górę do pełnych minut.
Pytania i odpowiedzi
Czy wtyczka z katalogu musi dołączać własne pliki .mo?
Nie. Po imporcie do translate.wordpress.org powstają paczki językowe, które WordPress sam pobiera do wp-content/languages/plugins/. Istniejące pliki .mo zostają przejęte jednorazowo przy pierwszym imporcie; według podręcznika Meta da się je potem usunąć z paczki i z SVN.
Czy komunikat o zbyt wczesnym ładowaniu pojawia się tylko na stronie z plikiem tłumaczeń?
Nie, w WordPressie 7.1.2 nie. Kontrola w _load_textdomain_just_in_time() działa, gdy rdzeń zna katalog językowy dla domeny, a od wersji 6.8 rdzeń rejestruje dla każdej aktywnej wtyczki jej katalog lub katalog z Domain Path. Test na stronie z en_US bez żadnego pliku tłumaczeń również pokazuje więc ten komunikat. Paczki językowe pozostają potrzebne do sprawdzenia samego przetłumaczonego wyniku.
Teksty PHP są przetłumaczone, a teksty skryptu zostają po angielsku. Jaka jest zwykle przyczyna?
wp_set_script_translations()działa przed rejestracją skryptu albo z innym uchwytem.- Hash w nazwie pliku JSON nie pasuje do względnej ścieżki dostarczanego pliku, na przykład dlatego, że plik POT powstał z
src/zamiast zbuild/. - W SVN brakuje zbudowanego pliku, więc WordPress.org nie znajduje tekstów JavaScript.
Źródła
- Plugin Handbook: How to Internationalize Your Plugin (dostęp: 29.09.2026)
- Plugin Handbook: Localization (dostęp: 29.09.2026)
- Block Editor Handbook: Internationalization (dostęp: 29.09.2026)
- Code Reference: __(), _e(), _x(), _n(), esc_html__() (dostęp: 29.09.2026)
- Code Reference: load_plugin_textdomain() i wp_set_script_translations() (dostęp: 29.09.2026)
- Make WordPress Core: I18N Improvements in 4.6 (dostęp: 29.09.2026)
- Make WordPress Core: JavaScript i18n support in WordPress 5.0 (dostęp: 29.09.2026)
- Make WordPress Core: I18N Improvements in 6.5 (Performant Translations) (dostęp: 29.09.2026)
- Make WordPress Core: Internationalization improvements in 6.7 (dostęp: 29.09.2026)
- Make WordPress Core: Internationalization improvements in 6.8 (dostęp: 29.09.2026)
- Meta Handbook: Translations (dostęp: 29.09.2026)
- Polyglots Handbook: Plugin/Theme Authors Guide (dostęp: 29.09.2026)
- WP-CLI: wp i18n make-pot, make-json, make-php, make-mo (dostęp: 29.09.2026)
- wordpress-develop, gałąź 7.1: wp-includes/l10n.php i script-modules.php (dostęp: 29.09.2026)
- GlotPress: locales.php (reguły liczby mnogiej) i translate.wordpress.org: projekt Akismet, eksport polski (dostęp: 29.09.2026)
- Plugin Check 2.1.0: DiscouragedFunctionsSniff i Plugin_Header_Fields_Check (dostęp: 29.09.2026)
- @wordpress/scripts (README i konfiguracja webpacka) oraz @wordpress/i18n (dostęp: 29.09.2026)
- WordPress Coding Standards: EscapeOutputSniff (dostęp: 29.09.2026)
- WordPress Releases (dostęp: 29.09.2026)