Vícejazyčný web postavený na Nextras/ORM
- David Kregl
- Člen | 52
Ahoj,
potřebuji vytvořit vícejazyčný web včetně administrace. Statické texty budu nejspíš překládat přes Kdyby/Translation, ale v případě těch dynamických jsem se rozhodl jít cestou databáze.
Chtěl bych využít následující přístup:
/**
* …
* @property OneHasMany|PageTranslation[] $translation {1:m PageTranslation::$page}
*/
class Page extends Entity {}
/**
* @property string $title
* @property string $content
* @property string $lang {default self::LANG_CZ, enum self::LANG_*}
* @property ManyHasOne|Page $page {m:1 Page::$translation}
*/
class PageTranslation extends Entity
{
const LANG_CZ = 'cs_CZ';
const LANG_EN = 'en_GB';
}
Jen si nevím rady, jak s tím pracovat. Proto se tedy ptám:
- Jak by měla vypadat nějaká service, která se mi bude starat o jazyky v aplikaci? Předpokládám, že nestačí persistentní parametr $lang v BasePresenteru.
- Jak získám entitu Page včetně správných překladů, které jsou aktuálně nastavené v aplikaci?
Předem děkuji za váš čas,
David
Editoval David Kregl (16. 12. 2016 14:44)
- newPOPE
- Člen | 648
Niečo podobné sme riešili v práci. Problém prekladov a iných properties k entite ktoré sú potrebné ale dáš ich do nového stĺpca je na nič.
Prečo je to na nič? Lebo relacna db je relacna db a relacnym svetom sme ovplyvnení :)
Takze preklady rvem do stĺpca napr. translations kde je pre každý jazyk uložený preklad. Je to v JSONe (keď máš MySQL 5.7+ môžeš dať stĺpec ako JSON type inak napr. Text). Trieda ktorá mi to obaluje sa stará o to že keď chcem preklad tak mi ho da ak nie je pre daný jazyk dá bud null alebo default jazyk, podľa nastavenia.
Výhoda? Nepotrebuješ sahat do inej tabuľky. Preklady pre všetky jazyky máš okamzite. Kombinuje relacny a nerelacny svet…
Editoval newPOPE (16. 12. 2016 15:58)
- hitzoR
- Člen | 51
Já to řeším tak, že mám celkem tři entity:
Lang
– informace o jazyce (název, kód, doména)
Translate
– obsahuje pouze ID překladu, na tuhle entitu jsou
pomocí oneSided ManyToOne odkazovány sloupce v ostatních entitách, které
mají být přeloženy
Translation
– obsahuje samotný překlad, tzn. ID z Translate,
kód z Lang a text
Do modelu si pak ukládám perzistentní parametr $lang
. Pracuju
s tím tak, že v Translate máme několik metod, které se mi starají
o překlady:
__toString() // vypíše hodnotu podle jazyku nastaveného v modelu ($this->getModel()->lang), pokud text není do daného jazyka přeložen, vypíše se angličtina (defaultní jazyk), a pokud není ani ten, tak se prostě vypíše text z první Translation entity
getByLang($lang) // vypíše hodnotu ze zadaného jazyka nebo hodí BadRequestException, pokud překlad pro konkrétní jazyk neexistuje
set($lang, $text) // nastaví překlad pro daný jazyk
Takže s tím pracuju třeba tak, že na frontendu při výpisu článku
dám jen $article->title
a chová se to tak, jako by title byl
obyčejný string. Samozřejmě se mi ještě v routeru kontroluje, zda daná
entita vážně má překlad do požadovaného jazyka – router mi totiž
řeší i dodávaní entit nebo kolekcí do akcí presenterů, takže není
problém zkontrolovat, jestli ta entita má překlad v jazyce, který je
v perzistentním parametru $lang.
Co se týká setování hodnot, tak to řeším pomocí té metody
set($lang, text)
, šlo by to i pomocí __set($val)
v entitě Translate, ale vzhledem k tomu, že zatím 100% překladů vkládám
na backendu, který není závislý na perzistentním parametru, tak jsem tohle
ani neimplementoval.
Z pohledu implementace i návrhu DB mi tohle řešení přišlo z několika variant jako nejideálnější. Třeba řešení pomocí JSONu, které tu nahodil @newPOPE , je podle mě v relační databázi úplný nesmysl, když to jde udělat čistě pomocí relací.
Editoval hitzoR (17. 12. 2016 13:25)
- newPOPE
- Člen | 648
@hitzoR ano ide to urobit pomocou relacii to sa hadat nebudem.
Ale skus sa zamyslet aky to bude mat dopad na vykon ked budes mat entitu ktorej 5 properties chces mat prekladatelnych?
- s tym JSON-nom to mas na 0 queries nakolko uz nikam sahat nemusis a to bez cache
- u teba je to min dalsich 5 queries + mozno cache ale o tu sa treba starat
Ja to nikomu nenutim, riesenie ktore navrhujes je okej lebo ako pisem „sme relacnymi DB ovlyvneni“. Podpora JSON-u do relacnych DB neprisla len tak z nicoho nic. Su na to dovody :)
- hitzoR
- Člen | 51
JSON ano, ale jen v hodně krajních případech. Například já ho používám u logování určitých eventů, kde každý typ eventu má úplně jinou strukturu. Naopak tam, kde ta struktura je vždycky víceméně stejná, je to imho akorát restriktivní a to, co by jinak dělala databáze (kontrolu datových typů, hodnot a samotné struktury) by musela dělat samotná aplikace. Což by třeba nebyl až takový problém v okamžiku, kdy by ta aplikace přistupující k databázi byla jediná a projekt by nebylo potřeba rozšiřovat, což ale nikdy nejde na 100% říct. Pak by byl overkill tohle řešit ve více aplikacích nebo to upravovat.
Co se týče výkonu, tak nikdy sem žádný benchmark na tohle téma neviděl ani sám nedělal, ale pár query navíc by problém být neměl. A json_decode taky není „zadarmo“, ale v obou případech bych řekl, že jde o jednotky milisekund, což je v kontextu většiny aplikací zanedbatelný čas.
- David Kregl
- Člen | 52
Ahoj,
nakonec jsem zkombinoval ukládání dat do formátu JSON s řešením od @pfilipek, které je k nahlednutí tady.
Přikládám třídy pro budoucí řešitele stejného problému:
TranslationEntity
abstract class TranslationEntity extends Entity
{
/** @var LanguageService */
protected $languageService;
/**
* @param LanguageService $languageService
*/
public function injectTranslator(LanguageService $languageService)
{
$this->languageService = $languageService;
}
/**
* @param string $code
* @return Entity
*/
public function getTranslation($code = NULL)
{
$language = NULL;
if (!isset($this->translations)) {
throw new InvalidArgumentException('Entita ' . get_called_class() . ' nemá definovanou property translations s vazbou na překladovou tabulku.');
}
if (!is_null($code)) {
try {
$language = $this->languageService->getLanguageByCode($code);
} catch (InvalidArgumentException $e) {
throw new InvalidArgumentException($e->getMessage());
}
}
if (is_null($language)) {
$language = $this->languageService->getCurrentLanguage();
}
$translation = Json::decode($this->translations);
return $translation->$language;
}
/**
* @param $name
* @return mixed
*/
public function &__get($name)
{
$translate = function ($name, $msgs = []) {
try {
return $this->getTranslation()->$name;
} catch (InvalidArgumentException $e) {
$msgs[] = $e->getMessage();
throw new InvalidArgumentException(implode(' --> ', $msgs));
}
};
try {
$value = parent::__get($name);
} catch (InvalidArgumentException $e) {
$value = $translate($name, [$e->getMessage()]);
} catch (InvalidStateException $e) {
$value = $translate($name, [$e->getMessage()]);
}
return $value;
}
}
Profile
<?php
namespace App\Model;
/**
* @property string $category
*
* @property string $translations
*
* @property-read string $title {virtual}
* @property-read string $subtitle {virtual}
* @property-read string $content {virtual}
*/
class Profile extends TranslationEntity {…}
- David Kregl
- Člen | 52
Ahoj, který try, catch
je tam podle Tebe zbytečný?
Editoval David Kregl (20. 12. 2016 19:13)
- Pavel Janda
- Člen | 977
@DavidKregl
try {
$language = $this->languageService->getLanguageByCode($code);
} catch (InvalidArgumentException $e) {
throw new InvalidArgumentException($e->getMessage());
}
- David Kregl
- Člen | 52
@CZechBoY @PavelJanda Aha, už tomu rozumím. Ty jsou tam, pro případ, kdy přístupuji k těm datum přes $profile->getTranslation(‚en_GB‘)->title.
@newPOPE Asi nerozumím tomu, co po mě chceš. Promiň