- předchozí článek - následující článek - obsah - úvodní stránka -

Linuxové noviny 06/98

Jak psát lokalizované programy

Michael 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, 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, _("You have forgotten to pay %n to\
the author of this program.\n"), (double) 1234);
   printf("%s", buf);
}

main(int argc, char **argv) {
   init();
   invite();
   lc_collate();
   lc_time();
   lc_numeric();
   lc_ctype();
   lc_monetary();
}

Výpis č. 3: Příklad lc_example.c

Nyní přistupme k tvorbě vlastního programu Příklad lc_example.c. 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 %x v řetězci format č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" funkce ctime(), 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ž v libc.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 %x peněž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 typu double nebo long double. O této funkci se bohužel v info dokumentaci nedozvíte, navíc v libc.5 tato funkce vůbec nebyla. Více informací naleznete v souboru strfmon.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 ftp://ftp.fi.muni.cz/pub/linux/local/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.c
kde
  • --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. *


- předchozí článek - následující článek - obsah - úvodní stránka -