GNU gettext z punktu widzenia programisty oraz tłumacza

GNU gettext dla programisty i tłumacza

Arkadiusz Miśkiewicz

$Id: gettext-art.lyx,v 1.1 2000/03/12 21:12:04 misiek Exp $

1 Wprowadzenie

Systemy UNIX, a w szczególności Linux zdobywają coraz większą popularność
na świecie. Linux używany jest zarówno przez wyszkolonych administratorów
jak i początkujących. Kilku programistów wychodząc na przeciw początkującym
użytkownikom nie znającym języka angielskiego stworzyło pakiet GNU
gettext. GNU gettext jest zbiorem aplikacji oraz bibliotek przeznaczonych
do tworzenia programów oraz skryptów potrafiących komunikować się
z użytkownikiem w dowolnym (obsługiwanym) języku.

2 Jak to działa ?

GNU gettext działa na zasadzie modułowej. Każde nowe tłumaczenie programu
jest osobnym plikiem tzw. plikiem MO (jeden plik dla każdego języka).
W większości dystrybucji Linuxa tego typu pliki można znaleźć w katalogach
/usr/share/locale/*/LC_MESSAGES/. Załóżmy, że jakiś program wyświetla
tekst “Hello world!”. Gdy ów program będzie wykorzystywał gettext
zostaną wykonane następujące operacje:

1. sprawdź aktualne zmienne locale (opisane w locale(7)). Najczęściej
używa się zmiennych LC_ALL oraz LANG.

2. sprawdź czy istnieje odpowiedni plik MO dla języka określonego w
zmiennych locale.

3. jeśli istnieje – wczytaj i używaj tłumaczeń; jeśli nie istnieje –
użyj domyślnego języka (w naszym przykładzie jest to język angielski).

Zaletą takiego rozwiązania jest to, że na jednym systemie może pracować
kilkunastu użytkowników używających różnych języków ! Ponadto dodanie
nowego tłumaczenia to tylko dodanie jednego niewielkiego pliku. Jeśli
czytelnik zaglądał to któregoś z plików MO to zapewne wie iż jest
to plik binarny. Pliki binarne MO generowane są z plików tekstowych
PO (zazwyczaj dostępnych wraz ze źródłami programu) przez niewielki
program ,,msgfmt” należący do pakietu gettext.

3 Chcę zostać tłumaczem.

Pliki tekstowe PO są to pliki zawierające wszystkie komunikaty w języku
domyślnym oraz (po przetłumaczeniu) komunikaty w języku docelowym
(np. polskim). Fragment typowego pliku PO przed tłumaczeniem wygląda
mniej więcej tak (w nawiasach klamrowych moje komentarze, których
nie powinno być w oryginalnym pliku PO):

#: src/man-config.c:233
{ nazwa pliku i numer lini, w której występuje podany tekst }
#, c-format
{ dodatkowe informacje o tekście }
msgid “Reading config file %sn”
{ sam tekst w języku domyślnym }
msgstr “”
{ miejsce na tekst w języku docelowym }

Praca tłumacza sprowadza się do przetłumaczenia tekstu znajdującego
się po polu msgid na język docelowy. Uwaga: nie wolno zmieniać tekstu
znajdującego się po polu msgid ! Powyższy fragment po przetłumaczeniu
może wyglądać tak:

#: src/man-config.c:233
#, c-format
msgid “Reading config file %sn”
msgstr “Odczytuję plik konfiguracyjny %sn”

Powyżej, w komentarzu do ,,c-format” napisałem o ,,dodatkowych informacjach
o tekście”. W miejscu tym może się pojawić kilka słów kluczowych:

* fuzzy. Generowane przez program msgmerge (o którym później). Oznacza
to iż prawdopodobnie tłumaczenie nie jest prawidłowe. Po skontrolowaniu
poprawności tłumaczenia usuwamy słowo fuzzy z pliku PO.

* c-format, no-c-format. Dodawane automatycznie (nie powinny być dodawane
czy modyfikowane przez użytkownika). Odpowiednio oznaczają, że tekst
zawiera znaki formatujące z języka C, oraz że ich nie zawiera. Słowo
c-format jest jednocześnie sygnałem dla msgfmt by zwrócił większą
uwagę na poprawność tłumaczenia tegoż tekstu podczas generowania
plików MO.

Przed zabraniem się do tłumaczenia pliku PO pochodzącego z jakiegoś
programu należy zaktualizować plik PO. W typowym programie wykorzystującym
autoconf’a wygląda to mniej więcej tak: ./configure && make -C po
update-po. Oczywiście można wygenerować nowy plik PO (bez żadnych
tłumaczeń) wykonując ./configure && make -C po Nazwa_Programu.pot.
Powstanie plik *.pot – zwykły plik PO nie zawierający żadnych tłumaczeń
(POT == PO Template). Pozostaje jedynie zabranie się za tłumaczenie
pliku. Przyjęto zasadę by w tłumaczeniach używać form bezosobowych.

Poprawność syntaksy przetłumaczonego pliku PO można sprawdzić przy
użyciu msgfmt: msgfmt -v plik.po -o /dev/null. Program poinformuje
o wszelkich nieprawidłowościach. Rola tłumacza właściwie się tu kończy.
Przetłumaczony plik należy przesłać do odpowiedniej grupy tłumaczy
GNU (np. polska grupa tłumaczy GNU jest dostępna pod adresem: pl@li.org)
lub autora programu.

4 Piszę program i chcę dodać obsługę wielu języków.

Poniższy opis dotyczy nie tylko autorów nowych programów ale także
wszystkich osób, które chcą dodać obsługę języków do już istniejącego
oprogramowania. Na początek parę słów o programach oferowanych w pakiecie
GNU gettext:

– gettextize; wykonanie tego skryptu w katalogu, w którym znajduje
się nasz program spowoduje stworzenie struktury katalogów i skopiowanie
standardowych plików używanych przez gettext podczas kompilacji itp.
Ponadto skopiowane zostaną źródła biblioteki libintl na wypadek gdyby
systemowa biblioteka nie obsługiwała funkcji gettext() i jej towarzyszących.

* msgcmp; porównuje dwa pliki PO, żeby sprawdzić czy zawierają te same
zbiory łańcuchów msgid.

* msgcomm; wyszukuje podobne zbiory łańcuchów w plikach PO

* msgfmt; generuje binarne pliki MO z plików tekstowych PO

* msghack; do automatycznej zmiany plików PO np. zamiany miejscami
zbiorów msgid z msgfmt.

* msgmerge; łączy dwa pliki PO, tak by wynikowy plik zawierał maksimum
tłumaczeń z podanych dwóch plików

* msgunfmt; funkcja odwrotna do msgfmt

* xgettext; generuje plik PO z tekstami do przetłumaczenia wynajdując
je w plikach źródłowych programu (*.c itp).

Część z powyższych programów nie wchodzi w skład standardowej dystrybucji
GNU gettext i jest dostępna w formie patchy (pełny pakiet m.in. w
PLD GNU/Linuxie).

Programiście pakiet gettext oferuje kilka funkcji:

char *textdomain (const char *domain_name);

– funkcja ustala tzw. TEXTDOMAIN. Tłumaczenia będą pobierane z pliku
TEXTDOMAIN.mo z katalogu zależnego od ustawień systemowych (typowo
jest to /usr/share/locale/).

char *bindtextdomain (const char *domain_name, const char *dir_name);

– funkcja ,,przydziela” danej domenie (domain_name) katalog, w którym
będzie poszukiwany plik TEXTDOMAIN.mo (na wypadek gdyby występowało
kilka plików *.mo o tej samej nazwie).

char *gettext (const char *msgid);

– funkcja, której przekazujemy string do tłumaczenia.

char *dgettext (const char *domain_name, const char *msgid);
char *dcgettext (const char *domain_name, const char *msgid, int category);

– funkcje będące połączeniem funkcji gettext() z bindtextdomain();.

Poza powyższymi funkcjami wykorzystuje się jeszcze jedną zawartą w
bibliotece glibc – setlocale(3). Funkcja ta służy do ustawienia aktualnie
używanego locale. Prototyp tej funkcji to:

char *setlocale(int category, const char *locale);

gdzie category to nazwa kategorii dla jakiej ustawiamy locale (typowo
jest to LC_ALL lub LC_MESSAGES) natomiast locale może być zarówno
pustym stringiem “” (wtedy locale zostanie ustawione zgodnie z wartościami
zmiennych powłoki odpowiedzialnych za locale tj. LC_ALL, LC_MESSAGES
itd) lub stringiem oznaczającym język dla jakiego ustawiamy locale
(np. “da_DK”). Ponieważ w większości wypadków program powinien wykorzystywać
zmienne powłoki dlatego będziemy używali pustego stringu “” jako locale
w funkcji setlocale(3).

Dla ułatwienia sobie pracy zamiast funkcji gettext będziemy używali
makra _(). Ponadto zdefiniujemy puste makro pełniące funkcję informatora
dla programu xgettext (o czym później).

#define _(String) gettext(String)
#define N_(String) (String)

Dodajmy więc obsługę gettextu do przykładowego programu:

#include
int main()
{
printf(“Hello world!n”);
exit(0);
}

Po pierwsze musimy włączyć dodatkowe pliki nagłówkowe: “libintl.h”
(zawiera funkcje typowe dla gettextu) oraz “locale.h” (prototyp funkcji
setlocale(3)). Następnie dodajemy ustawienie locale, przydzielenie
TEXTDOMAIN, a we wszystkie teksty, które mają być tłumaczone przekazujemy
do funkcji gettext() (w naszym wypadku za pośrednictwem makra _()).
W efekcie plik będzie wyglądał następująco:

#include
#include #include

#define _(String) gettext(String)

int main()
{
setlocale(LC_ALL, “”);
bindtextdomain(“example”, “/usr/share/locale”);
textdomain(“example”);
printf(_(“Hello world!n”));
exit(0);
}

Uważny czytelnik zapewne zauważył, że puste makro N_() nigdzie nie
zostało użyte – więc po co ono jest potrzebne. Otóż program xgettext
(o którym pisałem wcześniej) generuje plik PO wyszukując w plikach
*.c teksty przekazywane do funkcji gettext(). W naszym programie przykładowym
xgettext znalazł by tekst “Hello world!n”. Niestety w pewnych okolicznościach
nie można bezpośrednio używać funkcji gettext() np. mamy:

char *tablica[] = { “dogn”, “catn”, “Polandn” };
[…]
int main()
{
int i;
[…]
for (i = 0; i < 3; i++) printf(tablica[i]); } Użycie funkcji gettext() w definicji "tablica" jest oczywistym błędem programisty. Wyjściem z tej sytuacji jest właśnie makro N_(). Powyższy program w wersji z obsługą gettextu: #define _(String) gettext(String) #define N_(String) (String) char *tablica[] = { N_("dogn"), N_("catn"), N_("Polandn") }; [...] int main() { int i; [...] /* tutaj powinny znaleźć się funckcje setlocale(), textdomain() itd */ for (i = 0; i < 3; i++) printf(_(tablica[i])); } xgettext dzięki N_() będzie teraz ,,wiedział'', które teksty są przeznaczone do tłumaczenia, a sama funkcja gettext() zajmie się tłumaczeniem. No dobrze. Mamy już pliki źródłowe zawierające odpowiednie wywołania funkcji gettext(). Jak teraz wygenerować plik PO ? Właśnie w tym momencie użyjemy programu xgettext: $ xgettext -d example -o example.po -s plik1.c plik2.c plik3.c W wyniku działania programu xgettext (na pliku z pierwszym przykładem - "Hello world" otrzymamy plik PO o następującej zawartości): # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR Free [az url='http://www.amazon.com/b/?node=229534&tag=0202020202-20']Software[/az] Foundation, Inc. # FIRST AUTHOR , YEAR.
#
#, fuzzy
msgid “”
msgstr “”
“Project-Id-Version: PACKAGE VERSIONn”
“POT-Creation-Date: 2000-03-10 22:28+0100n”
“PO-Revision-Date: YEAR-MO-DA HO:MI+ZONEn”
“Last-Translator: FULL NAME n”
“Language-Team: LANGUAGE n”
“MIME-Version: 1.0n”
“Content-Type: text/plain; charset=CHARSETn”
“Content-Transfer-Encoding: ENCODINGn”

#: przyklad1.c:17
msgid “Hello world!n”
msgstr “”

Po przetłumaczeniu pliku PO generujemy plik MO:

$ msgfmt example.po -o example.mo

i kopiujemy go do katalogu /usr/share/locale/JĘZYK/LC_MESSAGES/ (gdzie
JĘZYK to kilkuliterowa nazwa oznaczająca określony język. Dla języka
polskiego będzie to “pl”).

Pozostaje już tylko podziwianie efektów naszej pracy:

$ LANG=pl ./przyklad1
Witaj świecie!
$ LANG=C ./przyklad1
Hello world!

(“C” – oznacza domyślny język ustawiony w bibliotece systemowej; typowo
jest to język angielski).

Programiści stosujący pakiety automake oraz autoconf w swych programach
mogą używać paru ułatwień związanych z gettextem. Po pierwsze należy
uruchomić program gettextize (o którym już pisałem):

~/moj_program/$ gettextize -c -f

Do configure.in dodajemy:

ALL_LINGUAS=”pl” – lista języków dla których już posiadamy tłumaczenie
(oddzielone spacją np. “pl fr de”)

AM_GNU_GETTEXT – makro robiące całą czarną robotę i przegenerowujemy
skrypt configure (wywołując program: autoconf).

To już wszystkie najważniejsze rzeczy dotyczące dodawania gettextu
do programów. Pełną informację jak zawsze można znaleźć na stronach
info dostarczanych z pakietem GNU gettext.

5 A co ze skryptami w shellu czy perlu lub innych językach ?

Jedyną znaną mi powłoką obsługującą skrypty z tłumaczeniami jest bash
2. W skrypcie basha 2 wpisujemy po prostu:

$ echo $”Hello world!”

Plik PO przygotowujemy natomiast przy użyciu samej powłoki:

$ bash –dump-po-strings skrypt.sh

Generację pliku MO przeprowadzamy już przy użyciu msgfmt z pakietu
GNU gettext. Należy ponadto w skrypcie ustawić zmienną TEXTDOMAIN
na nazwę tekstowej domeny. Niestety nie każdy ma czy chce używać basha
2. Pozostali powinni wykorzystywać w swych skryptach program gettext
zawarty w pakiecie GNU gettext. Przykładowe wywołanie to:

$ gettext –domain=TEXTDOMAIN “Hello world!”

Mogę na marginesie dodać, że dzięki tej metodzie skrypty startowe w
PLD GNU/Linuxie potrafią komunikować się z użytkownikiem m.in. w języku
polskim ! Podobna sytuacja jest w perlu – wystarczy ściągnąć dodatkowy
moduł z archiwum CPAN dodający funkcję gettext. Z gettextu można również
korzystać pisząc programy w innych, nie wspomnianych tutaj językach
programowania np. w pascalu (FreePascal Compilator), a także pod innymi
niż Unixowe systemami – np. pod DOSem (dzięki źródłom biblioteki libintl).

6 Problemy z GNU gettextem.

Niestety gettext nie jest rozwiązaniem idealnym. Jest to narzędzie
dość ograniczone. Podstawowym problemem jest odmiana czy liczba mnoga.
W języku angielskim kilka plików to po prostu ,,files”. W języku
polskim sytuacja się zmienia zależnie od ilości plików (2,3,4 pliki;
5-21 plików itd), natomiast GNU gettext nie pozwala na tłumaczenie
zależne od np. ilości plików. Problem ten nie został rozwiązany do
dnia dzisiejszego (osobom zainteresowanym polecam kod źródłowy programu
lftp, w którym gettext został uzupełniony o częściowe rozwiązanie
umożliwiające tłumaczenie z uwzględnieniem odmian itp.).

7 Na zakończenie.

Mimo licznych ograniczeń GNU gettext jest ważnym narzędziem przybliżającym
środowisko Linuxa ludziom z poza grona osób znających język angielski
– np. sporej liczbie użytkowników polskiej wersji Windows 8-). Zapraszam
wszystkich chętnych do tlumaczenia plików PO w ramach GNU Translation
Project oraz do dodawania obsługi gettexu do już istniejących programów.

8 Adresy.

* GNU Translation Project
http://www.iro.umontreal.ca/~pinard/po/HTML/
http://www.iro.umontreal.ca/contrib/po/

* Polski ,,oddział” GNU Translation Project
http://www.ceti.com.pl/~kravietz/gnu/gnu_tp.html

* Repozytorium CVS PL GNU TP
http://cvsweb.pld.org.pl/index.cgi/i18n/

* Słownik informatyczny, zasady tłumaczenia tekstów informatycznych
http://venus.ci.uw.edu.pl/~milek/

9. Przykłady

One thought on “GNU gettext z punktu widzenia programisty oraz tłumacza

  1. mtfk says:

    Warto by było dodać że przy wykorzystaniu narzędzia xgettext wyszukuje w plikach funkcji gettext czyli jak mamy zdefiniowane:

    #define _(str) gettext (str)

    program sobie z tym nie poradzi, chyba że podamy mu dodatkowy parametr -k_ który wskaże że znak _ jest dodatkowym słowem kluczowym który powinien szukać lub podczas wywołania skorzystać z pre-procesora np.

    xgettext -C -d nazwa.pl

    Reply to mtfk

Leave a Reply

Your email address will not be published. Required fields are marked *