Multi-Lingual - Redirect to always have code in url?

Is it possible to force urls to always have the country code in front of them? E.g. if a user visits “/projects” it would redirect to “/en/projects”?

Interesting question. I haven’t tested it, but have you tried something like this?

c::set('languages', array(
  array(
    'code'    => 'en',
    'name'    => 'English',
    'default' => true,
    'locale'  => 'en_US',
    'url'     => '/en',
  ),
  array(
    'code'    => 'de',
    'name'    => 'Deutsch',
    'locale'  => 'de_DE',
    'url'     => '/de',
  ),
));

That should at least work for subpages, but I really don’t know what will happen for the homepage.

Yes, that definitely works.

This does not seem to work for me. Running 2.2.3.
If I visit /projects it does not redirect to /en/projects.

This is a bit weird, because the start page does in fact redirect. You probably need an additonal route, then.

Would I have to put in a route for every section on my site?

No, a single route should be sufficient.

No, redirecting subpages without language codes is not going to work as this would be guesswork for Kirby. Let’s say you have a page with the URI de. This would break the redirection.

As @texnixe wrote, you will need a route for this. But please note that the route will have the same problem in edge-cases.

I came up with this, which seems to work, though it looks a bit hacky:

c::set('routes', array(
  array(
    'pattern' => '(:any)',
    'action' => function($uri) {
      if ($uri == 'de' or $uri == 'en' or substr( $uri, 0, 2 ) === "de" || substr($uri, 0, 2) == 'en') {
        $site = kirby()->site();
        $page = page('home');
        return $site->visit($page);
      } else {
          $site = kirby()->site();
          $page = page($uri);
          go('en/' . $page);
      }
   }
  )
));
1 Like

Do you know if there is any way to have a not-match in the pattern? Not seeing anything in the documentation.

The pattern is a regular expression, you can use any valid expression there. But not matching if the URL contains something is generally pretty difficult with regular expressions.

What exactly do you want to do?

I was concerned about the overhead of processing each request through the route so wanted the pattern to basically contain NOT “/(en|fr|de)/(:any)”.

Should I be concerned about overhead, would something like texnixe’s post slow down my site?

Kirby uses a fallback route itself internally and the first matching route will win, so you are basically just replacing Kirby’s route with your one.

You could use negative lookbehinds or other tricks like (*SKIP)(*F), but complex regular expressions are slower than simpler ones, so I think just matching everything should be fine here.

I made some changes to @texnixe solution to make it a bit more flexible.

c::set('routes', array(
    array(
        'pattern' => '(:any)',
        'action' => function($uri) {
            
            $lang   = substr( $uri, 0, 2 );
            $codes  = array_keys(c::get('languages'));

            $site   = kirby()->site();

            if (in_array($lang, $codes)) {

                // URI contains the language, all good just show the page.
                $path = ($lang == $uri) ? c::get('home') : substr($uri, 3);

                $page = page('home');
                return $site->visit($path);

            } else {

                // No language code, redirect.                   
                $page = page($uri);
                go($site->language()->code() . '/' . $page);

            }
        }
    )
));

This just required changing the language config to have named keys. Much easier to pull out a list of all languages without having to iterate over it. Doesn’t seem to mess up Kirby at all.

c::set('languages', array(
    'en' => array(
        'code'    => 'en',
        'name'    => 'EN',
        'default' => true,
        'locale'  => 'en_US',
        'url'     => '/en',
    ),
    'fr' => array(
        'code'    => 'fr',
        'name'    => 'FR',
        'locale'  => 'fr-FR',
        'url'     => '/fr',
    ),
    'cn' => array(
        'code'    => 'cn',
        'name'    => '中文',
        'locale'  => 'zh-CN',
        'url'     => '/cn',
    ),
));

Looks good, but you could improve it a little bit more:

c::set('routes', array(
    array(
        'pattern' => '(:any)',
        'action' => function($uri) {
            
            $lang  = substr($uri, 0, 2);
            $codes = array_keys(c::get('languages'));

            if (in_array($lang, $codes)) {

                // URI contains the language, all good just show the page.
                $path = ($lang == $uri) ? c::get('home') : substr($uri, 3);

                return site()->visit($path, $lang);

            } else {

                // No language code, redirect.                   
                $page = page($uri);
                go(site()->language()->code() . '/' . $page);

            }
        }
    )
));
1 Like

Ah yea, not sure why I had that extra $page part. Looks good, thanks.

For anyone checking this out in the future. Small update to make sure page exists before redirecting.

c::set('routes', array(
    array(
        'pattern' => '(:any)',
        'action' => function($uri) {
            
            $lang   = substr( $uri, 0, 2 );
            $codes  = array_keys(c::get('languages'));

            $site   = kirby()->site();

            if (in_array($lang, $codes)) {

                // URI contains the language, all good just show the page.
                $path = ($lang == $uri) ? c::get('home') : substr($uri, 3);

                return $site->visit($path, $lang);

            } else {

                // No language code, redirect to final page.
                $page = page($uri);

                if(!$page) {
                    // Oops, this page doesn't exists!
                    $page = $site->errorPage();
                    return $site->visit($page, $lang);
                } else {
                    go($site->language()->code() . '/' . $page);
                }

            }
        }
    )
));
1 Like