Computer Keyboard - Głównie JavaScript

Głównie JavaScript

rss feed icon

by

Tłumaczenie aplikacji w PHP za pomocą gettext

dwa znaki chiński oraz litera A ze strzałkami
Originał Ætoms, źródło Wikimedia Commons licencja CC-BY-SA

Internacjonalizacja, czyli po angielsku Internationalization, w skrócie i18n to dodawanie do aplikacji obsługi wielu języków. Ostatnio szukając czegoś na temat gettext, czyli biblioteki do obsługi wielu języków znalazłem artykuł na Wikipedii. Był on niekompletny, ponieważ nie zawierał opisu jak dodać tłumaczenie z liczebnikami, oczywiście edytowałem wpis i dodałem odpowiednie informacje. W związku z tym postanowiłem napisać wpis o getext w PHP, ponieważ w tym języku ostatnio pisałem aplikacje.

Pierwsza wersja biblioteki gettext została napisana w języku C i jest częścią projektu GNU. Czyli wolnej wersji unixa, dzięki której mamy dzisiaj dystrybucje Linuxa, które często nazywane są GNU/Linux. Ponieważ jest to system GNU + jądro Linux.

Gettext dostępny jest w wielu językach programowania, nawet w JavaScript-cie, przykładem jest np. angular-gettext do biblioteki AngularJS.

Gettext operuje na plikach z rozszerzeniem .po, które zawierają tłumaczenie. Narzędzie zawiera program, który potrafi wyciągnąć z kodu źródłowego różnych języków wywołanie funkcji _, która jest aliasem funkcji gettext. Nie będę jednak opisywał jak generować plik dla oryginalnego języka. Jeśli jesteś tym zainteresowany możesz przeczytać o tym w dokumentacji gettext.

Plik z tłumaczeniem

Oto przykład składni pliki .po:

msgid ""
msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: pl_PL\n"
"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"

msgid "week"
msgid_plural "weeks"
msgstr[0] "tydzień"
msgstr[1] "tygodnie"
msgstr[2] "tygodni"

W powyższym pliku mamy nagłówek, który zawiera jak w języku polskim wygląda liczba mnoga. Mamy 3 warianty oraz wyrażenie, dla których liczb ma być jaki wariant. Dla różnych języków są różne warianty np. dla języka angielskiego mamy tylko dwa z końcówkę s na końcu lub bez. msgid to słowo do tłumaczenia w języku angielskim. Natomiast msgid_plural to liczba mnoga. W przypadku zwykłych słów, których nie używamy przy liczebnikach np. jeśli mamy:

<label>user</label>

I chcemy go przetłumaczyć, aby mieć dwa języku Angielski i Polski to wystarczy:

msgid "user"
msgstr "użytkownik"

Plik .po z tłumaczeniem powien znajdować się w katalogu locale/pl_PL/LC_MESSAGES/ zamiast locale może być inny katalog np. lang albo i18n, ale wewnątrz muszą być katalogi dla poszczególnych języków. Najlepiej jak język jest w formacie locale dla standardu POSIX, czyli tego w systemach GNU/Linux. Nie próbowałem innego formatu i nie wiem czy działa.

Kompilacja

Jak mamy już przygotowany plik .po musimy wygenerować jego binarny odpowiednik, czyli plik .mo. Do tego służy program msgfmt, aby go wywołać należy wykonać takie polecenie, testowałem tylko z systemem GNU/Linux.

msgfmt site.po -o site.mo

jeśli znajdujemy się w którymś z katalogów LC_MESSAGES i jest w nim plik site.po wygenerowany zostanie plik site.mo.

Interfejs PHP

Teraz jak już mamy plik .mo możemy skonfigurować PHP, aby można było używać tej biblioteki. Kod inicjujący wygląda tak.

Najpierw musimy zmienić zmienne środowiskowe:

$lang = 'pl_PL';
$locale = $lang . ".utf8";
putenv("LC_ALL=$lang");
setlocale(LC_ALL, $lang);

Następnie ładujemy pliki, ja używam funkcji, która załaduje wszystkie dla danego języka. Ale można się ograniczyć tylko do tego, którego aktualnie będziemy używać.

function load_gettext_domains($root, $lang) {
    if (!preg_match("%" . DIRECTORY_SEPARATOR . "$%", $root)) {
        $root .= DIRECTORY_SEPARATOR;
    }
    $path = $root . DIRECTORY_SEPARATOR .
            $lang . DIRECTORY_SEPARATOR . "LC_MESSAGES";
    if (file_exists($path)) {
        foreach (scandir($path) as $file) {
            if (preg_match("/(.*)\.mo$/", $file, $match)) {
                bindtextdomain($match[1], $root);
            }
        }
    }
}

I wywołuję ją w ten sposób:

$root = __DIR__ . DIRECTORY_SEPARATOR
load_gettext_domains($root . "locale", $lang);

Jak mamy już załadowane wszystkie pliki (ja mam to w konstruktorze głównej klasy aplikacji) w ruterze aplikacji można określić, którego pliku będziemy używali. Robi się to w ten sposób:

textdomain("site")

Dzięki temu możesz np. mieć jeden plik admin.po z tłumaczeniami dla panelu administracyjnego, a drugi do strony głównej. Możesz mieć też mieć tak, że każdy url korzysta z innego pliku. Nic nie szkodzi jednak na przeszkodzie, aby mieć jeden plik z wszystkimi tłumaczeniami, i aby wywołanie textdomain także było w konstruktorze, jeśli takowego używasz.

Tłumaczenie ciągów znaków

Teraz główna część czyli tłumaczenie stringów. Aby przetłumaczyć zwykły tekst można użyć funkcji _ np.

echo _("week");

Funkcja _ jest to alias do funkcji gettext, drugą użyteczną funkcją jest ngettext, której użycie jest trochę bardziej skomplikowane. Przekazuje się do niej wersje liczby pojedynczej mnogiej oraz liczbę:

$n = 5;
echo "to było $n " . ngettext("week", "weeks", $n) . " temu";

Wewnątrz tłumaczonych znaków można też używać znaku %s, który zastępuje jakąś zmienną. Można ich używać z funkcją sprintf, np.:

$n = 5;
echo sprinf(ngettext("it was %s week ago", "it was %s weeks ago", $n), $n);

Podwójne wywołanie można zastąpić funkcją:

function _n($single, $plural, $count) {
    return sprinf(ngettext($single, $plural, $count), $count);
}

Dzięki temu, że funkcja sprintf nie zwraca błędu, gdy jest argument, a nie ma znacznika w ciągu znaków, można tej funkcji używać także zamiast zwykłego ngettext. Ale już gdy trzeba dodatkowo np. wstawić imię użytkownika, trzeba będzie użyć kombinacji obu funkcji.

Natomiast nasz plik .po będzie wyglądał tak:

msgid "it was %s week ago"
msgid_plural "it was %s weeks ago"
msgstr[0] "to było %s tydzień temu"
msgstr[1] "to było %s tygodni temu"
msgstr[2] "to było %s tygodnie temu"

Przeładowywanie pliku po kompilacji

Jedyny problem z gettext w PHP, jest to, że po wygenerowaniu pliku .mo należy zrestartować serwer apache. Nie wiem jak jest z innymi serwerami. Jeśli chcesz się dowiedzieć jak wyczyścić cache bez przeładowywania strony możesz sprawdzić na StackOverflow odpowiedź na pytanie: How to clear php’s gettext cache without restart Apache nor change domain?.

W moim przypadku dodanie wywołanie clearstatcache(); przed inicjacją gettext pomogło. Ale czasami, jak odświeża się cache po kompilacji, serwer zwraca 503, ale może to nie ma związku.

Podsumowanie

Gettext jest bardzo użytecznym narzędziem. A po skonfigurowaniu jego używanie jest bardzo proste. Przy generowaniu plików z kodu źródłowego, czego jeszcze nie robiłem, można pisać całą aplikacje w języku angielskim, dodając wywołania _ oraz sprintf(ngettext( i pod po jakimś skończonym etapie, generować plik .po. Można też rozdzielić pracę programistyczną od tłumaczenia. Może to robić inna osoba, np. po zakończeniu pisania kolejnej wersji. Tak to np. jest zrealizowane w programie Open Source Claws-Mail, gdzie jestem tłumaczem na język Polski.

źródło strony (aby zobaczyć kod na githubie musisz kliknąć przycisk raw)