I'm building an international site and am running into trouble with the language detection. I need to have urls structured like the below examples for SEO purposes, but nothing I've found will allow for that. I wrote a little module that hooked into pathauto to prepend the code to the alias, but it didn't seem to be recognized... I'm using Organic Groups and Domain Access to manage the multiple countries, and Entity Translation and i18n for the languages. I also have Global Redirect in place to help keep out duplicate content links. One requirement is that for some countries the default language should not be prefixed at all.

Has anybody had experience with urls like this? It seems like it should be possible without hacking into the core...

Country     |   Language    |   URL

Austria     |   German      |   www.domain.com/at
Belgium     |   Dutch       |   www.domain.com/nl-be
Belgium     |   French      |   www.domain.com/fr-be
Canada      |   English     |   www.domain.com/en-ca
Canada      |   French      |   www.domain.com/fr-ca
Global      |   English     |   global.domain.com/en
Global      |   French      |   global.domain.com/fr

2 Answers 2


I was able to accomplish this by creating a new language negotiation provider. It was mostly a matter of just copying the hooks and callbacks with only minor changes and a lot of trial and error. There wasn't much documentation that I could find on it... The only thing is that the first part of the url MUST be a language, though it can also be a directory.

  • So this ended up not working... There are apparently other spots within Drupal that don't like this structure. We opted to go with the standard URL negotiation.
    – Derefro
    Commented Mar 11, 2015 at 19:59
  • Hi Defero, could you maybe elaborate on which modules or functions are incompatible with restructuring the language prefix through hook_url_outbound_alter and hook_url_inbound_alter? I wrote a basic setup that does exactly what you want in your example above and it seems to be working (only basic testing done for now). See my code in my answer below. Commented Mar 9, 2017 at 17:48
  • The solution I wrote about above did appear to work, but was quickly abandoned due to other factors (that I no longer remember). We were essentially building multiple "sites" that all lived as subdirectories. OG handled the sites, but there were conflicts with the way the directories needed to function. We ended up using the standard drupal setup of /lang/sitedir.
    – Derefro
    Commented Mar 10, 2017 at 19:40

I'm currently working on a project that needs to achieve the exact same URL-structure as you mention in your question.

Below you'll find what I have so far. If you could let me know of any pitfalls you experienced that would be awesome!

  • all content-types get a country-field that saves the target country (just a simple drupal.org/project/country field. no OG or anything fancy)
  • my pathauto-pattern for all content is something like this: [ISO-country-code]/[node-title]
  • all content can be translated into all available languages (for now: Dutch, Vlamish (nl_be), English and French)
  • I wrote a small module that uses hook_url_*_alter
  • I hade to change the module weight to something very high (99) so my url_*_alter hooks are being called AFTER locale_url_outboud_alter from locale.module

This is my module code:

function flexas_cypaths_url_inbound_alter(&$path, $original_path, $path_language){
    $args = explode('/', $path);
    if(preg_match('/^([a-z]{2})_([a-z]{2})/', $args[0], $matches)) {
        $args[0] = $matches[1];
        $new_path = implode('/', $args);
        if($internal_path = drupal_lookup_path('source', $new_path, $matches[2])){
            $path = $internal_path;

function flexas_cypaths_url_outbound_alter(&$path, &$options, $original_path){
    $args = explode('/', $path);
    $c_list = array('nl','en', 'be');
    $system_paths = array('admin', 'user', 'sites', 'system', '<front>', '', 'js');
    $exclude_paths = array('node/add', 'sitemap');
    if (in_array($args[0], $system_paths) || strposa($path, $exclude_paths)) {
        // do nothing;
    }else if (in_array($args[0], $c_list) == false) {
        $country_prefix = '';
        $lang = NULL;
            $lang = $options['language']->language;
        if($alias = drupal_get_path_alias($path, $lang)){
            $alias_args = explode('/', $alias);
            if(preg_match('/^([a-z]{2})$/', $alias_args[0], $matches_first)){
                $country_prefix = $alias_args[0] . '_';
                $path = implode('/', $alias_args);
                $options['alias'] = 1;
        if(isset($options['prefix']) && !empty($options['prefix'])){
            $options['prefix'] = $country_prefix . rtrim($options['prefix'], '/') . '/';

function strposa ($haystack, $needles) {
    $bool = false;
    foreach ($needles as $needle) {
        if (strpos($haystack, $needle) !== false) {
            $bool = true;
    return $bool;

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.