Tłumaczenie skryptów PHP

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&nbsp;file.txt&nbsp;nie&nbsp;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 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> 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

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

Ten wpis został opublikowany w kategorii Informatyka i oznaczony tagami , , , , . Dodaj zakładkę do bezpośredniego odnośnika.

Możliwość komentowania jest wyłączona.