Vícejazyčný web postavený na Nextras/ORM

Upozornění: Tohle vlákno je hodně staré a informace nemusí být platné pro současné Nette.
David Kregl
Člen | 52
+
0
-

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:

  1. 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.
  2. 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
+
+2
-

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)

fizzy
Backer | 49
+
0
-

Riesil som podobny problem nedavno. Nakoniec som vytvoril prekladove entity ktore obsahuju len texty a jej interface implementuje taktiez prekladaná entita. Tej je nastavený po nahratí z db default jazyk (v doctrine je to post load event) podľa zvoleného jazyka.

hitzoR
Člen | 51
+
0
-

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
+
0
-

@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
+
0
-

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
+
0
-

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 {…}
CZechBoY
Člen | 3608
+
0
-

Ten try catch tam je trochu zbytecne, ne? Navic zahodis predchozi vyjimku.

David Kregl
Člen | 52
+
0
-

Ahoj, který try, catch je tam podle Tebe zbytečný?

Editoval David Kregl (20. 12. 2016 19:13)

Pavel Janda
Člen | 977
+
0
-

@DavidKregl

try {
   $language = $this->languageService->getLanguageByCode($code);
} catch (InvalidArgumentException $e) {
   throw new InvalidArgumentException($e->getMessage());
}
newPOPE
Člen | 648
+
0
-

@DavidKregl pusti ako to composer package :)

CZechBoY
Člen | 3608
+
0
-

@DavidKregl asi vsechny co znovu vyhazuji tu samou vyjimku a zahodi exception trace a kod vyjimky atd.

David Kregl
Člen | 52
+
0
-

@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ň