Jak psát lokalizované programy [cs] (HowTo write localised programs)
Monday, June 1, 1998 9:59:32 PM
Tento článek vyšel v Linuxových novinách č. 06/98 v červnu 1998, jeho spoluautorem je Vladimír Michl.
Jak psát lokalizované programyMichael Mráka a Vladimír Michl, 9. června 1998.
Tento článek by měl přinést (pokud možno jednoduchý) návod, jak psát programy snadno použitelné v různých jazykových mutacích.
Prvním předpokladem pro správné chování takového programu je fungující
lokalizace. Většina současných distribucí Linuxu je vystavěna nad
knihovnami glibc, které by již s lokalizací neměly mít
problémy. Zda je tomu skutečně tak, lze zkontrolovat pohledem do
adresáře /usr/share/locale, který by měl obsahovat podadresář
cs_CZ (na mém počítači je to cs_CZ.ISO-8859-2) a v něm
soubory
LC_COLLATE,
LC_CTYPE,
LC_MONETARY,
LC_NUMERIC,
LC_TIME
a adresář LC_MESSAGES. Pokud tomu
tak není, ale v systému existuje alespoň definiční soubor cs_CZ
(bývá standardně zahrnut do balíku glibc jako
/usr/share/i18n/locale/cs_CZ), je možné lokalizační soubory pomocí
příkazu
$ localedef -i cs_CZ -f ISO-8859-2 cs_CZ.ISO-8859-2
vygenerovat. Pokud systém nevlastní ani uvedený definiční soubor, je vhodné začít například na adrese ftp://ftp.fi.muni.cz/pub/localization/locale (Definiční soubory pro české locales), kde najdete více informací :-).
#include <locale.h>
#include <libintl.h>
#include <stdio.h>
#include <string.h>
#include <ctype.h>
#include <time.h>
#include <monetary.h>
#define LOCALEDIR "/usr/share/locale"
#define PACKAGE "lc_example"
#define BUF 80
#define _(str) gettext(str)
void init() {
setlocale(LC_ALL,"");
bindtextdomain (PACKAGE, LOCALEDIR);
textdomain (PACKAGE);
}
void invite() {
printf(Hello!\n);
}
void lc_collate() {
#define N 5
char *words[] = {"plaňka", "pláně", "Plášil", "cikáda", "chroust"};
char *p;
int i,j;
for (i = 0; i < N; i++)
for (j = 0; j < N; j++) {
if (strcoll(words[ i ], words[ j ]) < 0) {
p = words[ i ];
words[ i ] = words[ j ];
words[ j ] = p;
}
}
printf(Sorted words:);
for (i = 0; i < N; i++)
printf(" %s", words[ i ]);
printf("\n");
}
void lc_time() {
char buf[BUF];
time_t t = time(NULL);
strftime(buf, BUF, "%A", localtime(&t));
printf(%s - I hate it!\n, buf);
}
void lc_ctype() {
char buf[BUF];
int i;
printf(Wrote some text, please:\n);
fgets(buf, BUF, stdin);
for (i = 0; i < BUF && buf[ i ] != '\0'; i++) {
if (isalnum(buf[ i ]))
buf[ i ] = '+';
else if (isspace(buf[ i ]))
buf[ i ] = '_';
else
buf[ i ] = '-';
}
printf("%s\n", buf);
}
void lc_numeric() {
printf(Numbers: %'d %'f\n,
(int) 12345678, (float) 1234567.1235678);
}
void lc_monetary() {
char buf[BUF];
strfmon(buf, BUF, , (double) 1234);
printf("%s", buf);
}
main(int argc, char **argv) {
init();
invite();
lc_collate();
lc_time();
lc_numeric();
lc_ctype();
lc_monetary();
}
Příklad lc_example.c
Nyní přistupme k tvorbě vlastního programu lcexample. Nejprve
je potřeba program přimět, aby používal lokalizaci; to provedeme
pomocí funkce setlocale(LC_ALL, "") (viz funkce init())
--- parametry uvedené v příkladu sdělí programu, aby použil nastavení
určené proměnnými prostředí LC_* a LANG. Dále již stačí
používat standardní knihovní funkce pro práci s lokalizovaným
prostředím.
Pro jednotlivé kategorie jsou k dispozici následující funkce:
LC_COLLATE --- lexikografické třídění (viz funkce lc_collate()).
int strcoll(const char *s1, const char *s2)- porovnávání řetězců s ohledem na lokalizaci; syntaxe a návratové hodnoty odpovídají funkci
strcmp(). size_t strxfrm(char *dest, const char *src, size_t n)- transformace řetězce tak, aby porovnání dvou takto vzniklých řetězců pomocí
strcmp()mělo stejný výsledek, jako porovnání původních pomocístrcoll().
LC_CTYPE --- rozdělení znaků do tříd (malá a velká písmena,
oddělovače, čísla, bílá místa, ...) (viz funkce lc_ctype()).
int isalpha (int c)- písmena
int isascii (int c)- 7-bitová unsigned char hodnota z ASCII.
- (podobně
iscntrl(),isdigit(),isgraph(),islower(),isprint(),ispunct(),isspace(),isupper(),isxdigit())
LC_TIME --- časové údaje (viz funkce lc_time()).
size_t strftime(char *s, size_t max, const char *format, const struct_tm *tm)- nahradí sekvence
%xv řetězciformatčasovými údaji podle časové zóny a zvolené lokalizace. Např.%A= den v týdnu,%a= zkratka dne v týdnu,%B= měsíc, ... "Nahrazuje" funkcectime(),asctime().
LC_NUMERIC --- formátování čísel (viz funkce lc_numeric()).
int printf(const char *format, ...)- pokud
%-sekvence pro čísla obsahují apostrof (např.%'d) vytiskne oddělovač desetinných míst a tisícovek s ohledem na lokalizaci (nefunguje až vlibc.5).
LC_MONETARY --- formátování peněžních informací (viz funkce lc_monetary()).
ssize_t strfmon(char *s, size_t maxsize, const char *format, ...)- nahradí sekvence
%xpeněžními údaji podle zvolené lokalizace. Ve formátovacím řetězci lze zadat:%i= mezinárodní symbol měny s peněžní částkou (CZK),%n= národní symbol měny s peněžní částkou (Kč). Peněžní částky musí být čísla typudoublenebolong double. O této funkci se bohužel v info dokumentaci nedozvíte, navíc vlibc.5tato funkce vůbec nebyla. Více informací naleznete v souborustrfmon.man3.
Pokud by pro některou z kategorií neexistovaly standardní funkce, je
možné formátování udělat "ručně" za pomocí struktury lconv
získané z funkce localeconv().
Samostatnou kapitolu pak tvoří překlad (nejen chybových) zpráv
programu. V systémech s libc.6 (glibc) se provádí pomocí
balíku gettext. (Ve starších systémech s libc.5, případně
na jiných operačních systémech se k tomuto účelu používají funkce gencat; více viz
Czech-HOWTO.)
To, že program má používat katalog s českými překlady zpráv (kategorie
LC_MESSAGES), už akceptoval při volání setlocale(); je
ovšem potřeba ještě specifikovat jméno katalogu. To uděláme na začátku
programu voláním funkcí bindtextdomain(package, localedir) ---
cesta ke katalogu --- a textdomain(package) --- jméno katalogu,
který používáme (viz funkce init()). Všechny řetězce, které
mají mít svůj ekvivalent v katalogu, je nutné "prohnat" funkcí gettext() (viz funkce invite()).
Ještě zbývá napsat české překlady zpráv. Nejprve je potřeba pomocí
programu xgettext vygenerovat katalog obsahující všechny
originální zprávy. Ve vzorovém programu byl k tomu použit příkaz
$ xgettext --default-domain=lc_example \ --output-dir=. --add-comments --keyword=_ lc_example.ckde
--default-domain=lc_example- -- ulož výsledek do souboru lc_example.po
--output-dir=.- -- cílový adresář
--add-comments- -- napiš, kde se jednotlivé řetězce vyskytují
--keyword=_- -- místo gettext použij klíčové slovo _
Nyní lze ke každému řetězci připsat za klíčové slovo msgstr
příslušný překlad. Pokud by váš katalog byl větší je velmi výhodné
používat na překlad Emacs v tzv. po-módu (potřebný kód do Emacsu je
přiložen v balíku gettext). Hlavní výhoda je, že po-mode umí na
požádání zobrazit přímo místo zdrojového kódu, kde se zpráva nachází
(klávesa s). Dále umožňuje kontrolu, zda jsou zprávy dobře
přeloženy (zda sedí počet formátovacích značek, zda zprávy začínají
a končí novým řádkem stejně), automatické vyplnění úvodní hlavičky
i s datem revize (vše Shift-v). Samozřejmě editovat překlad lze
pomocí klávesy Enter, ukončení Ctrl-c Ctrl-c. Pokud se vám
v katalogu objeví zpráva označená jako "fuzzy"}, pak by tato zpráva
měla být neúplně přeložena. Toto označení lze odstranit klávesou
Tab. Zprávu lze jako "fuzzy" označit pomocí BackSpace.
Pro instalaci po-módu je třeba umístit soubor po-mode.el (po-mode.elc) do adresáře /usr/share/emacs/site-lisp a následně
do souboru $HOME/.emacs přidat:
(setq auto-mode-alist
(cons '("\\.po[tx]?\\'\\|\\.po\\." . po-mode)\
auto-mode-alist))
(autoload 'po-mode "po-mode")
Máme tedy přeložen celý katalog a pomocí
$ msgfmt -v -o lc_example.mo lc_example.po
ho přeložíme do binární podoby. Pak už stačí jen přesunout lc_example.mo do adresáře /usr/share/locale/cs_CZ/LC_MESSAGES, zkompilovat program a vyzkoušet.
$ gcc -o lc_example lc_example.c $ LC_ALL=cs_CZ.ISO-8859-2 ./lc_example Dobrý den! Setříděná slova: cikáda chroust pláně \ plaňka Plášil Úterý - to nesnáším! Čísla: 12 345 678 1 234 567,125000 Napište nějaký text, prosím: Umíš česky, ježku? ++++_+++++-_+++++-_ Zapomněl jste zaplatit 1 234,00Kč autorovi \ tohoto programu. $ LC_ALL=en_US ./lc_example Hello! Sorted words: chroust cikáda Plášil \ pláně plaňka Tuesday - I hate it! Numbers: 12,345,678 1,234,567.125000 Wrote some text, please: Umíš česky, ježku? ++--_-++++-_++-++-_ You have forgotten to pay $1,234.00 to the \ author of this program.
Je vidět, že funkce strfmon() zatím není dokonalá (zbytečná
mezera na začátku, mezi částkou a Kč není mezera, ač by podle definice
v souboru cs_CZ být měla), stejně jako funkce printf()
(desetinná čísla nejsou dělena do skupin). Časem se ale tyto chyby
určitě vyladí.
A na závěr ještě jedna užitečná funkce, která se může při lokalizaci hodit a to
#include <langinfo.h> char *nl_langinfo(nl_item item);
Tato funkce vrátí hodnotu, kterou po ní požadujeme -- např: první den
v týdnu nl_langinfo(DAY_1) nebo první měsíc v roce nl_langinfo(MON_1).
Argumenty které můžete požadovat naleznete
v souboru /usr/include/langinfo.h.