Welcome Guest, Not a member yet? Register   Sign In
{locale} placeholder in routes
#1

(This post was last modified: 06-11-2022, 04:35 AM by webdeveloper.)

Hi, I just want to share with you one of my changes I made to base router. Hopefully, it will save some time, to somebody who will search for the same solution like I did.

I was wondering why my project takes URL like

Code:
www.domain.com/blah/users

when I've never defined it? Why this works?

What I found out:

PHP Code:
$routes->group('{locale}', function ($routes) {
   $routes->group('users', function ($routes) {
      $routes->get('/''Users::index');
   });
}); 

{locale} will take ANYTHING

Why does all this URLs works?

Code:
www.domain.com/this/users
www.domain.com/makes/users
www.domain.com/no/users
www.domain.com/sense/users
www.domain.com/to/users
www.domain.com/me/users

{locale} should represent locale placeholder. According to me, it does not. It represent anything. Yes, it takes default locale value, if placeholder doesn't match any supported language.

PHP Code:
public $supportedLocales = ['en']; 

Only this should works.

Code:
www.domain.com/en/users

Changes I have made:

Create new file app/Services/Router.php

PHP Code:
<?php

namespace App\Services;

use 
CodeIgniter\HTTP\Request;
use 
CodeIgniter\Router\Exceptions\RedirectException;
use 
CodeIgniter\Router\RouteCollectionInterface;
use 
CodeIgniter\Router\Router as CoreRouter;

class 
Router extends CoreRouter
{
    public function __construct(RouteCollectionInterface $routes, ?Request $request null) {
        parent::__construct($routes$request);
    }

    /**
    * @param string $uri
    * @return bool
    * @throws RedirectException
    */
    protected function checkRoutes(string $uri): bool {
        $routes $this->collection->getRoutes($this->collection->getHTTPVerb());

        // Don't waste any time
        if (empty($routes)) {
            return false;
        }

        $uri $uri === '/' $uri trim($uri'/ ');

        // Loop through the route array looking for wildcards
        foreach ($routes as $key => $val) {
            // Reset localeSegment
            $localeSegment null;

            $key $key === '/' $key ltrim($key'/ ');

            $matchedKey $key;

            // Are we dealing with a locale?
            if (strpos($key'{locale}') !== false) {
                $localeSegment array_search('{locale}'preg_split('/[\/]*((^[a-zA-Z0-9])|\(([^()]*)\))*[\/]+/m'$key), true);

                // Replace it with a regex so it
                // will actually match.
                $key str_replace('/''\/'$key);
                $key str_replace('{locale}''[^\/]+'$key);
            }

            // Does the RegEx match?
            if (preg_match('#^' $key '$#u'$uri$matches)) {
                // Is this route supposed to redirect to another?
                if ($this->collection->isRedirect($key)) {
                    throw new RedirectException(is_array($val) ? key($val) : $val$this->collection->getRedirectCode($key));
                }
                // Store our locale so CodeIgniter object can
                // assign it to the Request.
                if (isset($localeSegment)) {
                    // The following may be inefficient, but doesn't upset NetBeans :-/
                    $temp = (explode('/'$uri));

                    /**
                    * CORE EDIT START
                    */
                    $supportedLocales config('App')->supportedLocales;

                    if (!in_array($temp[$localeSegment], $supportedLocales)) {
                        return false;
                    }
                    /**
                    * CORE EDIT END
                    */

                    $this->detectedLocale $temp[$localeSegment];
                }

                // Are we using Closures? If so, then we need
                // to collect the params into an array
                // so it can be passed to the controller method later.
                if (!is_string($val) && is_callable($val)) {
                    $this->controller $val;

                    // Remove the original string from the matches array
                    array_shift($matches);

                    $this->params $matches;

                    $this->matchedRoute = [
                        $matchedKey,
                        $val,
                    ];

                    $this->matchedRouteOptions $this->collection->getRoutesOptions($matchedKey);

                    return true;
                }
                // Are we using the default method for back-references?

                // Support resource route when function with subdirectory
                // ex: $routes->resource('Admin/Admins');
                if (strpos($val'$') !== false && strpos($key'(') !== false && strpos($key'/') !== false) {
                    $replacekey str_replace('/(.*)'''$key);
                    $val preg_replace('#^' $key '$#u'$val$uri);
                    $val str_replace($replacekeystr_replace('/''\\'$replacekey), $val);
                } else if (strpos($val'$') !== false && strpos($key'(') !== false) {
                    $val preg_replace('#^' $key '$#u'$val$uri);
                } else if (strpos($val'/') !== false) {
                    [
                        $controller,
                        $method,
                    ] = explode('::'$val);

                    // Only replace slashes in the controller, not in the method.
                    $controller str_replace('/''\\'$controller);

                    $val $controller '::' $method;
                }

                $this->setRequest(explode('/'$val));

                $this->matchedRoute = [
                    $matchedKey,
                    $val,
                ];

                $this->matchedRouteOptions $this->collection->getRoutesOptions($matchedKey);

                return true;
            }
        }

        return false;
    }


Check added part of code:

PHP Code:
/**
                     * CORE EDIT START
                     */
                    
$supportedLocales config('App')->supportedLocales;

                    if (!
in_array($temp[$localeSegment], $supportedLocales)) {
                        return 
false;
                    }
                    
/**
                     * CORE EDIT END
                     */ 


Add this use to app/Config/Services.php

PHP Code:
use App\Services\Router

Add custom router:

PHP Code:
    /**
    * @param RouteCollectionInterface|null  $routes
    * @param \CodeIgniter\HTTP\Request|null $request
    * @param bool                          $getShared
    * @return Router
    */
    public static function router(RouteCollectionInterface $routes null, \CodeIgniter\HTTP\Request $request null$getShared true): Router {
        if ($getShared) {
            return static::getSharedInstance('router'$routes$request);
        }

        $routes ??= AppServices::routes();
        $request ??= AppServices::request();

        return new Router($routes$request);
    
Reply
#2

You are right, it doesn't make a lot of sense how unsupported locales are handled by the framework. Instead of a custom router, I made a filter :

PHP Code:
<?php
namespace App\Filters;

use 
CodeIgniter\HTTP\RequestInterface;
use 
CodeIgniter\HTTP\ResponseInterface;
use 
CodeIgniter\Filters\FilterInterface;

class 
Localize implements FilterInterface
{
    public function before(RequestInterface $request$arguments null)
    {
        log_message('debug'"FilterLocalize --- start ---");
        $uri = &$request->uri;
        $segments array_filter($uri->getSegments());
        $nbSegments count($segments);
        log_message('debug'"FilterLocalize - {$nbSegments} segments = " print_r($segmentstrue));

        // Keep only the first 2 letters (en-UK => en)
        $userLocale strtolower(substr($request->getLocale(), 02));
        log_message('debug'"FilterLocalize - Visitor's locale $userLocale");

        // If the user's language is not a supported language, take the default language
        $locale in_array($userLocale$request->config->supportedLocales) ? $userLocale $request->config->defaultLocale;
        log_message('debug'"FilterLocalize - Selected locale $locale");

        // If we request /, redirect to /{locale}
        if ($nbSegments == 0)
        {
            log_message('debug'"FilterLocalize - Redirect / to /{$locale}");
            log_message('debug'"FilterLocalize --- end ---");
            return redirect()->to("/{$locale}");
        }

        log_message('debug'"FilterLocalize - segments[0] = " $segments[0]);
        $locale $segments[0];

        // If the first segment of the URI is not a valid locale, trigger a 404 error
        if ( ! in_array($locale$request->config->supportedLocales))
        {
            log_message('debug'"FilterLocalize - ERROR Invalid locale '{$locale}'");
            log_message('debug'"FilterLocalize --- end ---");
            throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
        }

        log_message('debug'"FilterLocalize - Valid locale '$locale'");
        log_message('debug'"FilterLocalize --- end ---");
    }

    public function after(RequestInterface $requestResponseInterface $response$arguments null)
    {
        // Do something here
    }


CodeIgniter 4 tutorials (EN/FR) - https://includebeer.com
/*** NO support in private message - Use the forum! ***/
Reply
#3

(This post was last modified: 06-09-2022, 01:57 AM by webdeveloper.)

Thank you for comment! Always happy to see any other solutions.

Yep, it can be done like this too. I have never think about to create filter for this. Router should handle this "right".

One important difference is that your locale have to be placed always on the first place of URL. 

This wont work for you:

PHP Code:
$routes->group('translations', function ($routes) {
  $routes->group('{locale}', function ($routes) {
      $routes->get('/''Translations::index');
  });
}); 

Second one difference is that router is called before filter. A lot of (possibly unimportant) code can be done until page not found exception is thrown.

Why did you remove second part of locale? This is used in most cases for variants of language e.g. "en-US". I consider this language fallback behaviour as useful

Quote:So, if you are using the locale fr-CA, then a localized message will first be sought in Language/fr/CA, then in Language/fr, and finally in Language/en.
Reply
#4

(06-09-2022, 01:56 AM)webdeveloper Wrote: One important difference is that your locale have to be placed always on the first place of URL. 

This wont work for you:

PHP Code:
$routes->group('translations', function ($routes) {
  $routes->group('{locale}', function ($routes) {
      $routes->get('/''Translations::index');
  });
}); 

You're right, I didn't think it could be a problem as all my routes are configured with the locale as the first segment of the URL.

(06-09-2022, 01:56 AM)webdeveloper Wrote: Why did you remove second part of locale? This is used in most cases for variants of language e.g. "en-US". I consider this language fallback behaviour as useful

Quote:So, if you are using the locale fr-CA, then a localized message will first be sought in Language/fr/CA, then in Language/fr, and finally in Language/en.

I remove the second part of the locale because it doesn't really make a difference. At least not in my case. My blog is in French and in English. So if the locale detected is en-US, en-UK or any other en-xx it redirects to the English version of the blog. Same thing for fr-FR, fr-CA, etc, it goes to the French version. Anything else goes to the default locale, with is English.
CodeIgniter 4 tutorials (EN/FR) - https://includebeer.com
/*** NO support in private message - Use the forum! ***/
Reply
#5

(This post was last modified: 06-10-2022, 12:03 AM by InsiteFX.)

It's probaliy best to use the language+region code for translations en-US etc.

@includebeer, I have been working on your multi-language tutorial and have a working language drop down
with country flags working, I just have to clean-up the flags because they are in language not country.

Example: Japan is named jp in the flags css flie not by language code jp
What did you Try? What did you Get? What did you Expect?

Joined CodeIgniter Community 2009.  ( Skype: insitfx )
Reply
#6

(06-09-2022, 11:57 PM)InsiteFX Wrote: It's probaliy best to use the language+region code for translations en-US etc.

I guess it depends on the use case. For me it was the easiest way to redirect to the english or french articles. I should probably modify my filter to keep the real locale (en-US, en-UK...) but the only difference I can think of is how the dates are displayed. Otherwise, everything should be the same in en-US vs. en-UK, or fr-FR vs. fr-CA.

(06-09-2022, 11:57 PM)InsiteFX Wrote: @includebeer, I have been working on your multi-language tutorial and have a working language drop down
with country flags working, I just have to clean-up the flags because they are in language not country.

Example: Japan is named jp in the flags css flie not by language code jp

Cool!  Cool
CodeIgniter 4 tutorials (EN/FR) - https://includebeer.com
/*** NO support in private message - Use the forum! ***/
Reply
#7

See this PR: https://github.com/codeigniter4/CodeIgniter4/pull/6073
Reply
#8

(06-10-2022, 06:05 PM)kenjis Wrote: See this PR: https://github.com/codeigniter4/CodeIgniter4/pull/6073

I like the idea of a $useSupportedLocalesOnly option. No need to hack the default behaviour with a filter!
CodeIgniter 4 tutorials (EN/FR) - https://includebeer.com
/*** NO support in private message - Use the forum! ***/
Reply
#9

(This post was last modified: 06-11-2022, 05:26 AM by webdeveloper.)

(06-10-2022, 06:34 PM)includebeer Wrote:
(06-10-2022, 06:05 PM)kenjis Wrote: See this PR: https://github.com/codeigniter4/CodeIgniter4/pull/6073

I like the idea of a $useSupportedLocalesOnly option. No need to hack the default behaviour with a filter!

The main question is why you run into this problem, that you need to redirect with right locale? How does the wrong locale appear in your URL? Is it even possible to happen just so?

@kenjis This makes the solution which I posted even better! Like that.

DONE. Fallback behaviour implemented.

Add to: app/Config/Routes.php

PHP Code:
$routes->useDefinedLocalesOnly(true); 

Create file: app/Services/Routes.php

PHP Code:
<?php

namespace App\Services;

use 
CodeIgniter\Autoloader\FileLocator;
use 
CodeIgniter\Router\RouteCollection;
use 
Config\Modules;

class 
Routes extends RouteCollection
{
    /**
    * @var bool
    */
    public bool $useDefinedLocalesOnly false;

    public function __construct(FileLocator $locatorModules $moduleConfig) {
        parent::__construct($locator$moduleConfig);
    }

    /**
    * @param bool $value
    * @return $this
    */
    public function useDefinedLocalesOnly(bool $value): self {
        $this->useDefinedLocalesOnly $value;

        return $this;
    }


Add to: app/Services/Router.php (check the code in first post to find the place where to put this)

PHP Code:
                    /**
                    * CORE EDIT START
                    */
                    if ($this->collection->useDefinedLocalesOnly) {
                        $supportedLocales config('App')->supportedLocales;

                        if (!in_array($temp[$localeSegment], $supportedLocales)) {
                            return false;
                        }
                    }
                    /**
                    * CORE EDIT END
                    */ 

Add use to: app/Config/Services.php

PHP Code:
use App\Services\Router

and function

PHP Code:
    /**
    * @param bool $getShared
    * @return Routes
    */
    public static function routes($getShared true): Routes {
        if ($getShared) {
            return static::getSharedInstance('routes');
        }

        return new Routes(AppServices::locator(), config('Modules'));
    
Reply




Theme © iAndrew 2016 - Forum software by © MyBB