Internacionalizace a lokalizace v C++

Ódy žlutého koně

Opraváři prý zkoušeli dálnopisy na větě quick brown fox jumps over the lazy dog. Tato věta obsahuje všechna písmena anglické abecedy a zkontrolovat, zda se napsala správně, je snazší, než kontrolovat jednotlivá písmena. V češtině se k podobnému účelu používá věta žluťoučký kůň příšerně úpěl ďábelské ódy, ta obsahuje všechna česká písmena s diakritickými znaménky.

V tomto článku se nebudeme zabývat dálnopisy, ale programy v C++ a jejich zacházením s neanglickými jazyky a s neanglickým národním prostředím. Přitom si pochopitelně všimneme zvláště češtiny a českého národního prostředí a jako příklad přitom budeme používat zmíněnou větu o žlutém koni.

Budeme tedy hovořit o nástrojích, které usnadňují internacionalizaci, resp. lokalizaci programů. (Záleží na úhlu pohledu: Chce-li autor svůj program prodat do zahraničí, musí ho internacionalizovat, kupující, který chce program přizpůsobit místním zvyklostem, ho lokalizuje.)

O co jde

Na první pohled by se mohlo zdát, že problémy s používáním národního jazyka (tedy libovolného jazyka jiného než angličtiny) v programech končí zvládnutím patřičné kódové stránky, věc je ale složitější: Má-li se program chovat podle místních zvyklostí, musí vedle čtení a zápisu znaků odpovídající abecedy umět také

Nejmodernější programovací jazyky, jako je Java nebo C#, tyto problémy umějí řešit. V jazycích C a C++ to v počátcích tak samozřejmé nebylo. Ovšem jazyk C++ podle standardu ISO 14882 z r. 1998 už obsahuje řadu nástrojů, které umožňují tyto problémy zvládnout. Jde především o

Při jejich používání ale můžeme narazit na řadu problémů. Nejobvyklejší z nich je, že tyto nástroje jsou v překladačích implementovány s chybami nebo nejsou implementovány vůbec. Nicméně to se může v dohledné době změnit, proto je nezbytné je znát.

Lokální nastavení a jeho jméno

Třída locale je definována ve standardní hlavičce locale. Má řadu konstruktorů, z nichž asi nejdůležitější má jako parametr znakový řetězec obsahující jméno lokálního nastavení:

explicit locale( const char* _Locname );

Zadáme-li nesprávný řetězec nebo ukazatel s nulovou hodnotou, vyvolá tento konstruktor výjimku typu runtime_error.

Jméno lokálního nastavení
Zde ovšem narazíme na první problém: Jména lokálních nastavení nejsou standardizována. Obvykle mají tvar

Jazyk[_stát[.kódování]]

Lomené závorky zde označují části, které lze vynechat – pak se doplní implicitní hodnota daná překladačem a operačním systémem. Často se ovšem používají i různé jiné zkratky. Například pro češtinu se můžeme setkat s řetězci "czech", "csy", "cs_CZ" a jinými, pro slovenštinu lze vidět řetězce "slovak" nebo "sky". Pro angličtinu najdeme "en_UK" (Velká Británie), "en_US" (Spojené státy) nebo "en_AU" (Austrálie). Ne všechny řetězce jsou ale k dispozici ve všech prostředích.

Protože jména lokálního nastavení nejsou, jak už víme, standardizována, musíme odpovídající informace zpravidla hledat v dokumentaci k překladačům. (Ta nás ovšem neméně často odkáže na dokumentaci k operačnímu systému.)

Vedle řetězců, představujících různé jazyky a státy, můžeme konstruktoru třídy locale předat řetězec "C", který označuje tradiční nastavení v jazycích C a C++, nebo prázdný řetězec "", reprezentující „uživatelem preferované nastavení“, tedy zpravidla implicitní nastavení na daném počítači. Nejběžnější komerční překladače na PC pod českými Windows při této volbě nastaví české prostředí s kódovou stránkou 852 nebo 1250.

Další možností může být řetězec "POSIX", jehož význam závisí na implementaci.

Zjišťujeme jméno lokálního nastavení
Úplné jméno lokálního nastavení lze zjistit pomocí metody name(). Jestliže např. napíšeme ve Visual C++ .NET následující program,

#include <iostream>
#include <locale>
#include <stdexcept>
using namespace std;
int main()
{
        try{
                locale L1("czech");
                cout << L1.name() << endl;
        }
        catch(runtime_error &e)
        {
                cout << e.what() << endl;
        }
        return 0;
}

dozvíme se, že úplné jméno lokálního nastavení pro češtinu ve Windows NT nebo Windows 2000 je

Czech_Czech Republic.1250

Toto jméno samozřejmě můžeme také použít pro zadání místního nastavení. Vedle kódové stránky 1250 v něm můžeme specifikovat i kódovou stránku 852, která se používá při výstupu na konzolu. Úplné jméno odpovídajícího nastavení je Czech_Czech Republic.852.
Jestliže zadáme např. řetězec Czech_Czech Republic.88592, vznikne výjimka a program vypíše např.

bad locale name

nebo

System does not recognize this locale name

neboť takovéto lokální nastavení není k dispozici. (Kódování ISO 8859-2 se skrývá pod označením "Czech_Czech Republic.28592", čeština pro počítače MacIntosh je k dispozici pod jménem "Czech_Czech Republic.10029").
Použijeme-li v konstruktoru třídy locale řetězec "slovak", zjistíme, že úplné jméno nastavení pro slovenštinu je

Slovak_Slovakia.1250

Řetězec "german" způsobí, že program vypíše

German_Germany.1252

zadání "english" dá výsledek

English_United States.1252

Poznamenejme, že v řetězci, který je parametrem konstruktoru třídy locale, může záležet na velikosti písmen. Řetězce "czech" a "Czech" sice v překladači pro Windows znamenají totéž, ale "C" znamená nastavení obvyklé v jazycích C++ a C, zatímco "c" znamená katalánštinu ("Catalan_Spain.1252").

Používání lokálního nastavení
Pokud znáte práci s lokálním nastavením v jazyce C, víme, že v něm lze pomocí funkce setlocale() ze standardní hlavičky locale.h předepsat lokální nastavení pro celý program. Tím určíme způsob psaní desetinné tečky nebo čárky při výstupu pomocí funkcí z rodiny printf(), formát data a času vráceného pomocí funkce strftime() a některé další věci.
V jazyce C++ můžeme na – rozdíl od jazyka C – pracovat s několika různými lokálními nastaveními. Dále uvidíme, jak je lze použít k porovnávání řetězců, k ovlivnění různých datových proudů atd.

Chceme-li použít některé lokální nastavení jako globální (aby ho mohly používat mj. i funkce z jazyka C), použijeme statickou metodu global(), které jako parametr předáme instanci třídy locale představující nastavení, kterou chceme používat jako globální. Tato metoda zároveň vrátí předchozí nastavení. Například příkazy

locale LocNove("Czech_Czech republic.1250");
locale LocStare = locale::global ( LocNove );

určí jako globální nastavení popsané proměnnou LocNove a zároveň uloží do proměnné LocStare dosavadní nastavení.

Znaky národních abeced

Málokterý evropský národ vystačí s písmeny latinské abecedy bez diakritických znamének – háčků, čárek, akcentů atd. (Kromě angličtiny nás napadá snad jedině holandština.) Některé jazyky kromě diakritických znamének používají i zvláštní písmena, neznámá v jiných jazycích – např. v němčině je to ostré s (znak ß), v dánštině znak a atd. Je jasné, že rozumný program bude s uživatelem komunikovat v jeho jazyce, a k tomu potřebuje úplnou znakovou sadu jazyka.

Klasické řešení tohoto problému při použití „úzkých“ znaků (typu char) bylo založeno na kódových stránkách, tj. na tom, že znaky s kódy v rozmezí 128–255 měly v různých prostředích různý význam. Toto řešení šlo použít především pro evropské jazyky, neboť ty používají hlásková písma o malém počtu znaků. Ovšem pro jazyky z Dálného východu, které používají slabičná písma o velkém počtu znaků, nemělo význam. Tyto jazyky používaly kódování založené na tzv. vícebajtových znacích (multibyte characters), v nichž různé znaky mohly být kódovány různým počtem bajtů. Navíc některé bajty měly význam podobný přeřaďovačům na klávesnici – určovaly, který z několika možných významů bude následující bajt mít.

Od konce 80. let se stále důrazněji prosazuje vícebajtové kódování Unicode, které pokrývá snad všechny jazyky, je popsáno mezinárodním standardem ISO/IEC 10646 (ve kterém se ovšem označení Unicode neobjevuje).

Pro práci se znaky v kódování Unicode slouží v C++ datový typ wchar_t, který zabírá obvykle dva nebo čtyři bajty a často se o něm hovoří jako o „širokých“ znacích. Znakovou konstantu tohoto typu zapisujeme podobně jako „úzkou“ znakovou konstantu, pouze před ni připojíme prefix L. Podobně připojíme prefix L před řetězcové literály typu wchar_t.

Pro uložení znakových řetězců můžeme používat buď pole těchto typů nebo (raději) instance typu string, resp. wstring.

Při výstupu se ovšem musí znaky z kódování Unicode převést do kódování, používaného v oknech, v souboru nebo na konzole. K tomu je třeba specifikovat lokální nastavení, tedy zadat objekt třídy locale, který je popisuje.

Úzké a široké proudy

Než přejdeme k výstupu českého textu, musíme si říci několik slov o datových proudech.
Datové proudy pro široké znaky („široké proudy“) jsou – stejně jako datové proudy pro úzké znaky, tedy „úzké proudy“ – instance tříd, vzniklých specializací šablon basic_istream<>, basic_ostream<>, basic_fstream<> atd. Prvním parametrem těchto šablon je typ znaků, tedy char v případě úzkých proudů a wchar_t v případě širokých proudů. Druhým parametrem je třída rysů (traits), popisující vlastnosti znaků. Implicitní hodnotou druhého parametru šablon proudů je specializace šablony rysů char_traits<> pro první parametr.

Pro úzké proudy jsou pomocí deklarace typedef zavedena obvyklá jména – istream, ostream, fstream atd. Pro široké proudy jsou zavedena podobná jména, která ovšem začínají písmenem w.
Z toho, že jde o různé specializace téže šablony, plyne, že s oběma druhy proudů zacházíme prakticky stejně.

Poznamenejme, že i široké proudy v dnešních operačních systémech ukládají do souborů zpravidla úzké znaky a pochopitelně v nich také úzké znaky očekávají. Při převodu z kódování Unicode do úzkých znaků při zápisu a při opačném převodu při čtení je ovšem třeba znát lokální nastavení (kódovou stránku, v níž jsou úzké znaky zapsány). Tu určíme tak, že k proudu pomocí metody imbue() připojíme objekt třídy locale.

Výstup českého textu

Používáme-li úzké znaky, je výstup českého textu v podstatě jednoduchý: Musíme si připravit potřebný znakový řetězec v odpovídajícím kódování. Jsme-li ve Windows, je to komplikováno tím, že v oknech se používá kódová stránka 1250, zatímco při výstupu na konzolu kódová stránka 852. Vstupní a výstupní proudy si s oběma poradí bez problémů.

Používáme-li široké znaky, je věc na první pohled složitější, neboť musíme používat i proudy pro široké znaky, jako je wistream, wostream, wfstream atd. Na druhé straně ale získáme i více možností.

Jako příklad si vezmeme za úkol zapsat do souboru a vypsat na konzolu větu o žluťoučkém koni. Jak to udělat?
Nejprve deklarujeme znakové pole nebo řetězec obsahující požadovanou větu:

wchar_t cw[] = L"Žluťoučký kůň příšerně úpěl ďábelské ódy.";
wstring S(L"Žluťoučký kůň příšerně úpěl ďábelské ódy.");

Dále v programu deklarujeme dvě instance třídy locale, které budou popisovat národní prostředí s kódovými stránkami 1250 (pro soubor) a 852 (pro konzolové okno):

locale Kon("Czech_Czech Republic.852"); // Pro konzolu
locale Sou("Czech_Czech Republic.1250"); // Pro okna

Pro výstup na konzolu musíme použít „široký“ proud wcout. Jak víme, lokální prostředí k němu připojíme pomocí metody imbue():

wcout.imbue(Kon);

Pak už můžeme obsah pole cw bez problémů vypsat:

wcout << cw << endl;

Výpis do souboru není o nic složitější: Vytvoříme instanci třídy wofstream napojenou na odpovídající soubor a připojíme k ní odpovídající lokální prostředí:

wofstream F("data.dta");
F.imbue(Sou);
F << cw << endl;
F.close();

To je vše. Celý program může vypadat takto:

#include <iostream>
#include <stdexcept>
#include <fstream>
#include <locale>
using namespace std;
wchar_t cw[] = L"Žluťoučký kůň příšerně úpěl ďábelské ódy.";
int main()
{
        try{
                locale Kon("Czech_Czech Republic.852");
                locale Sou("Czech_Czech Republic.1250");
                wcout.imbue(Kon);
                wofstream F("data.dta");
                F.imbue(Sou);
                F << cw << endl; // Výpis v Cp1250
                F.imbue(Kon);
                F << cw << endl; // Výpis v Cp852
                F.close();
                wcout << cw << endl;
        }
        catch(runtime_error &e)
        {
                cerr << e.what() << endl;
        }
        return 0;
}

Soubor data.dta bude obsahovat větu o žlutém koni, ovšem v úzkých znacích a v kódování daném předepsanou kódovou stránkou.
Poznamenejme, že lokální prostředí, připojené k datovému proudu, můžeme zjistit voláním metody getloc(). Je-li např. L proměnná typu locale, můžeme napsat

L = wcout.getloc();

Proměnná L pak bude obsahovat lokální nastavení používané proudem wcout.

Vstup českého textu

Při vstupu ze souboru obsahujícího český text ve známé kódové stránce postupujeme podobně. Jestliže např. soubor data.dta obsahuje text

Ahoj

způsobí příkazy

wchar_t cw[100];
wifstream I("data.dta");
I.imbue(locale("czech"));
I >> cw ;

že pole cw bude obsahovat řetězec "Ahoj" v kódování UNICODE. Ovšem použití operátoru >> není příliš výhodné, neboť vstup skončí u první mezery. Kdyby náš soubor obsahoval větu o žlutém koni, přečetli bychom takto jen slovo "Žluťoučký". Daleko rozumnější je použít funkci getline(), kterou najdeme v hlavičkovém souboru string:

wstring S;
getline(wcin, S);

getline() je šablonová funkce, které lze jako parametry předat buď úzký vstupní proud a instanci třídy string, nebo široký proud a instanci třídy wstring. Tato funkce přečte z daného proudu (a s jeho lokálním nastavením) vše do konce řádku, uloží to do dané řetězcové proměnné a vrátí odkaz na předaný proud.

Příklad
Nyní už známe vše, co je potřeba, abychom mohli napsat filtr, který bude sloužit ke konverzi textových souborů z kódové stránky 1250 do kódové stránky 852. To znamená, že si můžeme rovnou ukázat zdrojový text:

// Filtr pro konverzi Cp1250 -> Cp852
// Program konver.cpp
#include <iostream>
#include <locale>
#include <string>
#include <stdexcept>
using namespace std;

int main(int argv, char *argc[])
{
        wstring ws;
        try {
                locale L1("Czech_Czech Republic.1250");
                locale L2("Czech_Czech Republic.852");
                wcin.imbue(L1);
                wcout.imbue(L2);
                while (getline(wcin, ws))
                {
                        wcout << ws << endl;
                        if(!wcout) wcerr << L"sorry" ;
                }
        }
        catch(runtime_error &e)
        {
                cerr << "Nepodporovane kodovani";
        }
        return 0;
}

Jak jistě víte, jako filtry označujeme programy, které se spouštějí z příkazové řádky a používají standardní vstup a výstup. Při spuštění je zpravidla přesměrujeme, aby četly ze souboru a případně vypisovaly výstup do souboru.
V tomto programu nejprve deklarujeme pomocnou proměnnou ws typu wstring (řetězec se širokými znaky). Pak vytvoříme objekt L1 s lokálním nastavením pro Cp1250, který připojíme k proudu wcin, a objekt L2 s lokálním nastavením pro Cp852, který připojíme k proudu wcout.

Nyní už zbývá jen v cyklu přečíst jednotlivé řádky vstupu a hned je zase vypsat. Ke čtení použijeme metodu getline(), která vždy přečte ze zadaného proudu celý řádek a uloží ho do dané instance třídy wstring. Tato metoda vrací odkaz na předaný proud, proto ji lze použít v podmínce cyklu while; čtení skončí, narazí-li na konec souboru.

Při čtení se text převede z kódové stránky 1250 do kódování Unicode, při výpisu se převede z Unicode do kódové stránky 852. (Není to nejefektivnější způsob převodu, je to ale programátorsky nejsnazší způsob, ovšem za předpokladu, že obě tato lokální nastavení jsou na použitém počítači k dispozici).
Vezmeme-li soubor hou.txt, obsahující v kódové stránce 1250 text

Žluťoučký kůň
příšerně úpěl
ďábelské ódy

a spustíme-li ho příkazem

konver < hou.txt > py.txt

dostaneme soubor py.txt, v němž bude stejný text v kódové stránce 852. To znamená, že pod Windows uvidíme něco jako

ÄluŁouŔkř k¨˛
p°ÝÜerný ˙pýl
´ßbelskÚ ˇdy

Konverze řetězců

I když obvykle v jednom programu používáme buď jen široké nebo jen úzké řetězce, občas přece jen potřebujeme použít obojí zároveň, pak se můžeme setkat s úlohou převést široký řetězec na úzký nebo naopak. K tomu slouží metody narrow(), resp. widen() třídy ctype, která je součástí lokálního nastavení. (Můžeme se na ni dívat jako na objektové zapouzdření funkcí z hlavičky ctype.h z jazyka C). Platí-li deklarace

wchar_t cw[] = L"Žluťoučký kůň příšerně úpěl ďábelské ódy.";
char cn[100];

převedeme pole cw širokých znaků na pole úzkých znaků v lokálním nastavení Kon podivně vypadajícím příkazem

use_facet<ctype<wchar_t> >(Kon).narrow(
cw, cw+sizeof(cw)/sizeof(wchar_t), '*', cn);

Význam šablonové funkce use_facet<>() si vysvětlíme později; zatím nám stačí vědět, že tato funkce vrátí instanci třídy ctype lokálního nastavení Kon. Pro vrácenou instanci třídy ctype pak zavoláme funkci narrow. Jako parametry jí předáváme

Poznamenejme, že vedle funkcí pro konverzi úseku pole jsou k dispozici i přetížené verze těchto metod, které konvertují jediný znak.
Funkci widen() používáme podobně, nemá však třetí parametr (implicitní znak). To znamená, že platí-li deklarace

wchar_t cw[100];
char cn[] = "Žluťoučký kůň příšerně úpěl ďábelské ódy.";

a je-li zdrojový kód zapsán v kódové stránce 1250, způsobí příkazy

locale L("Czech_Czech Republic.1250");
use_facet<ctype<wchar_t> >(L).widen(cn, cn+sizeof(cn)/sizeof(char), cw);

že pole cw bude obsahovat uvedený řetězec v kódování Unicode.
Poznamenejme, že standardní knihovna zřejmě neobsahuje nástroj pro převod mezi typy string a wstring.

Budete-li to zkoušet

Už jsme si řekli, že jedním z nejrozšířenějších problémů s národním prostředím je, že jeho implementace v současných překladačích obsahuje chyby. Může se vám např. stát, že program nebude vypisovat znaky s diakritickými znaménky, může se ale také stát, že nebude znát žádný z výše uvedených řetězců, určujících jména národních prostředí. Podívejme se na nejrozšířenější komerční překladače.

Například Borland C++Builder 4 zná zřejmě jen řetězce "" a "C", přičemž prázdný řetězec znamená totéž co "C". Borland C++Builder 5 již zná řadu jmen lokálních nastavení, metoda name() třídy locale vypíše hodnoty pro jednotlivé kategorie lokálního nastavení jazyka C. Navíc při pokusu vypsat do proudu wcout nebo do proudu pro výstup do souboru text obsahující znaky s diakritickými znaménky se program zhroutí (vznikne výjimka bad_alloc). Také převod mezi úzkými a širokými znaky jaksi nezvládá. Borland C++Builder 6 sice již správně vypisuje jméno lokálního nastavení, ale operace s českými širokými znaky stále nezvládá.

Microsoft Visual C++ 6 správně zachází se jmény lokálních nastavení a zvládá i výstup českých znaků proudem wcout na konzolu, nezvládá však výstup pomocí širokých proudů do souborů (alespoň pod Windows 95, neboť jinou instalaci tohoto překladače nemáme k dispozici). Vzhledem k tomu, že tento překladač neimplementuje šablony v plném rozsahu, je třeba při převodu mezi úzkými a širokými znaky místo šablonové funkce use_facet<>() použít makro _USEFAC().
Všechny příklady, které zde uvádíme, jsem odladili ve Visual C++ .NET.

Příště

Příště se podíváme na převod malých písmen na velká a naopak, na abecední řazení a na některé další operace.

Miroslav Virius