Zenedith's dev blog

Dałem się namówić.., szatanowi chyba..

Gettext – tłumaczenia językowe w php5 dla Debiana i Windows.

Gettext to potężne i wydajne narzędzie do tworzenia wielojęzycznych (internacjonalizacji) tłumaczeń. Nie dotyczy to tylko aplikacji www tworzonych w php, lecz przede wszystkim aplikacji systemowych, czego przykładem jest katalog /usr/share/locale/ w systemach Linux.

Idea tłumaczeń z użyciem Gettext polega na sparsowaniu plików dla aplikacji/strony www, w którym teksty do tłumaczeń będą w jakiś szczególny sposób oznaczony. W php takie teksty oznaczane są następująco:

  • gettext(„A message to translate”) – oznacza szukanie tekstu zastępczego dla wyrażenia „A message to translate”,
  • _(„A message to translate”) – to skrócony zapis dla gettext(),
  • dgettext(„domyslna_domena”,”A message to translate”) – szuka tekstu tłumaczenia w konkretnym pliku (tzw. domenie),
  • ngettext(„File”, „Files”, $number) – zwraca odpowiednią formę mnogą dla wyrażenia, zależną od $number (liczba dodatnia).

Są to najczęściej używane zwroty stosowane do tłumaczenia całych wyrażeń lub poszczególnych wyrazów i ich automatycznej odmiany. Zaczynamy od początku.

Żeby wogóle coś wygenerować potrzebujemy aplikacji Gettext.

Na systemie Debian wystarcz zainstalować pakiet gettext (aptitiude install gettext) i nie ma w tym nic trudnego.

Dla Windows narzędzie te znajdziemy pod adresem http://gnuwin32.sourceforge.net/packages/gettext.htm. Mamy tu do wyboru instalator (Complete package, except sources) lub „luźne” pliki w postaci Binaries i Dependencies. Ponieważ użyłem drugiego sposobu to dodam, że potrzebne są wyłącznie pliki z obu katalogów bin\ więc możemy utworzyć katalog gettext i wypakować tam wszystkie pliki z katalogów bin\ oraz należy go dodać do zmiennej systemowej PATH (mówiłem już o tym wcześniej).

Czas na praktyczne zastosowanie w php i różnice pomiędzy systemami Windows a Debianem – oto prosty przykład:

<?php

    $language_code = 'pl_PL';
    putenv("LANG=$language_code");
    echo 'set locale: '. setlocale(LC_ALL, $language_code);

    $domain = 'wiadomosci';
    bindtextdomain($domain, getcwd()."/locale");
    bindtextdomain("compiz", "/usr/share/locale/");
    bind_textdomain_codeset($domain, 'utf-8');
    textdomain($domain);

?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html>
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  <title>Gettext App</title>
</head>
<body>

    <h1>Wybrane ustawienie językowe: <?php echo $language_code; ?></h1> 

<?php
    echo _("A message to translate") . "<br />\n";
    echo gettext("message2") . "<br />\n"; 

    //użycie textdomain do zmiany aktywnego pliku tłumaczeń
    //textdomain("compiz");
    //echo gettext("Acceleration") . "<br />\n";  

    //lub użycie metody, która pobiera nazwę domeny jako pierwszy parametr
    echo dgettext("compiz","Acceleration") . "<br />\n";  

    $number = 1;
    echo 'We have '.$number.' '.ngettext("File", "Files", $number).' in current dir.';

?>
</body>
</html>

Na początku definiujemy ustawienia językowe, dla których ma zostać pobrane tłumaczenie : $language_code = ‚pl_PL’;

Kolejna linia: putenv(„LANG=$language_code”); ustawia zmienną środowiskową LANG na wybrany język i jest to konieczne tylko dla Windows.

Następna linia: setlocale(LC_ALL, $language_code); jest z kolei konieczna dla Debiana, lecz niekoniecznie dla Windows a jej działanie opiera się na zmianie wielu ustawień językowych w zależności od wybranych ustawień. Wpływa m.in. na ustawienia stałych LC_COLLATE, LC_CTYPE, LC_MONETARY, LC_NUMERIC i LC_TIME, czyli sposób podawania wartości zmiennoprzecinkowych, czasu, symbolu monetarnego oraz ustawień językowych. Nadrzędnym ustawieniem dla stałej LC_ALL przy badaniu ustawień przez gettext jest stała LC_MESSAGES lecz stała ta jest dostępna tylko pod Linux’em (można ją sprawdzić w razie problemów na innych systemach unix’owych).

Ważne jest, żeby sprawdzić (dla Debiana) czy zwracana wartość nie jest czasem false. Oznacza to bowiem, że w systemie nie można ustawić wybranych ustawień językowych. I tutaj zaczynają się schody, ponieważ w cale nie łatwo jest się zorientować, że chodzi tutaj o ustawienia systemu, a więc dostępne w systemie Debian „locale”. Nie dość tego, wartość ta musi być ustawiona dokładnie na taką, jaka widnieje w systemie.

Wpisując w konsoli polecenie: locale, dostaniemy informacje o aktualnie aktywnych ustawieniach językowych w systemie Debian. Można zauważyć, że nie jest to ‚pl_PL‚ lecz ‚pl_PL.UTF-8‚, więc taka wartość jest poprawna dla systemu Debian, chyba że ustawimy inaczej. Okazuje się również, że jest to jedyna zainstalowana „lokalizacja” w systemie (przynajmniej gdy instalujemy system z ustawieniami domyślnymi dla Polski. Jeśli więc zapragniemy dodać tłumaczenie dla innego języka (en_US, pt_PT, nl_NL) to nie otrzymamy żadnego efektu po wykonaniu skryptu (z wcześniej wygenerowanym plikiem tłumaczenia).

Musimy więc dodać języki które chcemy używać (lub po prostu wszystkie). Mamy dwie możliwości:

  1. polecenie: dpkg-reconfigure locales czyli użycie graficznej rekonfiguracji ustawień językowych i zaznaczenie na liście potrzebnych języków,
  2. ręczną zmianę zawartości pliku /etc/locale.gen czyli odkomentowanie potrzebnych języków i zastosowanie zmian w systemie poleceniem: locale-gen

W ten sposób rozwiążemy problem istnienia w systemie potrzebnych ustawień językowych, jednak nadal problemem ( w sensie przenośności między systemami ) pozostanie przymus stosowania nazw dla ustawień językowych z pliku /etc/locale.gen. Możemy jednak dodać zgodne ze standardem ISO wpisy typu ‚pl_PL‚ tak: pl_PL UTF-8. Wówczas nazwa ‚pl_PL‚ będzie odpowiadała ‚pl_PL.UTF-8‚ a więc bez problemu będziemy mogli używać standaryzowanych oznaczeń językowych uruchamiając skrypty pod Windows i pod Debianem.

Linia: $domain = ‚wiadomosci’; ustawia nazwę domeny dla tłumaczenia. Jest to po prostu nazwa pliku z tłumaczeniem dla danej strony, aplikacji, itp. Jeśli więc będziemy generować (za moment) tłumaczenie dla tej strony www, to gettext będzie poszukiwał pliku o takiej nazwie. Możliwe jest korzystanie z wielu plików tłumaczeń (np. możemy tworzyć aplikację na bazie innej, jako jej rozszerzenie lub po prostu wykorzystać gotowe tłumaczenia dla nas dostępne), więc nazwa tego tłumaczenia powinna być powiązana z nazwą naszej aplikacji lub strony www. W naszym przypadku pliki tłumaczeń (dla każdego języka który będziemy chcieli uwzględnić) będzie się nazywał ‚wiadomosci’ z rozszerzeniem .mo, do czego za chwilę dojdziemy.

Następne dwie linie powodują skojarzenie używanych plików tłumaczeń (domen) z ich lokalizacją w systemie:

bindtextdomain($domain, getcwd().„/locale”);

bindtextdomain(„compiz”, „/usr/share/locale/”);

Mówimy systemowi, że naszego pliku tłumaczeń (domeny) ma szukać w pod katalogu locale/ naszej strony www. Użyłem tutaj zapisu z użyciem getcwd() która zwraca całą ścieżkę do pliku oraz dodałem do niej podkatalog locale/. Równie dobrze może to być zapisane w następujący sposób:

bindtextdomain($domain,„./locale”); (proszę zwrócić uwagę na kropkę przed /locale).

W drugiej linii natomiast ładujemy inną domenę, która jest dostępna w systemie (u mnie jest to np. compiz więc właśnie taką wybrałem – dostępne pliki domen (*.mo) można znaleźć w katalogu /usr/share/locale/pl_PL/, a teksty które są tłumaczone przez ich podgląd).

Domyślnie kodowanie wczytywanych plików może być różne od tego, które rzeczywiście będzie zawierał plik. Dlatego warto je ustawić na poprawne przez wywołanie:

bind_textdomain_codeset($domain, utf-8);

gdzie utf-8 może być dowolną stroną kodową użytą przez plik tłumaczenia. Nie muszę chyba dodawać, że musi być ono również zgodne z kodowaniem strony www:

<meta http-equiv=„Content-Type” content=„text/html; charset=utf-8” />

Ważna uwaga na temat zmiany bind_textdomain_codeset należy zrestartować serwer apache’a przy jego zmianie, ponieważ z uwagi na cache;owanie, zmiany nie zawsze mogą być natychmiast widoczne. Ta sama uwaga tyczy się aktualizacji plików tłumaczeń (*.mo).

Ostatnia linia jakoby inicjalizacji ustawień dla gettexta:

textdomain($domain);

powoduje ustawienie aktywnego/bieżącego pliku tłumaczeń (domeny) dla występujących dalej wywołań funkcji gettext’a. Przykład takiego użycia jest pokazany w zakomentowanych liniach 30-32 lecz dla tego samego efektu użyłem alternatywnej funkcji dgettext() (linia 34-35) w celu pokazania jej użycia.

To wszystko jeśli chodzi o konfigurację gettext. Poniżej zaczyna się generowanie zawartości strony www a od linii 26 pokazane zostało użycie dla funkcji tłumaczeń, które wymieniłem na początku. Ważne jest, żeby teksty były wprowadzone w cudzysłowach a nie apostrofach! Ich treść zależy też tylko i wyłącznie od nas a ma to następujące znaczenie: jeśli nie uda się znaleźć pliku tłumaczenia dla zdefiniowanego na początku języka bądź wystąpi inny błąd związany z brakiem ustawień językowych itp. to w efekcie zostaną wyświetlone właśnie te wartości tekstowe, a więc jeśli tworząc polską wersję językową strony, która domyślnie była tworzona po angielsku (teksty w stylu „A message to translate”) to w rezultacie dla polskich ustawień językowych wyświetlony zostanie właśnie ten tekst „A message to translate” a nie brak tekstu.

Ostatni tłumaczony tekst:

echo ‚We have ‚.$number.‚ ‚.ngettext(„File”, „Files”, $number).‚ in current dir.’;

jest oczywiście błędny (ponieważ tylko słowo File/Files będzie przetłumaczone) ale użyłem go tutaj żeby nie zaciemniać użycie funkcji służącej do odmiany słówka File w zależności od parametru liczbowego.

Odpalając powyższą stronę w przeglądarce uzyskamy normalny efekt, tzn. taki, jak gdyby użycie funkcji narzędzia gettext nic z podanym tekstem nie robiło (a dokładniej zwracało go w niezmienionej postaci do funkcji echo. Czas bowiem wykorzystać parser xgettext do wygenerowania pliku, w którym sami przetłumaczymy użyte zwroty.

Do generowania pliku wzorcowego (*.po), który będziemy tłumaczyć, a który zawiera wszystkie zwroty podlegające tłumaczeniu, wydajemy polecenie:

xgettext -o wiadomosci.po -n *.php

Parametr -o wskazuje plik wyjściowy i musi być on zgodny z nazwą domeny, którą określiliśmy w skrypcie *.php:

    $domain = 'wiadomosci';
    bindtextdomain($domain, getcwd()."/locale");
    textdomain($domain);

Parametr -n dodaje komentarze w pliku (*.po) z informacją o miejscach wystąpienia danego zwrotu. Jeśli będziemy stosować wspomniany wyżej parametr -j to komentarz odnośnie położenia może się bardzo szybko rozrastać.

Dodatkowy parametr -j jest bardzo pomocny przy ciągłych aktualizacjach pliku (*.po), zawierającego zwroty do przetłumaczenia i zwroty które przetłumaczyliśmy ponieważ nie usuwa jego treści a tylko dodaje nowe linie, które musimy przetłumaczyć. Warto więc o nim później pamiętać.

Ostatecznie podajemy nazwę pliku, który ma zostać sprawdzony pod kontem występowania tekstów do tłumaczenia – w tym przypadku sprawdzone zostaną wszystkie pliki *.php w katalogu bieżącym.

Zawartość tak wygenerowanego pliku będzie wyglądać tak:

# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2009-01-02 01:19+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=CHARSET\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n"

#: gettext.php:27
msgid "A message to translate"
msgstr ""

#: gettext.php:28
msgid "message2"
msgstr ""

#: gettext.php:35
msgid "Acceleration"
msgstr ""

#: gettext.php:39
msgid "File"
msgid_plural "Files"
msgstr[0] ""
msgstr[1] ""

Musimy wprowadzić następujące zmiany:

  • zaktualizować wartość CHARSET w linii: „Content-Type: text/plain; charset=CHARSET\n
    np. tak: „Content-Type: text/plain; charset=utf-8\n„,
  • przetłumaczyć zwroty msgid i wpisać je w cudzysłowie w wyrażeniach msgstr.

Może on wyglądać np. tak:

# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2009-01-02 01:19+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n"

#: gettext.php:27
msgid "A message to translate"
msgstr "Wiadomość do przetłumaczenia."

#: gettext.php:28
msgid "message2"
msgstr "wiadomość druga."

#: gettext.php:35
msgid "Acceleration"
msgstr "Akceleracja"

#: gettext.php:39
msgid "File"
msgid_plural "Files"
msgstr[0] "plik"
msgstr[1] "plików"

Teraz stworzymy ostateczny plik tłumaczenia (*.mo), który jest plikiem binarnym, a więc ręczna jego zmiana jest niezalecana. Służy do tego polecenie:
msgfmt -o wiadomosci.mo wiadomosci.po
Również tutaj należy użyć poprawnych nazw dla plików (odpowiadających nazwie domeny w skrypcie php).
Otrzymaliśmy plik wiadomosci.mo, który przetłumaczyliśmy na jeżyk polski (w moim przypadku), ponieważ chcę użyć właśnie takiego tłumaczenia. Jeśli spróbujemy jeszcze raz odpalić stronę w przeglądarce to nie uzyskamy zakładanej zmiany tłumaczeń. To dlatego, że pliki tłumaczeń (*.mo) muszą być umieszczone w bardzo jasno określonej lokalizacji.
Po pierwsze, będzie to katalog, który zdefiniowaliśmy funkcją:

bindtextdomain($domain, getcwd()."/locale");

a więc katalog locale/.
W podkatalogu locale/ musimy utworzyć katalog z ustandaryzowaną nazwą języka, tzn. wszelkie nazwy typu ‚pl_PL‚,en_US‚, itd. W moim przypadku tworzę więc podkatalog pl_PL w katalogu locale\.
W tak utworzonym katalogu musimy utworzyć jeszcze jeden, ostatni już podkatalog o nazwie LC_MESSAGES. Otrzymujemy więc taką ścieżkę katalogów:
\locale\pl_PL\LC_MESSAGES\
i to właśnie tutaj kopiujemy utworzony plik wiadomosci.mo.
W tym momencie, jeśli odświeżymy stronę przeglądarki z naszym skryptem php dla gettext’a, otrzymamy jej przetłumaczoną wersję.
Plik *.po służący do stworzenia pliku *.mo powinniśmy trzymać zawsze razem z plikiem *.mo (w podanej powyżej lokalizacji) dlatego, żeby zawsze móc szybko wprowadzić zmiany, dodać nowe wpisy a nie męczyć się z ponownym tłumaczeniem (jeśli jednocześnie tworzymy parę wersji językowych strony).
Należy również pamiętać o przeładowaniu serwera apache jeśli aktualizujemy pliki *.mo, ponieważ może nas zmylić cache’owanie plików.
Jeśli tworzymy plik, który ma mieć wybrane kodowanie, to musi zostać stworzony i zapisany w danym kodowaniu! W kodzie trzeba także to kodowanie ustawić (dla pewności) metodą bind_textdomain_codeset. Również nagłówek strony html musi posiadać to samo kodowanie.

Na koniec chciałem jeszcze podać parę własnych wniosków:

  • pliki *.mo są plikami binarnymi, co może oznaczać problem z ich przenoszeniem między systemami operacyjnymi , jednak w moim przypadku pliki generowane pod Debianem działały również na Windows i vice versa (na tym samym komputerze 32-bit),
  • jeśli nie działa wywołanie setlocale(LC_ALL, $language_code); można spróbować zmieniając stałą LC_ALL na LC_MESSAGES,
  • Windows akceptuje nazwy języka w postaci pl_PL.UTF-8 (wczyta pl_PL),
  • nazwy podkatalogów w katalogu locale\ nie mogą mieć nazw niezgodnych ze standardem, czyli nie mogą się nazywać np. tak: pl_PL.UTF-8 a muszą tak: pl_PL,
  • można używać wiele domen na jednej stronie/portalu www, wystarczy odpowiednio parsować skrypty php i nadawać im różne domeny- wówczas katalog LC_MESSAGES danego języka będzie zawierał wiele plików *.mo (i *.po),
  • pamiętać o przeładowaniach serwera apache,
  • sprawdzić jaki jest rezultat wywołania metody setlocale() w systemie Debian (w Windows nie musi być tej funkcji ale w zamian musi być wywołanie putenv(„LANG=$language_code”)

Gdybym o czymś zapomniał, proszę dać znać w komentarzach.

Konfiguracja (Debian):
Apache/2.2.9 (Debian) PHP/5.2.6-0.1~lenny1 with Suhosin-Patch Server,
Processor : AMD Athlon(tm) XP 3000+,
Memory : 1296MB,
Operating System : Debian GNU/Linux 5.0,
Pakiet: gettext, Wersja: 0.17-4

Konfiguracja (Windows):
Apache/2.2.10 (win32) PHP/5.2.6,
Processor : AMD Athlon(tm) XP 3000+,
Memory : 1296MB,
Operating System : Windows XP 32 bit SP3,
Biblioteka: gettext, Wersja: 0.13.1 woe32.

Advertisements

2 Styczeń, 2009 - Posted by | debian, scripts, web |

Brak komentarzy.

Skomentuj

Please log in using one of these methods to post your comment:

Logo WordPress.com

Komentujesz korzystając z konta WordPress.com. Log Out / Zmień )

Zdjęcie z Twittera

Komentujesz korzystając z konta Twitter. Log Out / Zmień )

Facebook photo

Komentujesz korzystając z konta Facebook. Log Out / Zmień )

Google+ photo

Komentujesz korzystając z konta Google+. Log Out / Zmień )

Connecting to %s

%d blogerów lubi to: