
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 sformuowania).
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:
// 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);
}
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!
/(…)/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).
# SOME DESCRIPTIVE TITLE. Jakieś tłumaczenie czegośtam
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 2008
# This file is distributed under the same license as the PACKAGE cośtam package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR Ździchu Pędzel <zdzichoo@yahoo.com>, 2008.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION 1.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> "Language-Team: Polish\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=CHARSET UTF-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
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"
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 np. w litewskim 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