Jak se řeší routy pro multijazyčný web
- Damo
- Člen | 55
Ahoj,
potřeboval bych vědět, jak je obvyklé řešit u multijazyčného webu i lokalizované url. Dám příklad
https://mojedomena.cz/statistika
https://mojedomena.cz/en/statistics
https://mojedomena.cz/sk/statistika
https://mojedomena.cz/statistika/zakladni-cast
https://mojedomena.cz/en/statistics/regular-season
https://mojedomena.cz/sk/statistika/zakladna-cast
Jak nastavit routy? Nejde mi o locale, ale aby
statistika, statistics směřoval na presenter – statisticsPresenter.php
zakladni-cast, regular-season, zakladna-cast směroval na action –
actionRegularSeason
Vypisovat vsechny varianty do routy, to muze byt na stovky radku, takze mi to neprijde jako sikovny.
Možná mě napadlo, ze si v kazdem presenteru udelam anotaci na tridu a metodu, a v route třídě si posbiram tyto udaje a vytvorim ty routy nějak dynamicky. Ale pořád nevím, jestli je to správný postup.
Prosím naveďte mě, jak tohle nejlépe řešit. Snad už na to existuje třeba nějaká šikovná věcička. Díky
Editoval Damo (24. 4. 2023 19:16)
- Damo
- Člen | 55
Kamil Valenta napsal(a):
https://doc.nette.org/…tion/routing#…
Zaměř se na FilterIn a FilterOut.
Tak jsem dal dohromady asi toto, stejně musím pro ten FILTER_IN dodat nějaký array jak a co překladat.
Příklad, který mi funguje jak potřebuji.
Mám v app/ManagersModule/Presenters/DefaultPresenter.php
kde jsem si definoval anotacemi překlady, které budou směrovat na tento
presenter a na danou action
Anotace třídy obsahuje navíc u modul.
je to DefaultPresenter, ale treba u TablePresenter, by ty preklady nebyly
Default, ale @routeTranslation {„sk“: „tabula“, „cs“:
„tabulka“, „en“: „table“}
<?php
declare(strict_types=1);
namespace App\ManagersModule\Presenters;
use App\Presenters\BasePresenter;
/**
* @routeTranslation {"sk": "manazeri/default", "cs": "manazeri/default", "en": "managers/default"}
*/
final class DefaultPresenter extends BasePresenter {
/**
* @routeTranslation {"sk": "default", "cs": "default", "en": "default"}
*/
public function actionDefault() {
echo $this->translator->translate("ui.title");
exit;
}
}
Udelal jsme si třídu, co mi posbírá všechny presentery v projektu a
vytvori mi json pro preklady v routach
Jeste by se to melo dat do cache, ale to snad pozdeji
<?php
declare(strict_types=1);
namespace App\Router\Collector;
use App\Models\Di;
use App\Models\Helpers\Annotations;
use ReflectionClass;
use ReflectionException;
class PresenterAnnotationCollector {
/**
* @throws ReflectionException
*/
public static function getRouteCollection(): array {
$translations = [];
$services = Di::getContainer()->findByType(\Nette\Application\UI\Presenter::class);
foreach ($services as $name) {
$presenter = Di::getContainer()->getService($name);
if (!$presenter instanceof \Nette\Application\IPresenter) {
continue;
}
$annotations = new Annotations($presenter);
$routeTranslation = $annotations->getClassAnnotations("routeTranslation");
if (!$routeTranslation) {
continue;
}
$presenterPathTranslatedNames = json_decode($routeTranslation, true);
$presenterReflection = new ReflectionClass($presenter);
$presenterName = str_replace('Presenter', '', $presenterReflection->getShortName());
$module = self::getModule($presenterReflection->getNamespaceName());
foreach ($presenterPathTranslatedNames as $routeLanguage => $presenterPathTranslatedName) {
[$moduleTranslatedName, $presenterTranslatedName] = explode('/', $presenterPathTranslatedName);
$moduleTranslatedName = ucfirst($moduleTranslatedName);
$presenterTranslatedName = ucfirst($presenterTranslatedName);
if (!isset($translations[$routeLanguage]['modules'][$moduleTranslatedName])) {
$translations[$routeLanguage]['modules'][$moduleTranslatedName]["translation"] = $module;
}
$moduleNode = &$translations[$routeLanguage]['modules'][$moduleTranslatedName];
if (!isset($moduleNode["presenters"][$presenterTranslatedName])) {
$moduleNode["presenters"][$presenterTranslatedName]["translation"] = $presenterName;
}
$presenterNode = &$moduleNode["presenters"][$presenterTranslatedName];
$actions = get_class_methods($presenter);
foreach ($actions as $actionName) {
if (!str_starts_with($actionName, "action")) {
continue;
}
$action = self::getAction($actionName);
$actionRouteTranslation = $annotations->getMethodAnnotations($actionName, "routeTranslation");
if (!$actionRouteTranslation) {
continue;
}
$actionTranslatedNames = json_decode($actionRouteTranslation, true);
if (!isset($presenterNode["actions"][$action])) {
$actionTranslatedName = $actionTranslatedNames[$routeLanguage];
$presenterNode["actions"][$actionTranslatedName]["translation"] = $action;
}
}
}
}
return $translations;
}
private static function getModule(string $namespace): string {
$parts = explode('\\', $namespace);
$modules = [];
foreach ($parts as $part) {
if (preg_match('/Module$/i', $part)) {
$moduleName = ucfirst(substr($part, 0, -6));
$modules[] = $moduleName;
}
}
return implode(".", $modules);
}
private static function getAction(string $methodName): string {
return preg_replace('/^action/', '', $methodName);
}
}
Tahle trida mi vrati assoc array, ktera vypada asi takto
array(3) {
["sk"]=>
array(1) {
["modules"]=>
array(1) {
["Manazeri"]=>
array(2) {
["translation"]=>
string(8) "Managers"
["presenters"]=>
array(1) {
["Default"]=>
array(2) {
["translation"]=>
string(7) "Default"
["actions"]=>
array(1) {
["Default"]=>
array(1) {
["translation"]=>
string(7) "default"
}
}
}
}
}
}
}
["cs"]=>
array(1) {
["modules"]=>
array(1) {
["Manazeri"]=>
array(2) {
["translation"]=>
string(8) "Managers"
["presenters"]=>
array(1) {
["Default"]=>
array(2) {
["translation"]=>
string(7) "Default"
["actions"]=>
array(1) {
["Default"]=>
array(1) {
["translation"]=>
string(7) "default"
}
}
}
}
}
}
}
["en"]=>
array(1) {
["modules"]=>
array(1) {
["Managers"]=>
array(2) {
["translation"]=>
string(8) "Managers"
["presenters"]=>
array(1) {
["Default"]=>
array(2) {
["translation"]=>
string(7) "Default"
["actions"]=>
array(1) {
["Default"]=>
array(1) {
["translation"]=>
string(7) "default"
}
}
}
}
}
}
}
}
v RouteFactory, pak pomoci FILTER_IN pouzji tu array k prekladum url na presenter a action
<?php
declare(strict_types=1);
namespace App\Router;
use App\Router\Collector\PresenterAnnotationCollector;
use Nette;
use Nette\Application\Routers\RouteList;
use Nette\Routing\Route;
use ReflectionException;
final class RouterFactory {
use Nette\StaticClass;
/**
* @throws ReflectionException
*/
public static function createRouter(): RouteList {
$routeCollection = PresenterAnnotationCollector::getRouteCollection();
$router = new RouteList();
// modul - presenters
$router->addRoute('[<locale=sk cs|en>/]<module>/<presenter>/[<action [a-zA-Z][a-zA-Z0-9-]*>][/<id>]', [
'presenter' => 'Default',
'action' => 'default',
'id' => null,
null => [
Route::FilterStrict => true,
Route::FILTER_IN => function($params) use ($routeCollection): array {
$locale = $routeCollection[$params["locale"]];
$module = $locale["modules"][$params["module"]];
$moduleTranslation = $module["translation"];
$presenter = $module["presenters"][$params["presenter"]];
$presenterTranslation = $presenter["translation"];
$action = $presenter["actions"][$params["action"]];
$actionTranslation = $action["translation"];
$params["module"] = $moduleTranslation;
$params["presenter"] = $presenterTranslation;
$params["action"] = $actionTranslation;
return $params;
},
],
]);
$router->addRoute('[<locale=sk cs|en>/]<presenter>/<action>[/<id>]', 'Home:default');
return $router;
}
}
Takze potom
https://domena.sk/manazeri
https://domena.sk/cs/manazeri
https://domena.sk/en/managers
mě nasmeruje vsechno do jednoho presenteru app/ManagersModule/Presenters/DefaultPresenter.php, kde mi to krásne zobrazi v přílušném jazyce.
Je to cesta jak to může fungovat v rozsáhlejším projektu?
Editoval Damo (27. 4. 2023 16:20)
- Damo
- Člen | 55
Tak ještě po upravach jsem to zprovoznil jak IN tak OUT filtry. Ale není to ideální.
- Seznam presenterů v containeru je cachovany, takze pri vyvoji kazdy pridany nebo odebrany presenter s sebou nese, že se musí promazat cache, jinak neni pak v seznamu pro preklad routy k dispozici.
- Ten seznam co se vytvari nelze dobre, resp. vůbec cachovat, protoze nelze navazat na zadnou podminku invalidace. Tedy, aby se seznam invalidoval, kdyz provedu změnu v presenterech, pridam, odeberu, zedituju anotace. Takze se kazdym requestem ten seznam generuje znovu, coz neni hezke.
Tak otázka zase co jsme měl nahoře. Jak se tohle běžně řeší. Kde ty
překlady se v aplikaci drží? Existuje nějaký příklad?
Když koukám tady na nette.org, tak ty preklady se neresi, cely url je anglicky
i pro verzi /cs aj. jazyky. Což je taky řešení, ale chtěl bych mít
lokalizované url. Nebo chci něco co je naprosto zbytečný, když budu mit
lokalizovany description a keyword v headru stránky, tak na url nesejde a
může zůstat anglická?
- dakur
- Člen | 493
@Damo Otázka je, jestli to chceš ty, nebo jestli je to k něčemu potřeba. Člověk má jako vývojář někdy nápady co by bylo hezký, ale z hlediska užitečnosti to má nulový přínos a zbytečně na tom tratí čas.
Spíš bych si tedy položil otázku, jestli to k něčemu potřebuje uživatel/klient. Pokud ne, neřešil bych to.
Editoval dakur (28. 4. 2023 10:45)
- Kamil Valenta
- Člen | 762
Nechal bych to zacachovaný bez tagu pro invalidaci.
Předpokládám, že „každý“ maže cache po deploy automaticky.
A pokud náhodou ne, tak bych to zacachoval a o ivalidaci by se postaral
nějaký run-after skript navázaný na doběhnutý deploy (tam se děje řada
věcí, např. db migrace, tak proč tam neinvalidovat cache s překlady akcí
presenterů).
Řešení s anotacemi u akcí se mi zdá dobré. Zrovna tohle asi člověk
nepotřebuje tahat se zbytkem slovníku.
Jen bych ještě vypodmínkoval, že anotace může chybět a pak to reaguje
klasicky na action*().
- Damo
- Člen | 55
Diky za info, nevěděl jsem zda jdu správným směrem v tomto.
Invalidaci jsme si udelal tak, ze kontroluji hash adresare, pokud se v nem zmeni, smaze, pribude soubor, dostane jinou hash a invaliduje cache. Znovu se ten prekladovy array sestavi, a jeste predtim invaliduje i Container, aby si nacetl vsechny aktualni presentery.
$dependencies[Cache::Callbacks] = [
[self::class . '::checkDirHash', $dir, $dirHash]
];
Editoval Damo (28. 4. 2023 14:28)