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/

komentáře

RSS Komentáře k článku RSS Komentáře   Add to Google
30.11.2009 12:28 | Anonym (Jakub Vrána) | Plurály

Já jako identifikátor používám anglický text nezávislý na počtu, tedy např. „%d window(s)“. K identifikátoru může existovat i anglický překlad respektující počet, tedy „%d window“ a „%d windows“. Pro angličtinu se tedy mohou překlady tvořit stejně jako pro ostatní jazyky a fallback funguje také stejně.

01.12.2009 10:25 | Administrátor | Re: Plurály

jde mi o kontext, nebo o to že časem se význam identifikátoru změní a pak může být zavádějící %d window na místě, kde by třeba podle vývoje mělo být popup, to byl opravdu rychlý nástřel, nebo menu není vždy jídelníček a nebo výpis kategorií nebo rozbalovacího menu, bojím se toho že se změnami aplikace by si programátor mohl vytvořit svůj vlastní slovník pro programátora, a překládal by identifikátory do podoby identifikátorů kterým rozumí

01.12.2009 12:37 | Anonym (Miloslav Ponkrác) | Re: Re: Plurály

Přesně tak. Není třeba míchat identifikátor a překlad, byť jen naznačený.

30.11.2009 15:23 | Anonym (Miloslav Ponkrác) | Také si myslím, že identifikátor ála gettext není vhodné řešení

Také si myslím, že identifikátor ála gettext není vhodné řešení. Protože často se stává, že tentýž string je třeba přeložit několika různými texty podle kontextu.

Po čase jsem proto začal dávat přednost identifikátorům typu REG_NAME, AUTHOR_NAME, apod., který vyjadřují účel, nikoli to, co by se v textu mělo objevit. Odpadne tím spousta problémů.

Jinak překlady řeším v databázi, sice ne v sqlite. Ale dobrá databáze s dobře navrženou strukturou je pekelně rychlá, určitě rychlejší, než parsování xml.

Překlady držím v jiné tabulce, než základní data, většinou s unique indexem (id_dat, language).

01.12.2009 10:28 | Administrátor | Re: Také si myslím, že identifikátor ála gettext není vhodné řešení

ta databáze mi nejde pořád do hlavy :) vím že už jsme se na toto téma několikrát nepohodli, např. v článku http://www.webfaq.cz/…je-to-stejne

to jako myslíš, že pro každý překlad položíš jeden sql dotaz? tedy kvůli překladům např. 100× se zeptáš databáze na každé stránce? což nemusí být vůbec nic neobvyklého!

nedokážu si představit, že je to rychlejší než asociativní pole

01.12.2009 12:47 | Anonym (Miloslav Ponkrác) | Re: Re: Také si myslím, že identifikátor ála gettext není vhodné řešení

Základem každého řešení je dobrá architektura programu/skriptu i struktura databáze.

Obvykle kvůli překladu neprovedu ani jeden SQL navíc, než bych provedl bez lokalizace. V krajním případě, když není zbytí jeden až dva SQL dotazy navíc v horším případě.

Bohužel někteří lidé, jako třeba pan Vrána tu šíří takové věci jako udělat si SQL dotazy tak, aby byly pro programátora co nejjednodušší, a přizpůsobí tomu i strukturu tabulek. Pak není divu, že výkon není takový, jaký by mohl být.

Obvykle si navrhnu strukturu aplikace tak, aby databáze mohla načíst všechny potřebné překlady najednou. Nedělá to žádnou práci. Když načítám data z databáze, ve stejném SQL k nim načtu i překlad.

01.12.2009 12:58 | Administrátor | Re: Re: Re: Také si myslím, že identifikátor ála gettext není vhodné řešení

dobrý ale není tedy databáze pouze určitá forma cache? přece vytváříš z dtb asociativní pole a moje navrhované řešení vytváří asociativní pole již jako kešovanou verzi php souboru kterou pouze includuji, viz ukázky ke stažení

01.12.2009 14:06 | Anonym (Jakub Vrána) | Re: Re: Re: Také si myslím, že identifikátor ála gettext není vhodné řešení

Struktura databáze by měla být dobře rozšiřitelná, měla by být výkonná a když umožní psát snadno SQL dotazy, tak tím lépe.

Podobné narážky prosím příště včetně odkazu, který je podloží.

01.12.2009 14:12 | Administrátor | Re: Re: Re: Re: Také si myslím, že identifikátor ála gettext není vhodné řešení

nešlo by reagovat na příspěvek, který naráží? :) nejsem si vědom, že já bych na někoho nějak narážel

01.12.2009 14:17 | Anonym (Miloslav Ponkrác) | Re: Re: Re: Re: Re: Také si myslím, že identifikátor ála gettext není vhodné řešení

Já jsem narážel na pana Vránu a to bylo na mě.

01.12.2009 14:15 | Anonym (Miloslav Ponkrác) | Re: Re: Re: Re: Také si myslím, že identifikátor ála gettext není vhodné řešení

Priorita databáze by neměla být snadnost psaní SQL dotazů, to nikdy a v žádném případě. Pokud to tak vyjde, že jsou snadné SQL dotazy, je to dobře, ale je to jen vítaný side effect.

Na Vaše řešení ohledně lokalizace, například na článku na interval.cz jsme Vás nejen já, ale i další člověk upozorňovali, že není moc dobré. Bohužel interval.cz pak smazal všechny staré komentáře (smazal je u všech článků při přestavbě webu), nebo je někam přesunul.

Protože stále šíříte své teze o důležitosti hlavně jednoduchých SQL dotazů, přizpůsobujete otmu řešení, já považuji za povinnost upozornit na naprostou zcestnost a špatné výsledky takového přístupu u jakéhokoli databázového projektu. JAKÉHOKOLI. Je to špatný přístup.

01.12.2009 15:59 | Anonym (Jakub Vrána) | Re: Re: Re: Re: Re: Také si myslím, že identifikátor ála gettext není vhodné řešení

Opět vidím nařčení bez jakéhokoliv odkazu, který by ho dokládal. Svůj postoj jsem shrnul v předchozím příspěvku, tvá interpretace je mylná.

01.12.2009 14:06 | Anonym (Miloslav Ponkrác) | Je

V zásadě je databáze vždy jen formou cache, od toho byla vždy.

Každé řešení má něco do sebe – záleží na použití a kontextu. Pro statická řešení (tedy stále stejný počet položek) a malý počet položek je asociativní pole celkem príma.

Naproti tomu databázi nedělá problém mít obrovský počet položek, se kterou bude pracovat plus mínus stejně efektivně jako třeba s deseti položkami.

Databáze bude mnohem lépe udržovatelnější. Zejména pokud v každé stránce potřebuji jinou podmnožinu překládaných slov, začne být databázové řešení nesmírně efektivní i po stránce aministrace/údržby překladů.

A další věc je, že většina stránek do té databáze sahá tak jako tak, takže když se vhodně navrhne databázové uložení, pak překlad není pro databázi téměř žádná práce navíc.

Já netvrdím, že databáze a jedině databáze. Jsem také ovlivněn tím, že jsem byl databázový administrátor, který databáze zná. Dokážu si představit, že běžný člověk se snaží najít nedatabázové řešení. Třeba pole/xml/soubor/sez­nam konstant/šablo­ny/cokoli.

Pokud máte stránku řekněme s několika sty překlady, které jsou potřeba a je to neměnná množina, pak není problém nasadit asociativní pole a bude to dobré řešení.

Problém databází je, že je třeba trochu znát teorii a rozumět jim, jinak vyjdou dosti pomalá řešení.

Bohužel je pravda, že pokud budete dělat db řešení tak, aby tabulky vyšly co nejjednodušší a dobře se Vám s nimi pracovalo a vyšly nejjednodušší dotazy, tak sice katastrofa to nebude, ale výkon db jde většinou snadno akcelerovat ještě o několik řádů výše zejména u rozsáhlých dat.

Čím máte větší data, tím budou výhody databáze zřejmější. Obávám se, že tak asi od 1000 položek už sotva obhájíte cokoli proti databázi. Rychlost, snadno přístupu, udržovatelnost i flexibilita změn překladů prostě bude hrát pro databázi.

01.12.2009 14:22 | Administrátor | Re: Je

Moje řešení, které navrhuji spočívá (a je to snad v článku uvedeno):

  1. v centralizaci překladů v databázi (trochu ještě o úroveň výš co se týče správy)
  2. centralizovaná verze umožňuje aktulizovat distribuovaným implementacím aplikace překlady a navíc je upravovat dle své libosti
  3. při každé aktualizaci se automaticky vytvoří asociativní pole z databáze
  4. měřil jsem to při 1000 překladech a nevidím sebemenší zpomalení, opět nedá se to měřit (samozřejmě sofistikovanými nástroji ano, ale pohybujeme se v desítkách milisekund – ani ne)
  5. moje řešení i předestírá metodu, kdy se generuje gettext z dtb místo pole
  6. stále nechápu to vyhledávání v databázi jsou pouze dvě metody:

a. jedním dotazem si stáhnu celý obsah potřebných slov a vytvořím asociativní pole (jsem tam kde jsem byl) b. s každým překladem hledám v databázi, čímž se množí počet dotazů do dtb a to už je jiná pohádka, kterou jsme spolu už jednou řešili (stále trvám na tom, že transakční náklady na neustálé dotazování do dtb jsou vysoké)

pochopte prosím, nechci si měřit pindíky, ale opravdu mi jde o nejvhodnější řešení za takové a takové situace, osobně už bohužel/bohudík nejsem běžným programátorem, který bere každou zakázku a tak bohužel/bohudík už nespatřuji tu koloritu každé jiné aplikace, ale už se soustředím pouze na určitý směr aplikací (proto nehledám řešení toho co řešit v příštích 10 letech nebudu, i když ocením když tady o tom bude zmínka a směr, kde se o tom dozvědět více), pojďme si říct pro co je jaké řešení vhodné a proč a pro koho a jestli vůbec se s tím můžeme setkat nebo alespoň více než 5% programátorů

P.S.: gettext by měl být vhodný od 5000 překladů, jestli jeho vhodnost končí na 10000 nevím, ale bude někdy nekdo z nás řešit takovou aplikaci? stojí za to to usílí s optimalizací databází a hledáním řešení přes databáze?

P.S.2.: mimochodem článek je hlavně o tom, jak řešit překlady aniž bych v budoucnu změnou metody si nějak ublížil

04.12.2009 05:31 | Anonym (David Grudl) | Re: Je

Nojo, huba plná keců, povýšenectví, machrování, útočení, ale ani jeden praktický příklad, jako obvykle :))

01.12.2009 14:09 | Anonym (Miloslav Ponkrác) | plurály

Mě by třeba zajímalo, jak řešíte ty plurály, zda opravdu stačí jen 3 formy, a zda existuje nějaký jazyk, kde je třeba to řešit nějak složitěji. Jsem celkem jazykový analfabet a kromě češtiny a angličtiny jsem nic moc jiného nepoznal. Zda to opravdu algoritmicky stačí.

01.12.2009 14:36 | Administrátor | Re: plurály

jak gettext tak moje navrhované řešení s asoc. polem umožňuje definovat libovolné množství pluralu, zapisuje se tam forma pluralu, tedy od X je to tato forma, od XY tahle atd.

každopádně podle mne by to mělo stačit, v kolika jazykových verzích je linux? a používá gettext. samozřejmě nejzajímavější je japonština, protože tam se s počtem mění i pořadí slov a způsoby oslovení atd. pak by prý měla být zajímavá ruština, protože tam se to počítá nějak po desítkách, ale co se týče množství pluralu mělo by se to vejít do tří

04.12.2009 06:07 | Anonym (David Grudl) | Kterou cestu zvolit?

Všechny vyjmenované případy jako XML, INI, assoc pole, gettext nebo databáze jsou především úložiště. Pro potřeby překladů mají podobnou vypovídací schopnost. Vše se parsuje bleskově, navíc lze poměrně snadno jedno úložiště transformovat do druhého, třeba jako formu cache.

Co může programátora pálit, je

  • pohodlí editace
  • schopnost načíst jen část svého obsahu

Zde hrají prim databáze. Je samozřejmě otázkou, jestli jejich výhody v aplikaci oceníme, tj. jestli chceme mít překlady statické, nebo nabídnout uživatelům editaci, jestli má překlad tolik položek, že načítat je vždy všechny je plýtváním časem i pamětí.

Pro geekovštější aplikace je zase elegantní XML, INI, PHP (rádi takové věci editují) rozdělené do více souborů (řeší problém s načítáním jen části obsahu).

Pokud jde o počet plurálů, nejčastější jsou tři, nejvíce asi čtyři (slovinština).

Jméno
Název
Text
Lze používat Texy! syntax. Příklad syntaxe: "text odkazu":odkaz, **tučně**, *kurzíva*, `code`. PHP kód uzavírejte do <?php ... ?> a JavaScript do <script> ... </script>