Skip to content

Instantly share code, notes, and snippets.

@croxton
Last active April 5, 2022 19:11
Show Gist options
  • Save croxton/38bab45e273ac42d15fd to your computer and use it in GitHub Desktop.
Save croxton/38bab45e273ac42d15fd to your computer and use it in GitHub Desktop.
Using Resource Router for roll-your-own multi-language ExpressionEngine sites

Multi-language routing with Resource Router

The idea is to set up ISO 639-1 language code prefixes for urls (e.g. en/, de/, fr/), and set global variables {global:lang} and {global:lang_dir} for use in your templates.

In the control panel create a category group "languages" and populate it with your languages, setting the category url title to the appropriate 2-letter language code. Assign the category group to the channels you want to translate.

For example:

Category Name Category URL Title
Deutsch (de) de
English (en) en
Français (fr) fr
日本語 (ja) ja
한국의 (ko) ko
中文 (zh) zh

Then set up these rules with Resource Router:

<?php

$config['resource_router'] = array(

	// set the default language and direction for all routes
	'(.*)' =>  function($router, $wildcard) {
	
		// default language, when there is no language code prefix
		$lang = 'en'; 
		$router->setGlobal('global:lang', $lang);

		// default direction
		$router->setGlobal('global:lang_dir', 'ltr'); 

		// setup default language category meta data
		$router->setWildcard(1, $lang);
		$wildcard->isValidCategoryUrlTitle();
		
		return; // signify this rule does not match a route
	},

	// do we have a language code in the first segment?
	'([a-z]{2})/:all' =>  function($router, $lang) {

		// yes - check the 2-letter language code is a valid category url title
		// if found, this will set the category meta data as global variables
		if (FALSE === $lang->isValidCategoryUrlTitle())
		{
			$router->set404();
		}

		// set the current language
		$router->setGlobal('global:lang', $lang);

		// reverse the text direction for known RTL languages
		switch($lang) {
			case 'ar' : case 'fa' : case 'ur' :
				$router->setGlobal('global:lang_dir', 'rtl');
				break;
		}

		// remove the language segment
		array_shift(ee()->uri->segments);
		
		// overwrite {segment_x} variables in templates
		foreach(ee()->uri->segments as $key => $value) {
            		$router->setGlobal('segment_'.($key+1), $value);
		}

		// overwrite the uri string too
		ee()->uri->uri_string = implode('/', ee()->uri->segments);

		return; // signify this rule does not match a route
	},	

	/* any custom routes here */

	/* re-route any url prefixed with a country code not matched by above rules */
	'([a-z]{2})/(.*)' =>  function($router, $lang, $url) {
    		$router->setTemplate($url);
	}
);

Now you can either use an asynchronous approach whereby separate entries are created for each language, or a synchronous approach where a single entry is translated into multiple languages via prefixed custom fields. In either case assigning the entries to the relevant language category(s) provides a native and fast mechanism for filtering entries by current language category id, which Resource Router makes available as {route_1_cat_id}. For example:

{!-- list blog entries in the current language --}
{exp:channel:entries 
    channel="blog"
    category="{route_1_cat_id}"
    dynamic="no"
    disable="member_data|pagination"
}
@croxton
Copy link
Author

croxton commented Jun 21, 2017

Here's an EE3 version:

$config['resource_router'] = array(

	// set the default language and direction for all routes
	'(.*)' =>  function($router, $wildcard) {
	
		// default language, when there is no language code prefix
		$lang = 'en'; 
		$router->setGlobal('global:lang', $lang);
		$router->setGlobal('global:lang_uri', '');

		// default direction
		$router->setGlobal('global:lang_dir', 'ltr'); 

		// setup default language category meta data
		$router->setWildcard(1, $lang);
		$wildcard->isValidCategoryUrlTitle();
		
		return; // signify this rule does not match a route
	},

	// do we have a language code in the first segment?
	'([a-z]{2})/:all' =>  function($router, $lang) {

		// yes - check the 2-letter language code is a valid category url title
		// if found, this will set the category meta data as global variables
		if (FALSE === $lang->isValidCategoryUrlTitle())
		{
			$router->set404();
		}

		// set the current language
		$router->setGlobal('global:lang', $lang);
		$router->setGlobal('global:lang_uri', $lang . '/');

		// reverse the text direction for known RTL languages
		switch($lang) {
			case 'ar' : case 'fa' : case 'ur' :
				$router->setGlobal('global:lang_dir', 'rtl');
				break;
		}

		$uri_array = array();

		// remove language segment
		array_shift(ee()->uri->segments);

		// reindex uri array starting at 1
		foreach(ee()->uri->segments as $key => $value) 
		{
			$uri_array[$key+1] = $value;
		}
		ee()->uri->segments = $uri_array;
		
		// overwrite {segment_x} variables in templates
		foreach(ee()->uri->segments as $key => $value) {
			$router->setGlobal('segment_'.$key, $value);
		}

		// overwrite the uri string too
		ee()->uri->uri_string = implode('/', ee()->uri->segments);

		return; // signify this rule does not match a route
	}
);	

@croxton
Copy link
Author

croxton commented Jun 21, 2017

And here's an EE3 version with support for Construct routing:

$config['resource_router'] = array(

	// set the default language and direction for all routes
	'(.*)' =>  function($router, $wildcard) {
	
		// default language, when there is no language code prefix
		$lang = 'en'; 
		$router->setGlobal('global:lang', $lang);
		$router->setGlobal('global:lang_uri', '');

		// default direction
		$router->setGlobal('global:lang_dir', 'ltr'); 

		// setup default language category meta data
		$router->setWildcard(1, $lang);
		$wildcard->isValidCategoryUrlTitle();
		
		return; // signify this rule does not match a route
	},

	// do we have a language code in the first segment?
	'([a-z]{2})/:all' =>  function($router, $lang) {

		// yes - check the 2-letter language code is a valid category url title
		// if found, this will set the category meta data as global variables
		if (FALSE === $lang->isValidCategoryUrlTitle())
		{
			$router->set404();
		}

		// set the current language
		$router->setGlobal('global:lang', $lang);
		$router->setGlobal('global:lang_uri', $lang . '/');

		// reverse the text direction for known RTL languages
		switch($lang) {
			case 'ar' : case 'fa' : case 'ur' :
				$router->setGlobal('global:lang_dir', 'rtl');
				break;
		}

		$uri_array = array();

		// remove language segment
		array_shift(ee()->uri->segments);

		// reindex uri array starting at 1
		foreach(ee()->uri->segments as $key => $value) 
		{
			$uri_array[$key+1] = $value;
		}
		ee()->uri->segments = $uri_array;
		
		// overwrite {segment_x} variables in templates
		foreach(ee()->uri->segments as $key => $value) {
			$router->setGlobal('segment_'.$key, $value);
		}

		// overwrite the uri string too
		ee()->uri->uri_string = implode('/', ee()->uri->segments);

		// do construct routing with the rewritten uri
		$routing = new \BuzzingPixel\Construct\Controller\Routing();
		$constructRoute = $routing->process(ee()->uri->uri_string);

		// set template
		$router->setTemplate($constructRoute);

		return; // signify this rule does not match a route
	}
);	

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment