Skip to content

Instantly share code, notes, and snippets.

What would you like to do?
Language context router for use with Babel extra for MODX Revolution
/* LangRouter
* ==========
* This plugin is meant to be used with Babel extra for MODX Revolution. It
* takes care of switching contexts, which hold translations, depending on URL
* requested by client. LangRouter works with so called subfolder based setup,
* in which many languages are served under a single domain but are
* differentiated by a virtual subfolder indicating the language, eg.
* The routing work as follows:
* - if URI contains cultureKey, which is defined in Babel configuration, then
* the matching context is served
* - if URI doesn't contain cultureKey (or one not defined in Babel
* configuration) AND at least one of the client's accepted languages is
* defined in Babel configuration, then the matching context is served
* - otherwise the default context is served
* LangRouter works out-of-the-box and doesn't require any changes to URL
* rewrite rules in the webserver configuration. All routing is handled
* internally by MODX. This greatly simplifies the setup and provides
* portability. LangRouter was tested with Apache and Lighttpd.
* Setup:
* 1. Prepare your contexts as you normally would for Babel.
* 2. For each context set `base_url` to `/`.
* 3. For each context set `site_url` to
* `{server_protocol}://{http_host}{base_url}{cultureKey}/`
* 4. Add new system setting `babel.contextDefault` and set it to the default
* context, which should be served when no language is specified in
* request, eg. `pl`.
* 5. Include static files from the assets folder with
* `[[++assets_url]]path/to/static_file`.
* 6. In template header use `<base href="[[++site_url]]" />`.
* 7. Use default URL generation scheme in MODX (ie. relative).
* This code is shared AS IS. Use at your own risk.
if($modx->context->get('key') != "mgr") {
* Debugs request handling
function logRequest($message = 'Request')
global $modx;
$modx->log(modX::LOG_LEVEL_ERROR, $message . ':'
. "\n q: " . $_REQUEST['q']
. "\n Context: " . $modx->context->get('key')
. "\n Site start: " . $modx->context->getOption('site_start')
* Dumps variables to MODX log
function dump($var) {
return ob_get_clean();
* Detects client language preferences and returns associative array sorted
* by importance (q factor)
function clientLangDetect()
$langs = array();
# break up string into pieces (languages and q factors)
preg_match_all('/([a-z]{1,8}(-[a-z]{1,8})?)\s*(;\s*q\s*=\s*(1|0\.[0-9]+))?/i', $_SERVER['HTTP_ACCEPT_LANGUAGE'], $lang_parse);
if (count($lang_parse[1])) {
# create a list like "en" => 0.8
$langs = array_combine($lang_parse[1], $lang_parse[4]);
# set default to 1 for any without q factor
foreach ($langs as $lang => $val) {
if ($val === '') $langs[$lang] = 1;
# sort list based on value
arsort($langs, SORT_NUMERIC);
return $langs;
#logRequest('Unhandled request');
# Get contexts and their cultureKeys
$babelContexts = explode(',', $modx->getOption('babel.contextKeys'));
$languages = array();
foreach ($babelContexts as $context) {
$ctx = $modx->getContext($context);
$languages[$ctx->config['cultureKey']] = trim($context);
#$modx->log(modX::LOG_LEVEL_ERROR, dump($languages));
# Determine language from request
$reqCultureKeyIdx = strpos($_REQUEST['q'], '/');
$reqCultureKey = substr($_REQUEST['q'], 0, $reqCultureKeyIdx);
# Serve the proper context and language
strtolower($reqCultureKey), array_change_key_case($languages))) {
# Remove cultureKey from request
$_REQUEST['q'] = substr($_REQUEST['q'], $reqCultureKeyIdx+1);
#logRequest('Culture key found in URI');
} else {
$clientCultureKey = array_flip(
array_intersect_key(clientLangDetect(), $languages));
if($clientCultureKey) {
$contextDefault = current($clientCultureKey);
} else {
$contextDefault = trim($modx->getOption('babel.contextDefault'));
#$modx->log(modX::LOG_LEVEL_ERROR, dump($contextDefault));
#logRequest('Culture key not found in URI');
# Serve site_start when no resource is requested
if(empty($_REQUEST['q'])) {
#$modx->log(modX::LOG_LEVEL_ERROR, 'Query is empty');

This comment has been minimized.

Copy link

@sottwell sottwell commented Dec 22, 2012

This did not work with the tutorial at

The web context being given a "folder" in the site_url and base_url ( i.e. /fr/ ) caused the plugin to try to load that non-existent context instead of the web context. Just before line 114 I added:

  if($reqCultureKey == $defaultLang) $reqCultureKey = 'web';

Then in the plugin's Properties I added defaultLang, with the value for the web context's base_url "folder", in this case fr.


This comment has been minimized.

Copy link

@goldsky goldsky commented Nov 30, 2014

sorry, which Event should this plugin applied to?


This comment has been minimized.

Copy link

@sottwell sottwell commented Dec 4, 2014

OnHandleRequest, just like the standard plugins.

Another point; there are two places where the key used for the context switch needs to be modified, lines 114 and 127, just before the switchContext functions. Your requirements may be different if your default context is not the system default cultureKey. I added the babel.contextDefault System Setting, then used
if($reqCultureKey == $modx->getOption('cultureKey')) $reqCultureKey = $modx->getOption('babel.contextDefault');
for line 114, and
if($contextDefault == $modx->getOption('cultureKey')) $contextDefault = $modx->getOption('babel.contextDefault');
for line 127.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.