Co to jest GetText?
GetText jest darmową biblioteką programistyczną i zestawem narzędzi do tworzenia tłumaczeń programów. Tłumaczenie polega na odszukaniu w pliku odpowiadającym danemu językowi odpowiednika tłumaczenia słowa podanego jako parametr funkcji, bez stosowania identyfikatorów zwrotów, ani dołączania całego pliku z tłumaczeniami do programu w formie tablicy. Jest więc to rozwiązanie nieco lepsze od różnych prostych systemów tłumaczeń własnego autorstwa. Ponadto działa nawet bez pliku z tłumaczeniem (funkcja gettext() zwraca wtedy tekst podany jako parametr — zwykle angielski odpowiednik danego sformułowania).
Instalacja modułu GetText
Abyśmy w ogóle mogli skorzystać z funkcji gettext() musimy mieć w PHP zainstalowany moduł GetText. PHP należy w tym celu skompilować z flagą –with-gettext, chyba że instalujemy je z użyciem instalatora — wtedy wystarczy zaznaczyć odpowiedni moduł podczas instalacji.
Poniższy kod pozwala sprawdzić czy moduł jest zainstalowany:
if (function_exists("gettext")) { echo "Gettext jest zainstalowany."; } else { echo "Gettext nie jest zainstalowany."; }
Możemy również sprawdzić czy phpinfo() wyświetla tekst „GetText Support enabled”.
Do tworzenia używanych przez moduł PHP plików *.mo (Machine Object) i pośrednich plików *.po (Portable Object) niezbędny będzie również program GNU GetText. Pod Linuksem jego instalacja nie powinna sprawić problemu (trzeba poszukać go w menadżerze pakietów swojej dystrybucji), wersję dla Windows znajdziemy natomiast pod adresem:
Zaczynamy zabawę z GetTextem
// Funkcja inicjalizująca gettext (ustawia domenę, język i kodowanie znaków) function GetTextInit($domain, $dir, $locale, $encoding='UTF-8') { // Stała NO_GETTEXT jest zdefiniowana jeżeli PHP nie ma zainstalowanego // modułu Gettext, a funkcja została wywołana po raz kolejny i musimy // wykryć, że funkcja gettext() istnieje tylko dlatego, że ją wcześniej // zdefiniowaliśmy. Bez tej stałej przy kolejnych wywołaniach otrzymywali // byśmy błąd spowodowany próbą ponownego zdefiniowania tej samej funkcji. if (defined('NO_GETTEXT')) return; // Poniższy blok wykona się jeśli nie istnieje funkcja gettext() — to znaczy // jeśli moguł Gettext nie jest zainstalowany. Skrypt będzie wtedy zawsze // korzystał z tekstów wpisanych w kodzie (dzięki zdefiniowanym tutaj funkcjom). if (!function_exists("gettext")) { function _($s) { return $s; } function gettext($s) { return $s; } define('NO_GETTEXT', 1); return; } // Ustawiamy język (np. pl_PL). // Kod języka składa się z 2 znaków definiujących język (np. pl, en, de, ru) // i 2 znaków definiujących kraj (np. PL, US, GB, DE, RU). setlocale(LC_ALL, $locale); putenv("LANGUAGE=$locale"); // Przypisujemy domenie katalog z tłumaczeniami bindtextdomain($domain, $dir); // Określamy kodowanie znaków z jakim chcemy otrzymywać tłumaczenia bind_textdomain_codeset($domain, $encoding); // I wybieramy domenę jako domyślną textdomain($domain); } // gettext() z sprintf() — aby uczynić takie wywołania prostszymi function _f() { $args = func_get_args(); return vsprintf(gettext($args[0]), array_slice($args, 1)); } // Konwertuje spacje na — aby było to prostsze w miejscach gdzie // nie chcemy przekazywać łańcuchów z do funkcji gettext(). function n($s) { return str_replace(' ', ' ', $s); }
Użycie funkcji gettext() w programie
Podstawową funkcją jest gettext(), zwracająca tłumaczenie tekstu podanego jako parametr lub sam ten tekst, w wypadku braku tłumaczenia. Ponadto mamy do dyspozycji kilka funkcji ułatwiających korzystanie z GetTexta. Pierwsza z nich, _(), wchodzi w skład samej biblioteki, dwie inne zostały zdefiniowane przez nas w podanym wcześniej kodzie.
_f() łączy w sobie funkcje gettext() i sprintf(), przez co jest użyteczna wszędzie tam gdzie potrzebujemy umieścić w niekoniecznie znanym miejscu przetłumaczonego tekstu jakieś inne łańcuchy (np. nazwy plików).
Drugą zdefiniowaną przez nas funkcją jest n() która służy do szybkiego zamienienia wszystkich spacji na niełamliwe ( ). Jest to przydatne np. przy tłumaczeniu menu, gdy nie chcemy aby kolejne słowa były przenoszone do nowej linii (np. z powodu zbyt niskiej rozdzielczości), ale jednocześnie nie chcemy mieć sekwencji ucieczki HTML w pliku z tłumaczeniem.
Przykład użycia opisanych funkcji:
$filename = "file.txt"; try { throw new Exception(_f('File "%s" does not exist!', $filename)); } catch (Exception $e) { die(_("Error") . "<br>\n" . n($e->getMessage())); } # Dla poniższych tłumaczeń w pliku *.po #: index.php:4 msgid "File \"%s\" does not exist!" msgstr "Plik „%s” nie istnieje!" #: index.php:6 msgid "Error" msgstr "Błąd" # Uzyskamy komunikat: Błąd<br> Plik „file.txt” nie istnieje!
Pliki z tłumaczeniami
Struktura katalogu z tłumaczeniami:
/(…)/katalog/pl_PL/LC_MESSAGES/ /(…)/katalog/en_US/LC_MESSAGES/ /(…)/katalog/de_DE/LC_MESSAGES/ itp.
Pusty plik tłumaczenia (*.po), z wszystkimi słowami użytymi jako parametry funkcji gettext() oraz _() (ale nie _f() !) generujemy komendą:
xgettext -n nazwa_pliku.php -o nazwa_pliku.po
Plik musi mieć rozszerzenie PHP — inaczej program nie rozpozna języka programowania (a że domyślnym jest C, to nie utworzy pliku z tłumaczeniem).
Tworzenie tłumaczeń
#SOME DESCRIPTIVE TITLE.Jakieś tłumaczenie czegośtam # Copyright (C)YEAR THE PACKAGE'S COPYRIGHT HOLDER2008 # This file is distributed under the same license as thePACKAGEcośtam package. #FIRST AUTHOR <EMAIL@ADDRESS>, YEARŹdzichu Pędzel <zdzichoo@yahoo.com>, 2008. # #, fuzzy msgid "" msgstr "" "Project-Id-Version:PACKAGE VERSION1.0\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2008-02-08 00:11+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator:FULL NAME <EMAIL@ADDRESS>Ździchu Pędzel <zdzichoo@yahoo.com>\n" "Language-Team:LANGUAGE <LL@li.org>Polish\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=CHARSETUTF-8\n" "Content-Transfer-Encoding: 8bit\n" #: index.php:4 msgid "File \"%s\" does not exist!" msgstr "Plik „%s” nie istnieje!" #: index.php:6 msgid "Error" msgstr "Błąd"
Oczywiście jeśli program nie zaindeksował jakiegoś słowa (np. dlatego że użyliśmy go jako parametru zdefiniowanej przez siebie funkcji, a nie _() ), to możemy je dopisać sami.
Po przetłumaczeniu pliku *.po umieszczamy go w katalogu odpowiadającym danemu językowi, a następnie generujemy na jego podstawie używany przez GetText plik *.mo:
msgfmt nazwa_pliku.po -o nazwa_pliku.mo
Kilka uwag odnośnie gramatyki
Gramatyki róznych języków różnią się między sobą i te same słowa często należą do innych kategorii gramatycznych. Z tego względu jeżeli przekazujemy do funkcji _() zdania, których podmiot jest domyślny, czy po prostu pojedyncze słowa odwołujące się do kontekstu (np. _(„next”)), musimy uważać aby nie popełnić błędu który popełnili swego czasu na przykład programiści Phorum.
Vide:
// Powiedzmy, że mamy taki kod wyświetlający menu nawigacyjne: echo "<b>" . _("Page") . ":</b> " . _("Next") . " • " . _("Previous") . "<br>"; echo "<b>" . _("Thread") . ":</b> " . _("Next") . " • " . _("Previous") . "<br>"; // W wersji angielskiej wszystko wygląda ładnie: Page: Next • Previous Thread: Next • Previous // Polski translator dokonuje tłumaczenia: #: menu.php:1 msgid "Page" msgstr "Strona" #: menu.php:2 msgid "Thread" msgstr "Wątek" #: menu.php:1 msgid "Next" msgstr "Następna" #: menu.php:1 msgid "Previous" msgstr "Poprzednia" // W ten sposób uzyskujemy wersję polską nie przystającą do polskiej gramatyki: Strona: Następna • Poprzednia Wątek: Następna • Poprzednia // Zamiast: Strona: Następna • Poprzednia Wątek: Następny • Poprzedni
A lekarstwo jest przecież banalnie proste:
echo _("Next page"); /(…)/en_UK/LC_MESSAGES/menu.po #: menu.php:1 msgid "Next page" msgstr "Next" /(…)/pl_PL/LC_MESSAGES/menu.po #: menu.php:1 msgid "Next page" msgstr "Następna"
Co więcej — liczby
Podobny problem pojawia się gdy chcemy wyświetlić komunikat zależny od liczby jakichś obiektów — np. „Brakuje %i plików”. Problem w tym, że nie dość, że różne języki mają różne liczby (w polskim akurat, poza podwójnymi oczami i uszami, mamy tylko dwie: l.p. i l.mn., ale są języki, w których liczba podwójna nadal ma się dobrze), to dodatkowo nie wszystkie liczebniki łączą się z tym samym przypadkiem (vide: trzy koty, pięć kotów).
Proste wywołania takie jak poniższe są więc nieskuteczne:
echo $i . " " . (($i==1) ? _("file") : _("files"));
Aby zaradzić tej trudności GetText oferuje funkcję ngettext($singular, $plural, $n), która przyjmuje jako parametr nie tylko formę l.p. i l.mn., ale również liczbę określającą ilość, co pozwala na tworzenie nawet skomplikowanych zależności tłumaczenia od liczby obiektów.
Trochę przekolorowany przykład dla języka polskiego:
// PHP for ($i=0; $i<30; ++$i) { echo $i . " " . ngettext("eye", "eyes", $i) . "<br>\n"; } // *.po "Plural-Forms: nplurals=5; plural=(n==1 ? 0 : n==2 ? 1 : n%10>=2 && n%10<=4 \ && (n%100<10 || n%100>=20) ? 2 : 3);\n" #: dualis.php:2 msgid "eye" msgid_plural "eyes" msgstr[0] "oko" msgstr[1] "oczu" msgstr[2] "oka" msgstr[3] "ok" // Rezultat: 0 ok 1 oko 2 oczu 3 oka 4 oka 5 ok 6 ok 7 ok 8 ok 9 ok 10 ok 11 ok 12 ok 13 ok 14 ok 15 ok 16 ok 17 ok 18 ok 19 ok 20 ok 21 ok 22 oka 23 oka 24 oka 25 ok 26 ok 27 ok 28 ok 29 ok
Uwagi końcowe
- Do funkcji _() lepiej przekazywać komunikaty po angielsku — ułatwi to pracę tłumaczom nie znającym polskiego.
- Na ile to możliwe używaj powszechnie znanych tłumaczeń angielskich terminów. Możesz w tym celu posiłkować się np. słownikiem przygotowanym przez zespół Projektu Tłumaczenia Manuali.
- Pracę z plikami *.po możesz sobie ułatwić korzystając ze specjalnych programów — np. KBabel (z KDE).
- Kiedy już oswoisz się z informacjami z tego artykułu przeczytaj również oficjalną dukumentację.
Opisuje ona wiele nieopisanych tu funkcji pakietu GetText.
Linki
- „Gettext w PHP” — na php.rk.edu.pl
- „Współpraca z getext” — rozdział dokumentacji PHPTAL
- „PHP: Gettext” — oficjalna dokumentacja PHP
- Language Codes — lista kodów języków w oryginalnej (angielskiej) dokumentacji biblioteki GetText
- Country Codes — lista kodów krajów w oryginalnej (angielskiej) dokumentacji biblioteki GetText