geo2.pl

I was using geo2.pl GPS/GPRS location tracking device for some time. Wasn’t best experience – frequent system failures (on device and on geo2 server side), lack of competent support, lack of information to users (once it died for a month without ANY information from geo2 company). So if you considered buying geo2 product – forget about it. With such quality it’s not worth your money. Especially that they rose monthly price over 300%. But if you have hardware already…

Hardware:

There are two serial ports available on this unit. One provides GPS coordinates in NMEA format and is available through USB connector outside the device. The other serial port is available internally at JP1 connector (see picture). Any USB-serial adapter will do (I used Profilic PL2303 based one in form of old Nokia CA42 cable).

Use internal JP1 serial port to access Telit GE683-GPS AT command interface. By default units starts internal geo2 software (written in python).

AT#LSCRIPT

#LSCRIPT: “config”,53
#LSCRIPT: “updater.log”,247
#LSCRIPT: “gsm.pyo”,5085
#LSCRIPT: “main.pyo”,10393
#LSCRIPT: “config.pyo”,1602
#LSCRIPT: “protocol.pyo”,11566
#LSCRIPT: “acc.pyo”,1565
#LSCRIPT: “kernel.pyo”,8684
#LSCRIPT: “device.pyo”,2207
#LSCRIPT: “gps.pyo”,4347
#LSCRIPT: “io.pyo”,1197
#LSCRIPT: “List.pyo”,6015
#LSCRIPT: “debug.pyo”,2394
#LSCRIPT: “boot.py”,27
#LSCRIPT: free bytes: 1944638

OK

“boot.py” is started after powering on geo2 device. You can change boot mode to “start after 10 seconds” and also change boot file to not existing file to prevent it from running any script. Finally reboot device. You have few seconds after powering on geo2 device to do that – otherwise boot.py will start.

AT#STARTMODESCR=1,10
AT#ESCRIPT=”blah.py”
AT#REBOOT

Now see what geo2 software prints for us. We will start “boot.py” without a reboot:

AT#ESCRIPT=”boot.py”
OK
AT#EXECSCR
OK
kernel start
import main # precompiled from main.pyo
import protocol # precompiled from protocol.pyo
import marshal # builtin
import gps # precompiled from gps.pyo
import gsm # precompiled from gsm.pyo
import config # precompiled from config.pyo
import device # precompiled from device.pyo
import List # precompiled from List.pyo
import acc # precompiled from acc.pyo
import IIC # builtin
import io # precompiled from io.pyo
main
main

(now it will periodically print “main” until some bad thing happens).

Can we see what geo2 software does? Yes, we can. There is “AT#RSCRIPT# command that prints content of the file. Unfortunately geo2 files were uploaded with a option disabling RSCRIPT for most of uploaded files.

Fortunately we can read the files from own python script. Someone wrote scripts for this already. telit-pyo.py script from telit-862-python-tools github repository when run on Telit device will print content of all “*.py” scripts to serial console output (in hex, with additional headers). Then decode-telit-pyo.py script will produce raw files from serial console log.

We need small modifications for our purposes – we have to patch telit-pyo.py script to also print content of “config” and “*.pyc” files (and also skip printing itself).

[crayon-57e05c3939541639994040/]

With this we end up having all files from geo2 device. There is another problem – most of these is “pyc” which is not raw source code. It’s python byte code, not really human readable.

decompyle – Python Decompiler comes to help. It’s not developed anymore (there is paid commercial service based on it though) and hard to compile. I’ve built it with python 1.6 and 2.7 after some patching.

With decompyle we can get more readable form of byte code files.

First look and – quality of geo2 code seems to be quite low.

GPRS part registers with “apn.o2.pl” APN (previously geo2 company was owned by o2.pl and, no, it wasn’t much better experience then) to a PlusGSM network.

Device talks with IP 193.17.41.249 on port 6288 (TCP connection). There seems to be no real authorization (yikes!) when talking to the server. Pseudo authorization is based on device IMEI and value stored in config file (as “CODE”).

GPS when no fix is found is… restarted after some time (300s), then restarted again, and again, and again (with longer periods each time).

Accelerometer is accessed over I2C and used to detect if device is moving (and then transmit new coordinates). It is initialized with such values:

_ADDRESS = 28
_INIT = ‘g’
_FILTER = ‘x07’
_SENSITIVITY = ‘x04’

# i2c device at _ADDRESS
_i2c.readwrite((‘ %s’ % (_INIT)), 0)
_i2c.readwrite((‘!%s’ % (_FILTER)), 0)
_i2c.readwrite((‘2%s’ % (_SENSITIVITY)), 0)

Configuration is stored in “config” file. This file can be read directly with “AT#RSCRIPT”.

[
3, # CONF_VER
‘8xff)’, # CODE (used in authorization header)
”, # PIN
1, # ROAM_OFF
0, # ROAM_ZONE_ID
[], # ROAM_ZONE
0, # ROAM_STATE
-1, # JAMMER_DATE
0 # DELAY
]

The device has a code that allows to run (via eval()) ANY code that’s sent from remote geo2 server (idea was to allow remote debugging I think). Good that there is no microphone available on the geo2 device since Telit chip is capable of establishing audio channel and thus allowing remote audio spying.

It is also possible to upload few version of software files remotely. “updater.log” stores some information about the process:

starting update
version /fizyka/thingy/trunk/geo2b105:1091
L5;L8;L10 gsm.pyo;L14 main.pyon;L10 config.pyo;L14 protocol.pyon;L10 acc.pyo;L14 kernel.pyon;L14 device.pyon;L14 gps.pyon;L10 io.pyo;L10 List.pyo;L10 debug.pyo;L17;L21;L25;L27;L29;L30;

All that information and great Telit documentation will allow you to write own software. You could also change geo2 IP to your own and simulate geo2 server software on own machine.

Mounting DOS HDD image

Mounting dos hdd image, first partition:

mount -o loop,offset=32256 dos-hdd.img dir

I’m using such image to update BIOSes in machines where bios doesn’t fit into floppy image. Such hdd image can be places inside iso image to be booted at virtual or real cdrom.

mkisofs -v -r -T -J –hard-disk-boot -b dos-hdd.img -o ../test.iso .

Tabs, vim and 256 colors – tips

Having tabs and spaces visible as separate entities in vim is very neat feature especially if you use colors to differentiate.

Switch TERM to konsole-256color in kde konsole terminal emulator (or whatever emulator you use – rpm -ql ncurses terminfo | grep 256 for more) and put into .vimrc:


colorscheme darkblue
highlight TabGroup ctermbg=233 guibg=233
match TabGroup /t/

Now tab characters are a bit brighter than spaces.

lftp and editing remote files

lftp is a very nice piece of software (for people who like text console).

How to edit remote files? Take a look!


[arekm@tarm ~]$ cat .lftp/rc
alias edit source -e ~/.lftp/edit.sh
[arekm@tarm ~]$ cat .lftp/edit.sh
#!/bin/sh
tempid=$$
echo get $1 -o /tmp/$tempid$1
echo shell vim /tmp/$tempid$1
echo put -E /tmp/$tempid$1 -o $1
[arekm@tarm ~]$ lftp -u arm ftp.somewhere.pl
Password:
lftp arm@ftp.somewhere.pl:~> ls
drwxr-xr-x 2 0 0 4096 Jan 29 20:35 .
drwxr-xr-x 2 0 0 4096 Jan 29 20:35 ..
lftp arm@ftp.somewhere.pl:~> edit test.txt
get: Access failed: 550 Can't open test.txt: No such file or directory

[HERE vim is opened; after saving]

9 bytes transferred
lftp arm@ftp.somewhere.pl:~> rels
drwx---r-x 3 10089 999 50 Jun 15 18:35 .
drwxr-xr-x 2 0 0 4096 Jan 29 20:35 ..
-rw-r--r-- 1 10089 nogroup 9 Jun 15 18:35 test.txt
lftp arm@ftp.somewhere.pl:~> cat test.txt
El test.
10 bytes transferred
lftp arm@ftp.somewhere.pl:~> edit test.txt
9 bytes transferred

[HERE again vim is opened; after saving]

23 bytes transferred
lftp arm@ftp.somewhere.pl:~> cat test.txt
El test.
Small change.
25 bytes transferred
lftp arm@ftp.somewhere.pl:~>

Don’t we all love lftp?

201504 EDIT: lftp 4.6.1 will have edit command built in!

CVS – System Kontroli Wersji

CVS – System Kontroli Wersji

Arkadiusz Miśkiewicz
$Id: cvs-art.txt,v 1.7 2000/05/07 17:39:00 misiek Exp $Większość dostępnych programów oferowanych użytkownikom Linuksa jest
dziś tworzonych przez duże grupy programistów. Zarządzanie takimi projektami
jak jądro Linuksa, glibc czy XFree86 było by bardzo trudne i uciążliwe gdyby
nie pomysłowi programiści, którzy stworzyli narzędzie CVS.

CVS jest efektywnym systemem kontroli wersji. System zapamiętuje
wszystkie wersje wszystkich plików, które umieścisz w CVSie.

Dzięki temu w
dowolnym momencie można powrócić do poprzednich wersji każdego pliku,
porównywać wersje ze sobą, tworzyć odgałęzienia, przeglądać historię zmian
itp. W jednym repozytorium CVS można przechowywać teoretycznie dowolną ilość
programów w postaci źródłowej. Typowo każdy program będzie traktowany jako
osobny ,,moduł”.
CVS zorganizowany jest na zasadzie serwer <--> klienci. Na serwerze
w tzw. repozytorium (repository) trzymane są wszystkie pliki (w specjalnym
formacie rozumianym przez System Kontroli Rewizji (RCS)). Użytkownik chcący
dokonać zmiany w którymś w plików najpierw musi pobrać dane z repozytorium do
swojego lokalnego katalogu. Następnie po dokonaniu edycji przesyła poprawki do
repozytorium (commit).
Serwer CVS obsługuje po kolei żądania wprowadzenia zmian od
użytkowników. W momencie gdy dwie osoby chcą przesłać poprawki, jedna zostanie
obsłużona jako pierwsza. Poprawki drugiej osoby nie zostaną natomiast
naniesione – system poprosi ją o zaktualizowanie danych z repozytorium.
W chwili aktualizacji CVS spróbuje automatycznie połączyć poprawki pierwszej
osoby z poprawkami drugiej. Jeśli zmiany kolidują ze sobą druga osoba będzie
musiała dokonać połączenia ręcznie (CVS ułatwia nam sprawę informując
o miejscach występowania konfliktów). Po połączeniu przesyłamy poprawki do
repozytorium.
Jak już pisałem CVS umożliwia dotarcie do dowolnej wersji każdego pliku.
Jest to możliwe dzięki przypisaniu każdej wersji pliku numeru rewizji
(revision). W przypadku przesłania poprawek numer rewizji jest inkrementowany
podczas gdy stary numer nadal jest powiązany ze starą wersją pliku. System CVS
umożliwia także znakowanie określonych plików w określonych wersjach poprzez
nadawanie im nazw symbolicznych (tag). Warto tutaj nadmienić, że tylko jedna
wersja danego pliku może być oznaczona pewną nazwą symboliczną – nie ma
możliwości oznaczenia tą samą nazwą dwóch rewizji jednego pliku.
Mamy już podstawowe wiadomości dotyczące CVSu. Możemy więc przystąpić
do prób. Na początek należy zainstalować oprogramowanie rcs oraz cvs (adresy
na końcu artykułu). Większość użytkowników znajdzie wspomniane oprogramowanie
w archiwach pakietów swoich dystrybucji.

Po skompilowaniu pakietu CVS uzyskamy świetną dokumentację w postaci
plików info, kilka skryptów oraz jeden plik binarny – program ,,cvs”. Ogólna
składnia tego programu wygląda następująco:

cvs [-opcje] komenda [-opcje komendy] [argumenty]

Najczęściej używane opcje (główne) to:

-zX kompresuj dane na poziomie X (zalecana wartość 7)
-d [cvsroot] ,,ścieżka” do repozytorium
-q wyświetlaj tylko najważniejsze komunikaty

Ścieżka ,,cvsroot” może wskazywać repozytorium lokalne lub na dowolnym
serwerze w sieci (więcej o cvsroot w dalszej części artykułu).

Do pobierania plików z repozytorium służy komenda ,,checkout” (można także
używać zamiennie co lub get). Przykład:

[misiek@dark example]$ cvs -z7 checkout test
cvs server: Updating test
U test/testzlib.c
[misiek@dark example]$ cd test && ls -l
razem 4
drwxr-xr-x 2 misiek users 4096 kwi 17 21:22 CVS
-rw-r–r– 1 misiek users 334 kwi 17 21:22 testzlib.c

Jak widać do lokalnego katalogu został sprowadzony plik ,,testzlib.c”,
który można edytować, poprawiać itp. CVS utworzył także katalog ,,CVS”, w
którym przechowuje dane administracyjne o lokalnych wersjach plików.

Po wprowadzeniu zmian w lokalnej kopii używamy komendy ,,commit” (lub ci) by
przesłać zmiany do repozytorium:

[misiek@dark test]$ cvs commit testzlib.c
< ... Tutaj otwiera się edytor w którym opisujemy charakter poprawek
które nanieśliśmy. Edytor określamy poprzez zmienne środowiska:
$EDITOR lub $CVSEDITOR… >
Checking in testzlib.c;
/usr/src/CVSROOT/test/testzlib.c,v <-- testzlib.c
new revision: 1.2; previous revision: 1.1
done

Widać od razu, że numer rewizji został zwiększony o 1 na ostatnim miejscu.
Charakter zmian można również opisywać bezpośrednio z linii poleceń:

[misiek@dark test]$ cvs commit -m “- podawaj rozmiar bufora” testzlib.c

Może się tak zdarzyć, że w momencie gdy wprowadzaliśmy zmiany do lokalnego
pliku ktoś przesłał do repozytorium swoje zmiany. Wówczas przesłanie naszych
zmian (commit) się nie powiedzie:

[misiek@dark test]$ cvs commit -m “- fprintf zamiast printf” testzlib.c
cvs server: Up-to-date check failed for `testzlib.c’
cvs [server aborted]: correct above errors first!

W takiej sytuacji korzystamy z komendy ,,update” (up), która uaktualni nasze
pliki oraz spróbuje automatycznie połączyć nasze zmiany w uprzednio
wprowadzonymi.

[misiek@dark test]$ cvs commit -m “- fprintf zamiast printf” testzlib.c
cvs server: Up-to-date check failed for `testzlib.c’
cvs [server aborted]: correct above errors first!
[misiek@dark test]$ cvs up
cvs server: Updating .
RCS file: /usr/src/CVSROOT/test/testzlib.c,v
retrieving revision 1.2
retrieving revision 1.3
Merging differences between 1.2 and 1.3 into testzlib.c
M testzlib.c
[misiek@dark test]$ cvs commit -m “- fprintf zamiast printf” testzlib.c
Checking in testzlib.c;
/usr/src/CVSROOT/test/testzlib.c,v <-- testzlib.c
new revision: 1.4; previous revision: 1.3
done

CVS ściągnął i poprawnie połączył zmiany. Niestety CVS to nie człowiek, więc
nie potrafi myśleć i dlatego nie zawsze uda mu się automatycznie połączyć
zmiany. W takiej sytuacji zasygnalizuje błąd ,,conflicts during merge”.
Dokończenia łączenia będzie musiał dokonać człowiek.

Podczas aktualizacji danych serwer informuje nas o statusie plików,
z którymi miał do czynienia podczas przeprowadzania aktualizacji.
Możliwe są następujące litery statusu:
– U serwer przesłał nam kompletny plik z repozytorium
– P serwer przesłał nam jedynie różnicę (diff) pomiędzy plikiem
w repozytorium a naszym lokalnym
– A plik został dodany do lokalnej kopii ale informacja o tym nie
została przesłana do repozytorium (nie wykonano komendy commit)
– R plik został usunięty z lokalnej kopii ale informacja o tym nie
została przesłana do repozytorium (nie wykonano komendy commit)
– M kopia lokalna pliku został zmieniona
– C podczas próby automatycznego łączenia został wykryty konflikt
– ? plik istnieje tylko w lokalnej kopii i nie posiada odpowiednika
w repozytorium

Możemy nakazać CVSowi ignorować określone pliki. Wystarczy wpisać nazwy
plików do pliku ,,.cvsignore” i przesłać ów plik do repozytorium.

Inną przydatną komendą jest komenda ,,log” pozwalająca obejżeć historię
wpisów dokonywanych przy przesyłaniu poprawek (commit -m xyz). Skrypt rcs2log
dostępny wraz z pakietem cvs generuje pliki ChangeLog na podstawie informacji
dostarczonych przez komendę ,,log”.

CVS połączył nam przed chwilą dwie wersje pliku – 1.2 oraz 1.3
i w efekcie zapisał wersję 1.4. Załóżmy, że chcemy zobaczyć różnicę pomiędzy
wersjami 1.2 oraz 1.3 – nic prostszego:

[misiek@dark test]$ cvs diff -u -r1.2 -r1.3 testzlib.c
Index: testzlib.c
===================================================================
RCS file: /usr/src/CVSROOT/test/testzlib.c,v
retrieving revision 1.2
retrieving revision 1.3
diff -u -r1.2 -r1.3
— testzlib.c 2000/04/17 19:33:22 1.2
+++ testzlib.c 2000/04/17 19:43:25 1.3
@@ -7,7 +7,7 @@
FILE *f;
char buf[1024];

– if ((f = gzopen(“test.gz”, “r”)) == NULL)
+ if ((f = gzopen(“test.gz”, “rb”)) == NULL)
{
fprintf(stderr, “Problem when opening test.gz file: %sn”,
strerror(errno));

Opcja ,,-r” pozwala na porównywanie wg. numerów rewizji. Możliwe jest
także porównywanie wg. daty (opcja ,,-D”) jak i mieszane np.

[misiek@dark test]$ cvs diff -u -r1.2 -Dnow testzlib.c
[misiek@dark test]$ cvs diff -u -D’1 minute ago’ -Dnow testzlib.c
[misiek@dark test]$ cvs diff -u -D’1 minute ago’ -Dnow testzlib.c
[misiek@dark test]$ cvs diff -u -D’2000-04-17′ -Dnow testzlib.c

Opcja ,,-u” (unified) ma takie samo znaczenie jak dla ogólnie znanego
programu diff i nie będę jej tu opisywał.Do repozytorium możemy dodawać nowe pliki i katalogi, a także je usuwać (z
usuwaniem katalogów bywają zazwyczaj problemy i dlatego zazwyczaj się ich nie
usuwa). Operację dodawania realizuje się za pomocą komendy ,,add”, natomiast
usuwania ,,remove”. Przykład:

[misiek@dark test]$ mv testzlib.c tzlib.c
[misiek@dark test]$ cvs remove testzlib.c
cvs server: scheduling `testzlib.c’ for removal
cvs server: use ‘cvs commit’ to remove this file permanently
[misiek@dark test]$ cvs add tzlib.c
cvs server: scheduling file `tzlib.c’ for addition
cvs server: use ‘cvs commit’ to add this file permanently
[misiek@dark test]$ cvs commit -m “- zmiana nazwy” testzlib.c tzlib.c
Removing testzlib.c;
/usr/src/CVSROOT/test/testzlib.c,v <-- testzlib.c
new revision: delete; previous revision: 1.4
done
RCS file: /usr/src/CVSROOT/test/tzlib.c,v
done
Checking in tzlib.c;
/usr/src/CVSROOT/test/tzlib.c,v <-- tzlib.c
initial revision: 1.1
done

W ten sposób dokonałem zmiany nazwy pliku testzlib.c na tzlib.c. Plik tzlib.c
jest traktowany przez CVS jako całkowicie nowy plik. Oczywiście do starych
wersji pliku testzlib.c nadal mamy dostęp (aż do momentu gdy ktoś nie doda
nowego pliku pod nazwą testzlib.c) ! Podczas dodawania plików binarnych należy
stosować opcję ,,-kb”, która wyłącza konwersję końca linii.

CVS udostępnia wiele informacji na temat każdego z plików. Jedną z komend
podających status pliku jest komenda ,,status”. Typowe wywołanie to:

[misiek@dark test]$ cvs status tzlib.c

Otrzymamy informacje o numerze rewizji, lokalizacji pliku w repozytorium,
oznakowaniu pliku (nazwami symbolicznymi) itp. Najistotniejsze dla nas jest
pole ,,Status:”. Plik może mieć jeden z poniższych statusów:
– Up-to-date – plik jest aktualny (identyczny z wersją
w repozytorium)
– Locally Modified – lokalna kopia pliku została zmieniona
– Locally Added – plik został dodany do lokalnej kopii
– Locally Removed – plik został usunięty z lokalnej kopii
– Needs Checkout – ktoś dokonał zmian w pliku i przesłał
– Needs Patch zmiany do repozytorium. Należy wykonać
update.
– Needs Merge – wymagane jest połączenie naszych zmian
w pliku ze zmianami innej osoby
– File had conflicts on merge – podczas łączenia wystąpiły konflikty
– Unknown – plik istnieje tylko w lokalnej kopii
i nie posiada odpowiednika w repozytorium

Potrafimy już operować danymi zawartymi w repozytorium więc przyszła
teraz pora na stworzenie własnego repozytorium. Jest kilka rodzajów
repozytoriów (lokalne, zdalne) i kilka sposobów autoryzacji (serwer haseł
(pserver), kerberos, ssh, rsh). Opiszę jak stworzyć repozytorium lokalne oraz
zdalne (przy użyciu serwera haseł).

Pliki w repozytorium lokalnym jak i zdalnym są identyczne, tak
więc tworzymy je w identyczny sposób – za pomocą komendy ,,init”:

[misiek@dark example]$ cvs -d :local:/home/misiek/CVSREPO init
[misiek@dark example]$ ls -l /home/misiek/CVSREPO
razem 4
drwxrwxr-x 3 misiek users 4096 kwi 17 22:28 CVSROOT

Jak widać powstał katalog CVSREPO zawierający katalog CVSROOT z danymi
administracyjnymi serwera CVS. Program ,,cvs” podczas wykonywania
jakichkolwiek operacji związanych z repozytorium CVS musi wiedzieć gdzie owo
repozytorium się znajduje. W powyższym przykładzie lokalizację repozytorium
podaliśmy jako parametr dla opcji ,,-d”. Nasuwa się pytanie jakim cudem w
poprzednich przykładach wszystko działało mimo iż nie było tam opcji ,,-d” ?
Otóż zamiast ciągłego wpisywania parametru ,,-d” można lokalizację
repozytorium ustawić w zmiennej $CVSROOT:

[misiek@dark example]$ CVSROOT=”:local:/home/misiek/CVSREPO”; export CVSROOT

Od tego momentu mamy już w pełni sprawny serwer CVS, można więc dodać nowe
moduły ze źródłami oprogramowania (o czym później).

Oczywiste jest, że dostęp do repozytorium będą mieli tylko lokalni użytkownicy
z prawem odczytu (zapisu) do katalogu ,,/home/misiek/CVSREPO”. W momencie gdy
odpowiedniej grupie użytkowników damy prawo do zapisu do tegoż katalogu to
każdy z nich będzie mógł wykonać ,,rm -rf /home/misiek/CVSREPO” dzięli czemu
całe repozytorium powędruje do /dev/null. Lepszym w tym wypadku rozwiązaniem
będzie dostęp do repozytorium poprzez sieć i serwer haseł.

Na początek należy dodać następujące linijki do pliku /etc/services:

cvspserver 2401/tcp # CVS client/server operations
cvspserver 2401/udp # CVS client/server operations

Następnie do serwera inetd dodajemy informacje o serwerze haseł, którego
rolę pełni program … ,,cvs”:

U mnie (rlinetd, /etc/rlinetd.conf) konfiguracja wygląda następująco:

service “cvspserver” {
protocol tcp;
port “cvspserver”;
user “misiek”;
server “/usr/bin/cvs”;
exec “cvs -f –allow-root=/home/misiek/CVSREPO pserver”;
initgroups;
}

natomiast w przypadku inetd (/etc/inetd.conf):

cvspserver stream tcp nowait misiek /usr/bin/cvs cvs
-f –allow-root=/home/misiek/CVSREPO pserver

(opcja -f nakazuje nie używać pliku ~/.cvsrc)

Prawo zapisu i odczytu do wszystkie plików w repozytorium
(/home/misiek/CVSREPO) ma jedynie użytkownik ,,misiek” co zapobiega żartom
użytkowników typu ,,rm -rf” (zazwyczaj na potrzeby serwera CVSu zakłada się
osobne konto, a ze względów bezpieczeństwa cały serwer jest umieszczony w
,,więzieniu” uzyskanym za pomocą funkcji chroot(2)).

W przypadku korzystania ze zdalnego serwera (poprzez serwer haseł) zmienna
$CVSROOT wyglądać może następująco:
CVSROOT=”:pserver:misiek@localhost:/home/misiek/CVSREPO”

Użytkowników i ich hasła należy umieścić w pliku
/home/misiek/CVSREPO/CVSROOT/passwd. Format pliku to: “użytkownik:hasło:grupa”
np. “misiek:$1$Ba2OGOy.$exqx4uSoXIMPCaoPvuAJy1:users”. Pole ,,grupa” pojawiło
się w nowszych wersjach CVSu i umożliwia przydzielanie określonym użytkownikom
praw dostępu do określonych częsci repozytorium CVS (standardowo każdy
użytkownik ma dostęp do wszystkich modułów repozytorium). Hasło jest
oczywiście zaszyfrowame. Najprościej do szyfrowania wykorzystać perla:

perl -e ‘print crypt “hasło”, “dwa dowolne znaki”; print “n”; ‘

Nasze repozytorium stoi gotowe – czas umieścić w nim jakieś pliki. Posłużymy
się kolejną komendą – ,,import”, która dodaje w całości nowy moduł.
Przechodzimy do katalogu zawierającego nasze pliki i:

[misiek@dark nowe]$ cvs import -m “- nowy modul” mojmodul misiek poczatek
N mojmodul/testr.c

No conflicts created by this import

mojmodul – nazwa nowego modułu; misiek – kto dodaje moduł (źródło pochodzenia
modułu czyli tzw. vendortag); poczatek – początkowa nazwa symboliczna (tzw.
release tag). Nie podałem nazw plików więc CVS zaimportował wszystkie pliki
z aktualnego katalogu. Teraz musimy ściągnąć pliki z repozytorium za pomocą
znanej już komendy ,,checkout”.

CVS pozwala także na używanie kilku słów kluczowych, które automatycznie
są zastępowane odpowiednimi ciągami znaków. Wystarczy umieścić słowo kluczowe
w dowolnym pliku, a podczas przesyłania poprawki do repozytorium (commit) CVS
dokona odpowiednich zastąpień. Przykładowe słowa kluczowe (zastąpione już
przez CVS):

$Id: cvs-art.txt,v 1.7 2000/05/07 17:39:00 misiek Exp $
$Date: 2000/05/07 17:39:00 $
$Revision: 1.7 $
$Header: /usr/src/CVSROOT/rozne/cvs-art.txt,v 1.7 2000/05/07 17:39:00 misiek Exp $

Kolejną niezastąpioną zaletą CVSu są odgałęzienia (branch). CVS pozwala
na równoczesne prowadzenie kilku wersji plików. Sytuacja taka występuje gdy
np. część programistów zajmuje się wersją stabilną programu, a część zajmuje
się wersją rozwojową. W tego typu sytuacjach za pomocą komendy tag lub rtag
ale z opcją ,,-b”. CVS umożliwia także wprowadzanie zmian z np. odgałęzienia
devel do odgałęzienia stable za pomocą komendy update z opcją ,,-j”.

Niemal wszystkie pliki konfiguracyjne związane z serwerem CVS znajdują
się w specjalnym module o nazwie ,,CVSROOT”. Użytkownicy mają oczywiście
dostęp do tego modułu i mogą go sprowadzić z repozytorium. Najbardziej
interesujące są dla nas pliki:
– checkoutlist
Pliki znajdujące się w module CVSROOT, a których nazwy wpiszemy do
pliku checkoutlist pojawią się po stronie serwera zarówno w formacie zrozumiałym
przez RCS jak i w zwykłej postaci jaką użytkownicy widzą po sprowadzeniu plików
do lokalnego repozytorium. Typowo w checkoutlist umieszcza się skrypty do
generowania plików ChangeLog, do wysyłania informacji o zmianach w
repozytorium na listę dyskusyjną itp. Ze względów bezpieczeństwa nie należy
umieszczać tam pliku ,,passwd”.
– commitinfo
zazwyczaj w tym pliku umieszczamy wywołania skryptu (np. commit_scan)
który przygotowuje dane przychodzące podczas przesyłania poprawek do obróbki
przez kolejny skrypt (np. log_accum.pl).
– config
zawiera podstawowe opcje konfiguracji serwera CVS. Najistotniejszą
opcją jest opcja SystemAuth. Ustawienie jej na ,,yes” wymusza na serwerze
haseł przeszukiwanie systemowej bazy haseł (np. /etc/shadow) w przypadku gdy
użytkownik nie został znaleziony w bazie serwera CVS (CVSROOT/passwd)).
– cvswrappers
pozwala wymusić pewne opcje podczas dodawania określonych plików i tak
np. podanie w tym pliku ,, *.jpg -k ‘b’ ” jest równoważne z podawaniem opcji
,,-kb” podczas dodawania plików *.jpg.
– loginfo
w tym pliku umieszczać należy wywołania skryptów operujących na danych
dostarczonych podczas przesyłania poprawek (commit) i przetworzonych przez
skrypty wywoływane w pliku commitinfo. Może to być wywołanie np. skryptu
log_accum.pl.
– modules
zawiera listę modułów. Można tworzyć moduły odwołujące się do innych
modułów itp.

W module CVSROOT może znajdować się dodatkowo kilka innych plików
administracyjnych jednak nie są one aż tak istotne jak przedstawione powyżej.

W pewnych sytuacjach (szczególnie przy projektach, w których uczestniczy
duża ilość programistów) rozwiązanie serwer <--> klienci powoduje znaczne
obciążenie serwera, a co za tym idzie spowolnienie pracy i pogorszenie się
komfortu wspólnej pracy nad projektem poprzez CVS. By odciążyć serwer stawia
się serwery kopie udostępniające zasoby jedynie w trybie tylko do odczytu
(serwery kopie mogą synchronizować swe zasoby np. za pomocą protokołu rsync
rozwijanego przez SAMBA Team). Mimo to poprawki nadal muszą być przesyłane do
serwera głównego.

W typowym projekcie serwer CVS poprzez komendy zawarte w plikach
administracyjnych sprzężony jest z listą dyskusyjną na którą przesyłane są
informacje o zmianach zachodzących w CVSie (po każdym wysłaniu poprawek),
uruchamiane są programy generujące pliki ChangeLog itp. Ponadto zasoby
serwerów CVS często bywają udostępniane poprzez www za pomocą skryptów cvsweb,
viewcvs. Ze stron można ściągać określone wersje plików, różnice pomiędzy
plikami, oglądać historie danych plików itp.

CVS jest wartościowym narzędziem również dla pojedynczych programistów
(sam osobiście w lokalnym CVSie trzymam oprogramowanie, skrypty, artykuły,
dokumentację itp.) czy nawet webmasterów (dla przykładu podam, że strony
http://www.pld.org.pl/ są przechowywane i automatycznie uaktualniane właśnie
na podstawie repozytorium). Innym udogodnieniem jakie niesie CVS jest np.
możliwość łatwego generowania pakietów rpm na podstawie najnowszych zasobów
znajdujących się w CVSie PLD. Przy odpowiednich ustawieniach wystarczy
wykonać: cd rpm/SPECS && ./builder -bb nazwa_speca.spec, a wszystkie niezbędne
do wygenerowania binarnego pakietu rpm pliki zostaną automatycznie ściągnięte
i przebudowane. Udogodnienie to jest specyficzne bo dotyczy niemal wyłącznie
użytkowników CVSu PLD jednak podaję tu ten przykład by pokazać ogromne
możliwości jakie dżemią w systemie kontroli wersji – możliwości zależne jedynie
od pomysłów administratora czy programistów. Wspomnę jeszcze, że CVS istnieje
również w wersji dla Windows co umożliwia wygodną pracę nad wielosystemowymi
projektami programistom używającym różnych systemów operacyjnych.

Co i skąd ?
Lista dyskusyjna dotycząca oprogramowania CVS:
info-cvs@gnu.org (zapisy pod adresem: info-cvs-request@gnu.org)
CVS (system kontroli wersji)
http://download.cyclic.com/pub/
RCS (system kontroli rewizji)
ftp://ftp.task.gda.pl:/pub/gnu/rcs-5.7.tar.gz
LinCVS (graficzna nakładka na CVS wykorzystująca interfejs Qt)
http://www.iapp.de/~trogisch/linux/lincvsen.html
tkcvs (graficzna nakładka na CVS wykorzystująca interfejs Tk)
http://www.teleport.com/~mokuren/
cvsweb (bramka CVS -> WWW jako skrypt CGI w perlu)
http://www.freebsd.org/~fenner/cvsweb/
cvsweb (rozbudowana wersja poprzedniego skryptu)
http://linux.fh-heilbronn.de/~zeller/cgi/cvsweb.cgi
Przykładowy serwis: http://cvs.pld.org.pl/
viewcvs (bramka CVS -> WWW jako skrypt CGI w pythonie)
http://www.lyra.org/greg/python/viewcvs/
Przykładowy serwis: http://cvsweb.pld.org.pl/
cvs2cl (odpowiednik rcs2log służący do generowania plików ChangeLog
napisany w perlu)
http://www.red-bean.com/~kfogel/cvs2cl.shtml

O autorze:
Autor jest studentem informatyki na Politechnice Wrocławskiej
oraz członkiem zespołu PLD GNU/Linux.
Z autorem można się skontaktować pod adresem: misiek@pld.org.pl

Przykłady

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