Autor Zpráva
Virtus
Profil
Zdravím,
řeším takový problém ohledně překladů, rád bych použil gettext, protože současný systém je plný duplicitních překladů, díky kterému bychom se těchto duplicit zbavil. Nicméně gettext má jednu takovou (ne)příjemnou vlastnost a to sice, že po prvním nakešování se již nedá rozumně nijak překešovat a veškeré překlady bere z cache a tady nastává můj problém, protože nemůžu jednoduše restartovat webový server kvůli rekešování překladu.

Prvně asi napíšu co mám za zdroje:
2x www server, každý: 2x Intel Xeon E5-2620, 8x 4GB DDR3 1333MHz ECC, a nějaký 15k otáčkový disky řízený RAID adapterem od Adaptec, systém FreeBSD 9.2, nginx a php-fpm
1x upload server s podobným výkonem jako www server, systém FreeBSD 9.2, apache22 s modulem php
databáze má extra server jenom pro sebe s podobným výkonem jako www server
celková zátěž na www servery je někde mezi 10M-16M zobrazených stránek za den (rozděluje ji další extra proxi server, na kterým běží i další služby, jako redis atd.), upload server je co se uživatelského přístupu týče o proti www serverům na nějakém 1%, nicméně na něm běží prozměnu encodery videí/obrázků a spousta cronů, které nelze jen tak vypnout->zapnout.

A teď k samotnému problému, www/upload stránky jsou často aktualizovány(i několikrát za den) a to včetně textů, které se mají překládat. Překládaný text v současné době reprezentuje nějakých 5k klíčů, po předběžné analýze by se počet klíču mohl zmenšit až o 1k při použití gettextu, nicméně proto aby se mi aktualizace překladu projevili, je potřeba restartovat (www:php-fpm/upload:apache22), na virtuální testovací sestavě mám ozkoušený že se tyto služby nemusí restartovat úplně ale stačí jejich reload:
upload> service apache22 graceful
www> service php-fpm reload
což by mělo fungovat tak, že aktuální requesty co běží doběhnou normálně a dokud se služba nereloadne s novou konfigurací není další request spuštěn, zde bych měl tayk otázku a to sice některé crony na upload serveru čekají až skončí enkód videa a to může trvat třeba i hodinu, jak se pak takový child apache zachová, nicméně absolutně netuším co tohle udělá s produkčními servery. Tak bych rád věděl jestli někdo neřešil podobnou situaci a jak ji vyřešil, případně hledám nějakou alternativu, ideálně pak upravený gettext systém s rozumnou možností invalidace gettext keše.

Děkuji za odpovědi a omlouvám se jestli sem zvolil špatné zařazení do diskuze, opravdu nevím kam sem tenhle dotaz totiž měl zařadit:)
Jozin
Profil
Zdravím,

dala by se použít funkce bindtextdomain, kde specifikuji cestu pro doménu, pokud je nalezena v cache porovná cestu, neshoduje-li se, flushne cache. Například:

bindtextdomain('myapp', '/cesta/nocache');
bindtextdomain('myapp', '/spravna/cesta');

Jak jsem ovšem pochopil, ne vždy to funguje, okolnosti nefunkčnosti mi ovšem nejsou známi. Tato metoda je ale opravdu hack a nevím, jestli bych jej dal na ostrý web, raději bych použil nějaký překladový nástroj, případně si jej sám dopsal.

Jozin.
Virtus
Profil
[#2] Jozin
díky za odpověď, toto řešení jsem zkoušel, i pár dalších, o kterých tvrdily, že fungují, bohužel na virtuální testovací soustavě ani jedno nefungovalo( a testovat to na ostrém serveru tak trochu nepřipadá v úvahu). Každopádně problém jsem již vyřešil, napsáním vlastního systému. A zde dodám krátký příběh otom jak tohle celý vzniklo a jak z toho ven ( asi jen pro ty co rádi čtou nebo se chtěj dozvědět jak se to dělat nemá a nebo se chtěj trochu zasmát) :)
Do teď jsme používaly pro ukládání překladů/textů XML soubor:
/* /lang/domain.com/en/admin.xml */
<?xml version="1.0" encoding="UTF-8" ?>
<document>
  <banners>
    <add>Add new banner</add>
    ...
adresa klíče pak byla Lang::get('admin.banners.add'), ze začátku projektu( když jsme jej začali přepisovat z čistě procedurálního kódu) když klíčů bylo málo, se toto jevilo jako dostačné a dokonalé řešení, pro zpříjemnění jsme ještě vymysleli takovou věc jako Lang::setNamespace('admin.banners'); aby sme nemusli vždy dokola opisovat celý klíč, takže pak stačilo napsat jenom Lang::get('add'), v tuhle chvíli to považuji za největší zlo co jsme do systému přidali, protože díky těmto jmeným prostorům jsme si uzavřeli cestu k nějakému automatizovanému přepsání systému klíčů a zároveň se v tom nedá ani rozumně vyhledávat, protože poslední část klíče nebývá zpravidla unikátní. Další nevýhoda tohoto systému je, že pokud se nějaký klíč přestane používat, většinou v xml souboru "straší" dál, každý se jej bojí odstranit, co kdyby se někde používal (nikdo to díky jmeným prostorům zjišťovat nebude(lol)) a protože zezačátku se nezdálo nutné zavádět nějaká extra pravidla, tak se klíče psali jak koho zrovna napadlo: admin.settings.videos.title,admin.videos.settings.title takže i když by byla snaha se třeba do xml podívat zda již neexistuje nějaký podobný text co hodlám zrovna přidat tak jej stejně většinou není šance nalézt. Další věc, co nás k tomuto napadla udělat byli tzv. dynamický klíče, což není nic víc než prostě text pro php funkci vsprintf(); Lang::get('admi.banner.numberOfView'souboru ,$bannerId,$views);, výstup takového klíče pak může vypadat nějak takto: Banner ID: 25, was seen by 200 people. O kus níže zjistíte proč je tohle problém. Někteří si dokonce vytvořili v těchto xml souborech malé datové tabulky, třeba pro barvu očí ('data.eye.color.id[1-9]{1}'), jistě chápete že v administraci z toho musel být server občas naprášky, když si někdo vyžádal filtrovat podle ID barvy očí (lol) a navíc se takový klíč nedá ani najít, protože se musí volat takto Lang::get('data.eye.color.id'.$eyeId) a se jmeným prostorem to pak má člověk jenom Lang::get('id'.$eyeId), a na závěr moje nejoblíbenější konstrukce volání klíče (odkazuje na na ten vsprintf()) Lang::get('admin.objectHas.eyeColor',Lang::get('data.eye.color.id'.$eyeId))(wtf (o_O) :D) v tenhle moment to totiž nemůžete ani rozumně vyparsovat pomocí regulérních výrazů a nezbývá nic jiného než projít soubor po souboru a všechny je zkontrolovat, no řeknu vám, projekt má aktuálně přes 1,5k souborů ve kterých je dohromady 15k volaný Lang, procházet každej z nich a díky jmenným prostorům hledat co kam patří za text...hmm -> nikdy :D, takže jak z toho ven.

PS: Snad se z tohohle někdo ponaučí, tím nechci tvrdit že použití xml souborů pro textové klíče je špatné, asi bych jej použil znova, ale jen pro projekty ve kterých těch klíčů nebu víc jak 200 a s přísnými pravidly jak jej používat a určitě bez jmenných prostorů :)

No pokračujem dál. V první řadě je potřeba se zbavit klíčů, které se nepoužívají. V mém případě je tohle zrovna celkem jednoduchá záležitost, při výběru klíče stačí prostě uložit záznam o klíči který se použije a s minimálním počtem 10M stránek za den, mi stačí právě ten jeden den abych měl kompletní databázi všech používaných klíčů. Takže první krok splněn :). Druhý krok, ke klíčům najít originální texty v xml souborec, nezapomenout nato že jeden a ten samý klíč může být pod více doménama, takže bude zapotřebý pěti tabulek v databázi (3 datové, 2 relační), datové tabulky: lang_keys, texts, domains a relační tabulky: lang_key_text (m:n), text_domain(m:n), možná někoho napadlo, že mám jistě klíče které jsou na všech doménách stejné, proto jsem si vytvořil extra doménu (common) nebo můžete použít třeba (default), cokoliv uznáte za vhodné, tím jsme zajistili že jeden klíč může odkazovat na více různých textů a nebo, pro nás důležitou věc, naopak více klíčů může odkazovat na jeden text (pro domény platí to samé). Texty do tabulky budem ukládat v datovém tipu BLOB, nechcem totiž aby nám databáze ořezala mezery (alespoň ne zezačátku), u některch kombinací html enitita+prohlížeč, je zobrazení závislé právě na mezerách/entrech za entitou, v tomto kroku je ještě ideální se rovnou zbavit na 100% stejných textů (duplicit). Další krok už bude o něco více zajímavý a to bude napsat script pro nalzení podobných textů, takže jak nato. V první řadě bude potřeba vytvořit další tabulku v databázi infixes a přidání dvou sloupečků do relační tabulky lang_key_text: prefix_id, sufix_id oba tyto klíče odkazují na id klíče v tabulce infixes. Druhý krok je napsání vyhledávacího scriptu, k tomu nám dobře poslouží php funkce: levenshtein, similar_text, soundex a metaphone, případně gmp_hamdist. K čemu je dobrá gmp_hamdist(); (Hamming distance) tato funkce nám poslouží dobře k vyhledání všech textů ve kterých je rozdíl v jednom písmenu a to konkrétně pouze ve velikosti stejného písmene př.:New Video ->New video, v tamto případě bude totiž Hamming distance vždy 1. Druhé kolo hledání podobných výrazů, použijeme funkci levenshtein a necháme projít pouze výrazy, mezi kterýma bude výsledek funkce menší než 6, do této kategorie tedy spadne 100% všech kombinací výrazů které mají délku menší než 6, ale celkem rozumně to odfiltruje dlouhé texty, takže jak na ty krátké, k tomu nám poslouží zbývající funkce, příklad:
<?php
$t1 = 'Rage'; // $t1 = 'by'; pro tuto kombinaci bude uvedeno vždy za středníkem
$t2 = 'Page'; // $t2 = 'BY:';
levenshtein($t1,$t2); // = 1; =3
similar_text($t1,$t2,$percent) // $percent = 75(%); = 0%
// jak vidíte celkem nepoužitelné, takže to trochu upravíme
$s1 = soundex($t1); //$s1 = R200; = B000
$s2 = soundex($t2); //$s2 = P200; = B000
$m1 = metaphone($t1); //$m1 = RJ; = B
$m2 = metaphone($t2); //$m2 = PJ; = B
$smt1 = $s1.$m1.$t1; //$smt1 = R200RJRage; = B000Bby
$smt2 = $s2.$m2.$t2; //$smt2 = P200PJPage; = B000BBy:
levenshtein($smt1,$smt2) // = 3; = 3
similar_text($smt1,$smt2,$percent) // $percent = 70%;  = 66.6%
//a můžem zkusit různé kombinace, ikdyž tady pozor všimněte si výstupů funkce similar_text
$p1 = soundex('O'.$t1); //$p1 = O620; = O100
$p2 = soundex('O'.$t2);//$p2 = O120; = O100
$smt1 = $p1.$smt1;
$smt2 = $p2.$smt2;
levenshtein($smt1,$smt2) // = 4; = 3
similar_text($smt1,$smt2) // $percent = 71.4%; = 78.2%
Nyní už to chce jen pár testů a zprávně nastavit filtry aby procházeli třeba jenom kombinace které budou mít víc jak 75% podobnost. Kombinace si někam vypíšem, a vytvoříme si akce co s kombinací budem chtít dělat (já to dělal v konzoli s readline(); ). Abych nezapoměl na tu tabulků infixes, do té přídou veškeré bláboly na konci a na začátku textu, které nejsou důležité pro překlad, jako je třeba mezera nakonci, pomlčka na začátku, dvojtečka na konci, v mém případě třeba i %s / %s / %s z textu: Date %s... :D, pozor nepatří sem vykřičník a otazník, protože v různých jazycích mění různě slovosled. A to je asi tak všechno. Shrnuto podtrženo z původních 5k klíčů, které jsem měl na začátku, jich mám aktuálně 3k díky tomuto. 1k byli čisté duplicity a další 1k byly právě tyto podobnosti s prefixy a sufixy. Pokud to dočet někdo až sem, klobouk dolů a můžete mi sem napsat co si otomto řešení myslíte?
PS: pozor na multibite texty u funkce levenshtein() a to že zvládá max 255 znaků dlouhý řetězec :)
Jozin
Profil
Když jsem četl začátek příspěvku, že překladový slovník máte v xml, hodně jsem se divil, sami jste přišli na to, že je to neudržovatelné ve velkém počtu a jak předpovíte, že projekt bude mít, jak jste uvedl 200 klíčů?

Nápad s jmennými prostory nebyl špatný, pokud by byl dobře implementovaný, dalo by se i hledat, samozřejmě ne v programu, ale přímo v php scriptu třeba v administraci.

Databáze je každopádně nejlepší volba, v dnešní době i free hostingy databázi podporují a my nemusíme řešit I/O požadavky, tedy spíš, jak moc jsou náročné.

Nakonec bych něco rád podotkl k návrhu databáze, pokud jsem to správně pochopil jde o překlopení xml do tabulek? Pokud ano, tak moc nechápu text_domain a už vůbec ne kardinality m:n, unikátním klíčem každého řádku tabulky je kombinace klíče jazyku a domény. Pak je to 1:n v rámci unikátního klíče (a to doporučuji, protože při úpravě/přidání se pak nedá udělat duplicitní řádek). Předpokládám, že tabulka domains má ID, jméno a naddoménu, nejvyšší domény nemají naddoménu (tedy NULL) a pak se dá udělat zanořování domén "donekonečna". Může se to zdát jako zbytečnost, ale při změně delimiteru mezi doménovými jmény (tedy domena1.domena2.text na třeba domena1/domena2/text) se vám budou záznamy v tabulce předělávat těžko (tedy protože jich tam bude hodně), při rozšiřování se vám nestane, že by jste změnili pořadí domén, jak se vám stalo v xml, přidávám doménu dejme tomu video, pokud pak chci přidat text, který má být v doméně video, mohu si jej najít (vylistovat), pak dojde na duplicitu doménových jmen, jako settings, ovšem vím k čemu dané doménové jméno patří, třeba video, což má třeba ID = 5, takže hledám všechny 'settings' s naddoménou = 5, což pokud nic nenajde, přidám záznam a pokud najde, použiji nalezené ID.

Poslední věcí jsou prefix a suffix: nezdá se mi to jako ideální, prefixy a suffyxy se v aplikaci mění v závislosti na jazyku? To se mi nezdá jako ideální, naopak si myslím, že je dobré si specifikovat formát, jako třeba u data, při převodu data v PHP může být právě překlad daný formát v daném jazyce.

Jeden nápad nakonec, co se týče problematiky udávání namespace a údajů uprostřed překladů:

1. Nemám rád singletony nebo statické metody u takovýchto věcí, proto bych instancioval překlady pro danou doménu
2. Zavedl bych u metody get neurčitý počet parametrů s tím, že každá lichý parametr je text a každý sudý je hodnota

$domain = new Lang('my.super.domain');

// get s více parametry
echo $domain->get('post', 10, 'has', 20, 'comments'); //= hodnota nemusi byt jen int

Dá se namítat, že třeba takové 'has' může být v common, to by se dalo vyřešit třeba takto:
$domain = new Lang('my.super.domain');
$common = new Lang('common');

// reseni 1.
echo $domain->get('post', 10, '', $common->get('has'), 20, 'comments');

// reseni 2. - pridani magicke metody __get
echo $domain->get('post', 10, '', $common->has, 20, 'comments');

// reseni 3. využití magické metody __get pro všechny
echo $domain->post . 10 . $common->has . 20 . $domain->comments;

Ale stejně tak mě napadá miliarda dalších řešení, ovšem, pokud vám vaše řešení vyhovuje, tak i tak dobré ;)

Jozin.

Vaše odpověď

Mohlo by se hodit


Prosím používejte diakritiku a interpunkci.

Ochrana proti spamu. Napište prosím číslo dvě-sta čtyřicet-sedm:

0