Jak se řeší routy pro multijazyčný web

Damo
Člen | 55
+
0
-

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)

Kamil Valenta
Člen | 762
+
+2
-

https://doc.nette.org/…tion/routing#…

Zaměř se na FilterIn a FilterOut.

Damo
Člen | 55
+
0
-

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

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

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

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

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)