Jak na lokalizaci webové aplikace v PHP, aneb multijazyčnost webu

Článek není o databázové struktuře, překladu článků nebo o lokalizaci uživatelských dat, ale o lokalizaci uživatelského prostředí. Jak lokalizovat formuláře, chybové hlášky, oslovení, ovládání aplikace apod.


Určitě Vás již potkala úloha jak lokalizovat aplikaci do jiných jazyků, na úvod snad uvedu článek jednoho shitless scared software architect bloggera, odkaz na článek je dosti výmluvný a napovídá, že to není nic až tak lehkého.

Příspěvek se sice zabývá optimalizací a benchmarkem jednotlivých metod, ale věnuje se krátce i tomu, co Vás čeká a co je potřeba si rozmyslet:

  1. zda design aplikace je přizpůsoben nárokům jiných jazyků (zda texty nebudou rozsáhlejší, zda nebude potřeba větší písmo),
  2. co s texty na obrázcích? Měly by se dělat bannery v několika jazykových mutacích, jak je zaměňovat,
  3. co unicode podpora v PHP, už nelze více používat funkce jako strtr, str_replace apod. (nejsou unicode friendly do verze PHP6),
  4. co legislativní pravidla dané země? Je možné označit výrobek za nejlepší apod.?

A další problémy. Nás však bude zajímat především otázka metody lokalizace textů.

1. Metody lokalizace textu

Na výběr máme asociativní pole, xml dokument, ini soubor, načítání přímo z databáze (např. sql lite), gettext a snad i jiné (konstanty apod. - asi nemá smysl ani řešit).

1. ini soubor

Ten mi nevyhovuje z důvodu, že neexistuje standard pro zapsání plurálů, pravidel apod., resp. museli bychom vytvářet složitě vlastní parser. Proti tomu také mluví, že v PHP 5.3 už nelze za klíče použít cokoli jiného než ASCII stringy!

2. xml dokument

Myslím, že bude celkem rychlý, spravovat ho nebude tak těžké, bude mu rozumět i obyčejný smrtelník s trochou snahy a v neposlední řadě vytvořit parser bude na pár řádků a nedá to moc přemýšlení.

3. sql lite databáze

Tohle řešení mne ani nenapadlo, to se přiznám. V již zmiňovaném článku je uvedeno jako nejvýhodnější a je prý 5x rychlejší než xml parsování! To mne překvapuje a stále to nedokážu pochopit. Přisuzuji to okolnostem testu a pouze 3 stringům.

4. asociativní pole

To je jeden z mých favoritů, pravděpodobně bude rychlejší než xml, nepotřebujeme žádný parser, jen budeme muset vymyslet standard ukládání plurálů, pravidel a dalších překladů.

Za pomoci kešování a nějakého administračního nástroje je to celkem vhodný adept.

Nevhodnost tohoto nástroje se uvádí okolo 500-1000 položek v překladu, v jiných zdrojích se uvádí až 5000 (například Zend_Translator uvádí nevhodnost tohoto adaptéru od 5000).

5. gettext

Nativní podpora php, používaná standardně v distribucích linuxu a v dalších aplikacích, tedy ověřeno zubem času a vybráno odborníky.

Podle testů je porovnatelně rychlejší než asociativní pole. Hůře se spravuje a horší bude implementace vlastního administračního nástroje (z důvodu nutného převodu do binární podoby).

Další podstatnou výhodou je, že gettext by měl udržovat překlady ve sdílené paměti, tím by se podstatně mělo ulehčit opakované načítání dat do paměti.

2. Asociativní pole nebo gettext

Dále se budu věnovat jen metodám použití asociativního pole a gettextu.

Před samotným výběrem řešení, bychom si měli uvědomit, jaké nároky na překlad máme a jaké nároky si v budoucnu naše projekty vyžádají a především počítat s tím, že nároky se budou měnit. Měli bychom navrhnout takové řešení, které nám umožní využívat překlady a bez zásahu do aplikace (čti bez dalších nákladů) přejít na překlady pomocí ini, gettextu nebo čehokoliv jiného.

Položme si následující otázky:

Budu překlady zadávat rovnou do kódu? Do šablon? Bude nutné vyhledávat v hotové aplikaci výskyt klíčových spojení pro překlad? Bude více variací překladů?

Vzhledem k tomu, že pro své účely potřebuji lokalizovat aplikace, které jsou implementovány v desítkách různých verzí, které spolu sdílí cca 80% všech překladů a tyto aplikace je nutné neustále udržovat při životě, tedy vytvářet nové moduly, aktualizovat již stávající moduly, je nutné překlady spravovat centrálně a ty distribuovaně aktualizovat v jednotlivých implementacích stejné aplikace. Jediné řešení je spravovat překlady dynamicky, nejlépe v databázi, tedy výskyt translací v php kódu není důležitý. Potom výhody gettextu pozbývají významu až na rychlost, vhodnějším řešením proto může být asociativní pole.

V případě, že každá aplikace je ojedinělá, bude asi vhodné jednotlivá spojení pro překlad vyhledávat rovnou v kódu, potom nemůžu doporučit nic jiného než gettext.

Identifikátory překladu nebo samotný překlad? GLOBAL_NEWMESSAGE nebo You have %d new messages?

Výhodou gettextu (případně i jiných implementací, pokud to tak naprogramujeme) je, že pokud neexistuje konkrétní překlad, tak se do textu vloží samotný klíč (vyhledávané spojení). Což lze napodobit i ve vlastních implementacích jiných metod.

Tohle ovšem považuji za nevhodné, text samotných překladů se může měnit a hlavně v kontextu nemusí jít vždy o to samé. Navykl jsem si používat FORM_ERROR_NOTEMPTY, REGISTRATION_NAME, REGISTRATION_COMPANY, je zřejmé v jakém kontextu a kde bude překlad použit, možná dochází k určité duplicitě, ale ne nejasnostem a budoucím problémům.

Také mi vadí, že je nutné pro ngettext zadávat i tvar plurálu, použití pak vypadá takto:

  1. printf(ngettext("%d window""%d windows"5), 5);

Přitom by stačilo:

  1. $translator->_("WINDOW", 5);

Ať si plurál vyhledá translator sám.

Problém s reloadem gettextu? Jak překlad aktualizovat?

Na pár diskusích jsem objevil, že prý je problém s reloadem gettextu, prý je potřeba restartovat apache. Na lokálu tento problém mám, ale na serveru jsem ho nikdy nespozoroval, přesto bych viděl jako přínos ukládat si předchozí verze překladu pro možnost obnovení, tedy vhodné by bylo po každé změně soubor s překlady přejmenovat např. ve tvaru webYYMMDD_1.mo, což s sebou nese tu potíž, že aktuální název souboru se musí někam ukládat do dynamicky upravovatelné konfigurace.

Jak budu překlady spravovat? Vytvářet a upravovat?

U gettextu je to jasné, je na to několik nástrojů např. asi nejznámější PoEdit, nebo Gted, KBabel a další.

V mém případě je problém spravovat překlady aplikace, která je implementována v desítkách verzí, musí umožnit každé verzi si vlastní překlad upravit, musí umožnit budoucí aktualizace a rozšíření překladu stejně jako jeho dynamickou správu s prvky centralizace.

Jdu na to tak, že je nějaký centrální server, který obsahuje aktuální verzi překladů, ostatní verze aplikace si v určitém časovém intervalu nebo na manuální příkaz administrátora vyžádají z centrálního úložiště souhrnu překladů, lokální databázové úložiště pak jednoduše pomocí sql příkazu doplní nové překlady.

  1. INSERT INTO translations (id, singular, plural1, plural2, originalsingular, originalplural1, originalplural2) (.. data získaná z xml nebo jiným způsobem z centrálního úložiště..) ON DUPLICATE KEY UPDATE originalsingular=VALUES(originalsingular), originalplural1=VALUES(originalplural1), originalplural2=VALUES(originalplural2);

Tento sql dotaz umožní přidat nové překlady a k těm již existujícím pouze upravit originální, tedy aktuální tvary. Nijak to tedy nenaruší lokální překlad. Toto řešení samozřejmě předpokládá pouze jeden identifikátor překladu např. REGISTRATION_NAME.

Jak na implementaci translatoru

Například pro použití translatoru v Nette musíme implementovat rozhraní ITranslator, které obsahuje metodu translate s parametry message (zpráva) a count (počet).

V Nette pak už jen translator nastavíme pro formulář jako $form->setTranslator($translator); nebo šabloně jako $template->setTranslator($translator);.

Funkce pro překlad by volitelně mohla i umět zaměnit proměnné v samotném překladu, např. {__USERNAME__}  apod. Určitě musíme počítat s tím, že úložiště nebo-li metoda překladu musí být libovolně a volně zaměnitelná. Volil jsem tedy implementaci na základě adaptéru po vzoru Zend_Translate.

Ukázka třídy translatoru Translator.php, ukázka třídy adaptéru TranslatorAdapterArray.php a ukázka interface Adaptéru.

Nastavení translatoru pak vypadá takto:

  1. $translator=new Translator("cs", APP_DIR."/misc/locale/cs_CZ/LC_MESSAGES/web-translator-array.php", "array");
  2. nebo gettext
  3. $translator=new Translator("cs", APP_DIR."/misc/locale/cs_CZ/LC_MESSAGES/web-translator-gettext.mo", "gettext");

V případě array adaptéru, budete překlady ukládat v této formě.

  1. <?php
  2. return array (
  3. 'meta' =>
  4. array (
  5. 'Plural-Forms' =>
  6. array (
  7. 0 => 1,
  8. 1 => 4,
  9. 2 => 0,
  10. ),
  11. ),
  12. 'data' =>
  13. array (
  14. 'GLOBAL_NEWMESSAGE' =>
  15. array (
  16. 0 => 'Máte %d novou zprávu!',
  17. 1 => 'Máte %d nové zprávy!',
  18. 2 => 'Máte %d nových zpráv!',
  19. ),
  20. )
  21. );

Celý koncept je jen nástřel. Co na to říkáte?

Zdroje literatury

1. Benchmark, a pohled na lokalizaci aplikace

http://www.litfuel.net/plush/?postid=84

2. Na seznámení s gettextem to stačí

http://interval.cz/clanky/gnu-gettext-prvni-kroky/