LW IT Solutions logo LW IT Solutions logo LW IT Solutions
« Przegląd bloga /Wtyczki i triki WordPress/Tutorials / Tłumaczenie wtyczek: text domain, paczki językowe i...
Ten artykuł w innych językach:

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

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

Management Summary

Na przetłumaczonej stronie WordPress także przyciski i opisy rozszerzeń powinny pasować do wybranego języka. Artykuł wyjaśnia tłumaczenie takich tekstów na przykładzie czasu czytania. Treści przygotowane na serwerze i zmieniane później w przeglądarce korzystają z osobnych dróg. Uwzględnienie tylko jednej może pozostawić na stronie mieszankę języków. Odpowiednie tłumaczenia oraz poprawna liczba pojedyncza i mnoga zapewniają spójną obsługę po polsku, niemiecku i angielsku.

Spis treści
  1. Text domain: identyfikator tekstów wtyczki
  2. PHP: tłumaczenie i bezpieczne wyświetlanie tekstu
  3. Formy liczby mnogiej: 1 minuta, 2 minuty, 5 minut
  4. Ładowanie tłumaczeń dopiero przy użyciu
  5. JavaScript: połączenie zbudowanego pliku z tłumaczeniem
  6. Odnalezienie właściwego pliku JSON
  7. Paczki językowe z WordPress.org
  8. Tworzenie lokalnych plików tłumaczeń
  9. Zakres testów i typowe błędy
  10. Powiązane artykuły i źródła

Przetłumaczony interfejs WordPressa nie tłumaczy automatycznie tekstów wtyczek. Wtyczka potrzebuje oznaczonych tekstów źródłowych i odpowiednich plików tłumaczeń. PHP i JavaScript wymagają różnych formatów plików. Obie drogi muszą działać, aby na przykład czas czytania i podpis przycisku pojawiły się w tym samym języku.

Przykład „LW Reading Time” pokazuje obie drogi. Język zależy od ustawienia wybranego przez WordPress lub rozszerzenie wielojęzyczne. Nie oznacza to automatycznego rozpoznawania preferowanego języka każdej wizyty. Przykład jest wzorem do prac deweloperskich, a nie potwierdzoną opublikowaną wtyczką.

Text domain: identyfikator tekstów wtyczki

Text domain przypisuje tekst do właściwego katalogu tłumaczeń. Dla nazwy w katalogu lw-reading-time identyfikator również brzmi lw-reading-time. Wywołania tłumaczeń zawierają ten identyfikator jako stały tekst. Zmienna utrudnia automatyczne zbieranie tekstów, ponieważ narzędzia podczas tego zbierania nie wykonują kodu PHP.

Nagłówek PHP podaje identyfikator w polu Text Domain. Domain Path: /languages wskazuje folder tłumaczeń wewnątrz wtyczki. Folder musi istnieć. Text domain korzysta z małych liter i myślników zamiast spacji i podkreśleń. Nawet zwykłe teksty, takie jak „Settings”, otrzymują identyfikator wtyczki; przypadkowo dostępne tłumaczenie WordPressa nie jest trwałą podstawą.

PHP: tłumaczenie i bezpieczne wyświetlanie tekstu

Poniższy główny plik dodaje do pojedynczych wpisów szacowany czas czytania i przycisk. Obliczenie czasu liczy fragmenty rozdzielone białymi znakami, usuwa HTML oraz shortcode i przyjmuje tempo 200 słów na minutę. To nadal przybliżenie. Nieprawidłowe UTF-8 lub brak wpisu pozostawia wynik bez zmian.

<?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|null Minutes, at least 1; null for invalid text.
 */
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 );

	if ( false === $words ) {
		return null;
	}

	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();
	if ( ! $post instanceof WP_Post ) {
		return $content;
	}
	$minutes = lw_reading_time_minutes( $post );
	if ( null === $minutes ) {
		return $content;
	}

	$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 )
	);

	// Prefer this explicit directory for local sample JSON files; core can then fall back to language packs.
	wp_set_script_translations( 'lw-reading-time', 'lw-reading-time', plugin_dir_path( __FILE__ ) . 'languages' );
}
add_action( 'wp_enqueue_scripts', 'lw_reading_time_enqueue_script' );

__() zwraca przetłumaczony tekst, a _e() od razu tekst wypisuje. Dla widocznego tekstu HTML funkcja esc_html__() dodatkowo zabezpiecza wynik: znak mniejszości jest traktowany jako tekst, a nie polecenie HTML. Odpowiednie funkcje esc_attr obsługują atrybuty HTML. Właściwe zabezpieczenie zależy od miejsca wyświetlania.

sprintf() wstawia wartości w miejsca oznaczone symbolami zastępczymi. Wzorzec pozostaje stały, na przykład Reading time: %s minute; liczba trafia do tekstu później. Przy kilku wartościach %1$s i %2$s pozwalają zmienić kolejność w tłumaczeniu. Bezpośrednio powiązany komentarz translators: wyjaśnia znaczenie symboli. Przykład zabezpiecza cały złożony tekst funkcją esc_html().

Formy liczby mnogiej: 1 minuta, 2 minuty, 5 minut

_n() otrzymuje dwa angielskie teksty źródłowe, liczbę i text domain. Plik tłumaczeń wybiera właściwą formę językową. Dla użytych tutaj liczb całkowitych angielski i niemiecki mają dwie formy gettext, a polski trzy. Polskie wyrażenie rozróżnia 1, liczby takie jak 2 i 22 oraz liczby takie jak 5, 12 i 21.

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)

Dlatego także liczba pojedyncza zawiera symbol zastępczy. Zapisane na sztywno „One minute” nie pasuje do języków, w których pierwsza forma obejmuje również inne liczby. Tutaj reguła dotyczy pełnych minut. Ułamki, takie jak 1,5 minuty, wymagają osobnego potraktowania gramatycznego.

Ładowanie tłumaczeń dopiero przy użyciu

Dla wtyczek z WordPress.org system ładuje tłumaczenia przy pierwszym użyciu od wersji 4.6. Od wersji 6.8 dotyczy to również wtyczek rozpowszechnianych poza katalogiem, z odpowiednimi nagłówkami Text Domain i Domain Path. Przy takich minimalnych wersjach dodatkowe wywołanie load_plugin_textdomain() nie jest potrzebne.

Nie oznacza to jednak, że tłumaczeń należy żądać już podczas pierwszego odczytu głównego pliku. WordPress 6.7 dodał komunikat dla deweloperów o zbyt wczesnym ładowaniu. Sprawdzony kod kontroluje, czy after_setup_theme właśnie działa lub już się wykonało; komunikat zaleca init albo późniejszy moment. Wywołanie tłumaczenia dopiero przy rzeczywistej potrzebie jest zwykle czytelniejsze niż przetłumaczona stała globalna.

<?php
// Too early: runs while WordPress loads the plugin file, before after_setup_theme and init.
// A known domain path can trigger this diagnostic even without a translation file.
// _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'];
}

Znana ścieżka tłumaczeń może uruchomić komunikat jeszcze przed otwarciem pliku językowego. Angielski test bez plików tłumaczeń nie wyklucza więc tego problemu. get_plugin_data(..., false, false) odczytuje metadane bez tłumaczenia, co jest odpowiednie na przykład dla numeru wersji.

Jawne wywołanie ładowania może być nadal potrzebne przy obsłudze starszych wersji WordPressa lub innych ścieżek. Od WordPressa 6.7 takie wywołanie rejestruje ścieżkę. WordPress ładuje plik dopiero przy pierwszym potrzebnym tekście. Późniejsza rejestracja nie naprawia żądania tłumaczenia, które wcześniej nastąpiło zbyt wcześnie.

JavaScript: połączenie zbudowanego pliku z tłumaczeniem

Przykład używa klasycznego skryptu WordPressa. Pakiet @wordpress/scripts buduje dostarczany plik build/index.js na podstawie src/index.js. Dodatkowo utworzony index.asset.php opisuje zależności, takie jak wp-i18n. Kod PHP przekazuje tę listę podczas dołączania skryptu.

{
  "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"
  },
  "engines": {
    "node": "^22.22.2 || ^24.15.0 || >=26.0.0"
  }
}

Wersja pakietu jest tutaj ustalona na 36.0.0. Wymaganie Node brzmi ^22.22.2 || ^24.15.0 || >=26.0.0: obejmuje odpowiednie zakresy 22.x i 24.x oraz wersje od 26, a nie każdą dowolną wyższą wersję główną. Plik blokady powstały podczas instalacji zapisuje także konkretne wersje zależności. Kolejne kompilacje mogą korzystać z tego pliku przez npm ci.

// 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 new Set( Array.isArray( stored ) ? stored.filter( ( id ) => Number.isSafeInteger( id ) && id > 0 ) : [] );
	} catch ( error ) {
		return new Set();
	}
}

const readPosts = getReadPosts();

function saveReadPosts() {
	try {
		window.localStorage.setItem( STORAGE_KEY, JSON.stringify( [ ...readPosts ] ) );
	} catch ( error ) {
		// The state remains available on this page only when storage fails.
	}
}

function showRead( button ) {
	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 locally known posts marked as read. */
		_n(
			'%d post marked as read locally',
			'%d posts marked as read locally',
			readPosts.size,
			'lw-reading-time'
		),
		readPosts.size
	);
}

document.querySelectorAll( '.lw-reading-time__mark' ).forEach( ( button ) => {
	const postId = Number( button.dataset.post );
	if ( ! Number.isSafeInteger( postId ) || postId <= 0 ) {
		button.disabled = true;
		return;
	}
	if ( readPosts.has( postId ) ) {
		showRead( button );
	}
	button.addEventListener( 'click', () => {
		for ( const id of getReadPosts() ) {
			readPosts.add( id );
		}
		readPosts.add( postId );
		saveReadPosts();
		showRead( button );
	} );
} );

_x() dodaje kontekst: „Read” oznacza tutaj stan „Przeczytany”, a nie polecenie rozpoczęcia czytania. Przycisk zapisuje wyłącznie lokalne oznaczenie przeczytania. Po ponownym załadowaniu strony wpisy z poprawnym zapisanym numerem odzyskują oznaczenie „Przeczytany”. Duplikaty i nieprawidłowe wartości są odrzucane.

Gdy pamięć przeglądarki jest zablokowana lub pełna, stan pozostaje dostępny tylko na bieżącej stronie. Przykład nie synchronizuje kont ani urządzeń i nie rozwiązuje w pełni jednoczesnych zapisów z kilku kart. Licznik opisuje lokalnie znane oznaczenia, a nie potwierdzone przeczytanie. Połączenie najnowszego zapisanego stanu podczas kliknięcia zmniejsza konflikty, ale nie gwarantuje zapisu bez konfliktów.

Odnalezienie właściwego pliku JSON

wp_set_script_translations() łączy zarejestrowany uchwyt skryptu z tłumaczeniami. Uchwyt to nazwa, pod którą WordPress zarządza skryptem. Wywołanie musi nastąpić po rejestracji. Przykład jawnie wybiera languages/ jako pierwsze miejsce wyszukiwania lokalnych plików JSON. Dzięki temu kolejność w tym teście jest jasna.

Dla standardowego pliku wtyczki build/index.js nazwa pliku łączy text domain, oznaczenie języka i MD5 względnej ścieżki. Ta wartość wynosi dfbff627e6c248bcb3b61d7d06da9ca9. Zmiana nazwy pliku może więc wymagać innej nazwy pliku tłumaczeń. Niestandardowe ścieżki lub filtry mogą zmieniać wyszukiwanie.

Sprawdzony kod WordPressa najpierw szuka w jawnym folderze nazwy zawierającej uchwyt, a potem nazwy ze skrótem ścieżki. Następnie sprawdza odpowiedni ogólny folder językowy. Bez jawnej ścieżki WordPress najpierw używa folderu ustalonego dla text domain. Dlatego pliki językowe PHP i pliki JSON JavaScript wymagają osobnej kontroli.

Paczki językowe z WordPress.org

translate.wordpress.org zbiera teksty z opublikowanego kodu wtyczki. Pierwsza paczka językowa wymaga przetłumaczenia i zatwierdzenia co najmniej 90 procent odpowiednich tekstów kodu. Readme nie wlicza się do tego udziału. Gdy paczka już istnieje, kolejne zmiany mogą uruchamiać tworzenie nowych paczek także po spadku udziału poniżej 90 procent.

Propozycja nie jest jeszcze zatwierdzonym tłumaczeniem. Wpisy sprawdzają odpowiednie redakcje językowe lub projektowe. Paczka dostarcza pliki PHP oraz wymagane pliki JSON skryptów. WordPress zapisuje paczki wtyczek pod wp-content/languages/plugins/. Sama funkcja tłumacząca nie gwarantuje ani gotowej paczki, ani instalacji tłumaczeń.

Pliki językowe PHP i JavaScript są osobne; oba wyniki wymagają kontroli.
Pliki językowe PHP i JavaScript są osobne; oba wyniki wymagają kontroli.

Tworzenie lokalnych plików tłumaczeń

Plik POT jest listą tekstów źródłowych do przetłumaczenia. Plik PO dodaje tłumaczenia dla jednego języka. Z tego edytowalnego pliku powstają pliki używane podczas działania: MO, opcjonalnie .l10n.php oraz JSON. Od WordPressa 6.5 system może używać pliku tłumaczeń PHP zamiast MO.

# Run in a separate plugin development directory, not on the live site.
# The JSON command below follows i18n-command 3.0.1 (requires WP-CLI 3.x).
# Older command versions may modify PO input; keep a separate source backup.

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

# Keep package-lock.json after installation; use npm ci for later builds.

# 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

# 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")

Krok tworzenia JSON odnosi się do i18n-command 3.0.1, który wymaga WP-CLI 3.x. Ta wersja nie ma opcji --no-purge i nie przepisuje pliku PO podczas eksportu JSON. Starsze instrukcje opisują inne zachowanie. Zainstalowana wersja i pomoc polecenia muszą więc odpowiadać instrukcji; edytowalne pliki PO nadal wymagają kopii zapasowej lub kontroli wersji.

--exclude=src zapobiega w tym przykładzie pobieraniu tych samych tekstów z pliku źródłowego i zbudowanego z różnymi ścieżkami. Odwołania do źródeł w PO muszą pasować do dostarczanego pliku JavaScript. Poniższy polski przykład pokazuje trzy formy liczby mnogiej; numery wierszy są poglądowe i zmieniają się podczas edycji.

# 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 locally known posts marked as read.
#: build/index.js:2
msgid "%d post marked as read locally"
msgid_plural "%d posts marked as read locally"
msgstr[0] "%d wpis oznaczony jako przeczytany lokalnie"
msgstr[1] "%d wpisy oznaczone jako przeczytane lokalnie"
msgstr[2] "%d wpisów oznaczonych jako przeczytane lokalnie"

Zakres testów i typowe błędy

Dziewięć izolowanych testów PHP obejmuje między innymi niemieckie i polskie znaki, spacje nierozdzielające, nieprawidłowe UTF-8 i brak wpisu. Osiemnaście testów JavaScript sprawdza zapisane oznaczenia, nieprawidłowe wartości, powtórzone kliknięcia i zablokowaną pamięć. Funkcje WordPressa, tłumaczenia i obiekty przeglądarki zastąpiono obiektami testowymi. Nie dowodzi to pełnego uruchomienia WordPressa, kompilacji npm ani importu paczki językowej.

PHP jest przetłumaczone, ale JavaScript pozostaje angielski. Co wymaga kontroli?

Uchwyt skryptu, rejestracja, nazwa JSON, oznaczenie języka i ścieżka muszą być zgodne. Odwołanie PO do src zamiast build może dać niewłaściwy skrót ścieżki. Przetłumaczone wpisy i dostarczany plik po kompilacji również muszą istnieć.

Czy wtyczki z katalogu muszą dołączać pliki językowe?

Po udostępnieniu tłumaczeń i wygenerowaniu paczki WordPress może sam pobrać pliki. Lokalne pliki nadal są przydatne przed pierwszą paczką albo przy dystrybucji poza katalogiem. Brakujące tłumaczenie pozostawia tekst źródłowy.

Czy wystarczy angielski test bez komunikatu błędu?

Nie. Potrzebne są także prawdziwe pliki niemieckie i polskie, kontrola liczby pojedynczej i mnogiej oraz wyników PHP i JavaScript. Przebieg bez błędu nie dowodzi załadowania właściwego tłumaczenia.

Powiązane artykuły i źródła

Lukas Wójcik

Lukas Wójcik

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

Masz pytanie lub pomysł na projekt internetowy? Napisz krótko, o co chodzi. Formularz korzysta z reCAPTCHA do ochrony przed spamem.

Napisanie komentarza

Doświadczenia z innymi wtyczkami lub środowiskami hostingowymi oraz pytania o konfigurację są tu mile widziane.

Adres e-mail nie jest publikowany. Pola obowiązkowe oznaczono gwiazdką.