WordPress add_rewrite_tag(), add_rewrite_rule(), and post_link()

I am trying to do the following:

Rewrite the URL structure of my WordPress installation so that a language field is in there. E.g. http://www.mydomain.com/lang/

Read More

I want to then take the input from /lang/ and use it to display the appropriate content. E.g. if lang is ‘en’ I will take custom fields in English and display the theme in English.

Here is what I have so far:

<?php
 function add_directory_rewrite() {
    global $wp_rewrite;
    add_rewrite_tag('%lang%', '(.+)');
    add_rewrite_rule('^/(.+)/', 'index.php?p=$matches[1]&lang=$matches[2]', 'top');
    add_permastruct('lang', '%lang%');

   }
    add_action( 'init', 'add_directory_rewrite' );
?>

This works as far as getting the language but the problem I am facing is now the_permalink() has “/%lang%/” where /en/ is supposed to be or /fr/ or /de/ or whatever language. To add more detail my permalink structure is /%lang%/%category%/%postname%/ and lets say I have a category called food and a post with the title chicken, the_permalink will generate http://www.mydomain.com/%lang%/food/chicken/

Any idea what I’m doing wrong? Cheers.

Related posts

Leave a Reply

3 comments

  1. You additionally need to add a function that will take the permalink that contains the erroneous ‘/%lang%/’ segment, and replace it with the appropriate default language for the post. Typically you can do this either by the 'pre_post_link' filter, or the 'post_link' filter. If you use the former, you will be creating the permalink from scratch (totally custom urls that use nothing that core WP has to offer). If the latter is use, then you can filter the permalink after WP has done it’s magic, but before it is used on the site. Here is an example:

    function lou_rewrite_lang_in_permalink($permalink, $post, $leavename) {
      // find the default post language via a function you have created to 
      // determine the default language url. this could be based on the current
      // language the user has selected on the frontend, or based on the current
      // url, or based on the post itself. it is up to you
      $default_post_language = get_default_post_lang($post);
    
      // once you have the default language, it is a simple search and replace
      return str_replace('%lang%', $lang, $permalink);
    }
    add_filter('post_link', 'lou_rewrite_lang_in_permalink', 11, 3);
    

    You don’t mention it so I will. With your original solo function, you are gunna have a hard time if it is stand alone. The reason is because, though you have told the rewriter that a new url segment exists, you didn’t tell WordPress to expect it as a url param. Thus, even though you have some fancy code to rewrite the url and tell WordPress the fancy lang param, WordPress does not know that it should be looking for it, and thus ignores it. You need to add something like this to rectify that:

    function lou_add_lang_query_var($vars) {
      // tell WP to expect the lang query_var, which you can then later use
      $vars[] = 'lang';
    
      // return the new list of query vars, which includes our lang param
      return array_unique($vars);
    }
    add_filter('query_vars', 'lou_add_lang_query_var', 10, 1);
    

    This will tell the WP() class that it needs to accept the 'lang' instead of just skipping over it. Then later you can do something like this to figure out that the current page sent as it’s language:

    function lou_somefunction() {
      // do stuff
      ...
    
      // access the $wp object
      global $wp;
    
      // determine the language from the query_vars of the current page
      $lang = $wp->query_var['lang'];
    
      // do other stuff with $lang
      ...
    }
    

    Hope this helps.

    EDIT

    First I want to say, this is an absolute travesty that language urls are not natively supported by WordPress. I have honestly never needed to do this, but most of my clients are not international companies, with international needs. I will be submitting something to WordPress in code form to solve this in a later version, but as of now, you will need a plugin like the one I have created below.

    So I did a lot of investigation to make this happen. After a short conversation with the questioner, if found that my solution was incomplete. Naturally, I started digging. What seems like it should be an otherwise mediocre task, has turned out to be a VERY-not-mediocre task. The short version is, WordPress simply does not want you to insert extra parts of the url, in the middle or beginning of the url, on every url. You can easily do this with post urls ONLY with the above code, but anything more (pages, attachments, author pages, etc…) you must do something special. You can also add parts to the end of the url (endpoints), but even that is complicated.

    I have worked with the WordPress rewriter extensively in the past and present, and I have what is considered Expert knowledge on the topic. Despite that, it still took me 4-5 hours to write something that will allow you to prepend a language indicator to all urls, that can then later be used to determine what language the page should be displayed in, regardless of page type. There is one catch, that I think is acceptable. You must know and specify exactly what language prefixes you want to support. I don’t foresee this as a problem for anyone who would make use of this, but none-the-less, it is a limitation, simply because of the way that the rewrite engine works.

    At long last, here is a plugin that you can use to accomplish this. I works on a barebone WP install, with a WooTheme as the theme. If you have other third party plugins installed, there is a possibility that this will not work for all their urls, depending on how they added their rewrite rules. In the short term, I will probably be converting this to a plugin for WP, and getting it up on WordPress.org, but that is several days away, at least. Here is a working prototype of the code in plugin form. Create a new directory in your plugins folder (something like /wp-content/plugins/lou-lang), and then paste this code in a php file inside that folder (something like /wp-content/plugins/lou-lang/lou-lang.php). Then activate the plugin, via your admin dashboard, which will be labeled ‘Loushou Language URLs’.

    CODE:

    <?php (__FILE__ == $_SERVER['SCRIPT_FILENAME']) ? die(header('Location: /')) : null;
    /**
     * Plugin Name: Loushou Language URLs
     * Plugin URI:  http://quadshot.com/
     * Description: Adding the ability to have language support in your frontend urls.
     * Version:     0.1-beta
     * Author:      Loushou
     * Author URI:  http://quadshot.com/
     */
    
    class lou_rewrite_takeover {
      protected static $add_rules = array();
    
      public static function pre_init() {
        // debug
        add_action('admin_footer-options-permalink.php', array(__CLASS__, 'qsart_rewrite_debug'));
    
        // add rw tag
        add_action('init', array(__CLASS__, 'add_directory_rewrite'));
    
        // rw rule adds
        add_filter(is_admin() ? 'setup_theme' : 'do_parse_request', array(__CLASS__, 'do_parse_request'), 0);
        add_filter('post_rewrite_rules', array(__CLASS__, 'post_rewrite_rules'));
        add_filter('date_rewrite_rules', array(__CLASS__, 'date_rewrite_rules'));
        add_filter('root_rewrite_rules', array(__CLASS__, 'root_rewrite_rules'));
        add_filter('comments_rewrite_rules', array(__CLASS__, 'comments_rewrite_rules'));
        add_filter('search_rewrite_rules', array(__CLASS__, 'search_rewrite_rules'));
        add_filter('author_rewrite_rules', array(__CLASS__, 'author_rewrite_rules'));
        add_filter('page_rewrite_rules', array(__CLASS__, 'page_rewrite_rules'));
        add_filter('rewrite_rules_array', array(__CLASS__, 'final_rules_correction'), PHP_INT_MAX, 1);
    
        // query vars
        add_filter('query_vars', array(__CLASS__, 'add_lang_query_var'), 10, 1);
        add_filter('request', array(__CLASS__, 'default_language'), 9);
    
        // fix permalinks
        $link_filters_needing_rewrite = array(
          'post_link',
          'post_type_link',
          'page_link',
          'attachment_link',
          'search_link',
          'post_type_archive_link',
          'year_link',
          'month_link',
          'day_link',
          'feed_link',
          'author_link',
          'term_link',
          'category_feed_link',
          'term_feed_link',
          'taxonomy_feed_link',
          'author_feed_link',
          'search_feed_link',
          'post_type_archive_feed_link',
        );
        add_filter('pre_post_link', array(__CLASS__, 'change_permalink_structure'), 10, 3);
        foreach ($link_filters_needing_rewrite as $link_filter)
          add_filter($link_filter, array(__CLASS__, 'rewrite_lang_in_permalink'), 11, 3);
      }
    
      public static function do_parse_request($cur) {
        self::get_page_permastruct();
        self::get_author_permastruct();
        self::correct_extras();
        return $cur;
      }
    
      public static function get_supported_langs() {
        return apply_filters('lou-get-supported-languages', array(
          'en',
        ));
      }
    
      public static function add_directory_rewrite() {
        global $wp_rewrite;
        $supported_languages = self::get_supported_langs();
        add_rewrite_tag('%lang%', '('.implode('|', $supported_languages).')');
      }
    
      public static function unleadingslashit($str) {
        return ltrim($str, '/');
      }
    
      public static function final_rules_correction($rules) {
        global $wp_rewrite;
    
        $new_rules = array();
        $supported_languages = self::get_supported_langs();
        $find = implode('|', $supported_languages);
        $find_find = '#(?<!()('.preg_quote($find, '#').')#';
        $preg_node = str_replace('%%%', '(d+)', preg_quote($wp_rewrite->preg_index('%%%'), '#'));
    
        foreach ($rules as $k => $v) {
          if (preg_match($find_find, $k)) {
            $nk = preg_replace($find_find, '('.$find.')', $k);
            $parts = explode('?', $v);
            $index = array_shift($parts);
            $pv = implode('?', $parts);
            $pv = preg_replace_callback('#'.$preg_node.'#', function ($matches) use ($wp_rewrite) {
              return $wp_rewrite->preg_index($matches[1]+1);
            }, $pv);
            $nv = $index.'?lang='.$wp_rewrite->preg_index(1).(!empty($pv) ? '&'.$pv : '');
            $new_rules[$nk] = $nv;
          } else {
            $new_rules[$k] = $v;
          }
        }
    
        return $new_rules;
      }
    
      public static function change_permalink_structure($struct) {
        $struct = self::unleadingslashit($struct);
        $struct = preg_replace('#^%lang%/?#', '', $struct);
        return '/%lang%/'.$struct;
      }
    
      public static function extras_rewrite_rules($rules, $struct) {
        global $wp_rewrite;
    
        if ( is_array( $struct ) ) {
          if ( count( $struct ) == 2 )
            $new_rules = $wp_rewrite->generate_rewrite_rules( self::change_permalink_structure($struct[0]), $struct[1] );
          else
            $new_rules = $wp_rewrite->generate_rewrite_rules( self::change_permalink_structure($struct['struct']), $struct['ep_mask'], $struct['paged'], $struct['feed'], $struct['forcomments'], $struct['walk_dirs'], $struct['endpoints'] );
        } else {
          $new_rules = $wp_rewrite->generate_rewrite_rules( self::change_permalink_structure($struct) );
        }
    
        return $new_rules + $rules;
      }
    
      public static function post_rewrite_rules($rules) {
        global $wp_rewrite;
    
        // hack to add code for extras type urls (usually created by other plugins)
        $func = array(__CLASS__, 'extras_rewrite_rules');
        foreach ($wp_rewrite->extra_permastructs as $type => $struct) {
          $filter = ($type == 'post_tag' ? 'tag' : $type).'_rewrite_rules';
          add_filter($filter, function ($rules) use ($struct, $func) { return call_user_func_array($func, array($rules, $struct)); });
        }
    
        return $wp_rewrite->generate_rewrite_rules( self::change_permalink_structure($wp_rewrite->permalink_structure), EP_PERMALINK ) + $rules;
      }
    
      public static function date_rewrite_rules($rules) {
        global $wp_rewrite;
        return $wp_rewrite->generate_rewrite_rules( self::change_permalink_structure($wp_rewrite->get_date_permastruct()), EP_DATE) + $rules;
      }
    
      public static function root_rewrite_rules($rules) {
        global $wp_rewrite;
        return $wp_rewrite->generate_rewrite_rules( self::change_permalink_structure($wp_rewrite->get_date_permastruct()), EP_DATE) + $rules;
      }
    
      public static function comments_rewrite_rules($rules) {
        global $wp_rewrite;
        return $wp_rewrite->generate_rewrite_rules( self::change_permalink_structure($wp_rewrite->root . $wp_rewrite->comments_base), EP_COMMENTS, false, true, true, false) + $rules;
      }
    
      public static function search_rewrite_rules($rules) {
        global $wp_rewrite;
        return $wp_rewrite->generate_rewrite_rules( self::change_permalink_structure($wp_rewrite->get_search_permastruct()), EP_SEARCH) + $rules;
      }
    
      public static function author_rewrite_rules($rules) {
        global $wp_rewrite;
        return $wp_rewrite->generate_rewrite_rules( self::change_permalink_structure($wp_rewrite->get_author_permastruct()), EP_AUTHORS) + $rules;
      }
    
      public static function page_rewrite_rules($rules) {
        global $wp_rewrite;
        $page_structure = self::get_page_permastruct();
        return $wp_rewrite->generate_rewrite_rules( $page_structure, EP_PAGES, true, true, false, false ) + $rules;
      }
    
      protected static function get_page_permastruct() {
        global $wp_rewrite;
    
        if (empty($wp_rewrite->permalink_structure)) {
          $wp_rewrite->page_structure = '';
          return false;
        }
    
        $wp_rewrite->page_structure = self::change_permalink_structure($wp_rewrite->root . '%pagename%');
    
        return $wp_rewrite->page_structure;
      }
    
      protected static function get_author_permastruct() {
        global $wp_rewrite;
    
        if ( empty($wp_rewrite->permalink_structure) ) {
          $wp_rewrite->author_structure = '';
          return false;
        }
    
        $wp_rewrite->author_structure = self::change_permalink_structure($wp_rewrite->front . $wp_rewrite->author_base . '/%author%');
    
        return $wp_rewrite->author_structure;
      }
    
      protected static function correct_extras() {
        global $wp_rewrite;
    
        foreach ($wp_rewrite->extra_permastructs as $k => $v)
          $wp_rewrite->extra_permastructs[$k]['struct'] = self::change_permalink_structure($v['struct']);
      }
    
      public static function get_default_post_lang($post) {
        return ( $lang = get_query_var('lang') ) ? $lang : 'en';
      }
    
      public static function rewrite_lang_in_permalink($permalink, $post=0, $leavename=false) {
        // find the default post language via a function you have created to 
        // determine the default language url. this could be based on the current
        // language the user has selected on the frontend, or based on the current
        // url, or based on the post itself. it is up to you
        $lang = self::get_default_post_lang($post);
    
        // once you have the default language, it is a simple search and replace
        return str_replace('%lang%', $lang, $permalink);
      }
    
      public static function add_lang_query_var($vars) {
        // tell WP to expect the lang query_var, which you can then later use
        $vars[] = 'lang';
    
        // return the new list of query vars, which includes our lang param
        return array_unique($vars);
      }
    
      public static function default_language($vars) {
        if (array_diff( array_keys($vars), array('preview', 'page', 'paged', 'cpage') ))
          $vars['lang'] = !empty($vars['lang']) ? $vars['lang'] : 'en';
        return $vars;
      }
    
      public static function qsart_rewrite_debug() {
        if (isset($_COOKIE['rwdebug']) && $_COOKIE['rwdebug'] == 1) {
          global $wp_rewrite;
          echo '<pre style="background-color:#ffffff; font-size:10px;">';
          print_r($wp_rewrite->rules);
          echo '</pre>';
        }
      }
    }
    
    if (defined('ABSPATH') && function_exists('add_action')) {
      lou_rewrite_takeover::pre_init();
    }
    

    By default, the only language code supported by this plugin is 'en'. Obviously you need more than just that. Thus, once you have installed the plugin, you can add some code to your <theme>/functions.php file that looks something like this, to add the remainders.

    function more_languages($list) {
      $my_languages = array(
        'de', 'zh', 'bg', 'fr'
      );
      return array_unique($list + $my_languages);
    }
    add_filter('lou-get-supported-languages', 'more_languages', 10, 1);
    

    Once you have both installed the plugin and defined your custom languages, then you have one final step. You must save your permalinks. To do this from the admin, go to: Settings -> Permalinks -> Save Changes (button). After all of this, you should be good to go!

    Hopefully this helps someone, and hopefully I can block out some time to get this up on wp.org.

  2. The question is old but.. I was working on a lightweight solution for multi-language site and i came across the same problem. There is no easy way to do it with built-in WordPress functions. However (like mentioned by user1254824) there is a really easy trick to achieve it.

    You can intercept the $_SERVER['REQUEST_URI'] global var, extract the /lang/ part and remove it before WordPress parsing. WordPress will serve you the regular page, but now you have your lang parameter in a var.

    function localization_uri_process() {
    
      global $lang; // then you can access $lang value in your theme's files
    
      if( preg_match("%A/([a-z]{2})/%", $_SERVER['REQUEST_URI'], $matches) ){
        $lang = $matches[1];
        $_SERVER['REQUEST_URI'] = preg_replace("%A/[a-z]{2}/%", '/', $_SERVER['REQUEST_URI'] );
      }
      else
        $lang = 'en'; // your default language
    }
    add_action( 'plugins_loaded', 'localization_uri_process' );
    

    Then, you can also use filters to automatically rewrite all your links.

    PS : The code need to be in a plugin. It won’t work in your template php files.

  3. I stumbled across this post while looking for a solution to put a language tag in front of the url path. While the wp_rewrite-solution is pretty much solid it kind of didn’t work for my purpose (e.g. not having the language tag in front for the default language etc.).

    So I took a closer look at the qTranslate-plugin and after I while I figured out that it uses a very simple and elegant solution:

    Basically it does two things:

    1. (Obviously) It changes all the WordPress generated links (e.g. post_link, post_type_link, page_link etc.) to include the correct language tag in the url.

    2. Instead of manipulating complex rewrite rules to have wordpress accept and correctly handle the language tag, it just hooks into “plugins_loaded” (that’s right before WordPress tries to parse the request) and manipulates $_SERVER['REQUEST_URI'] by cleaning out the language tag.
      So if you e.g. call http://www.example.com/en/myurlpath WordPress only “sees” http://www.example.com/myurlpath.
      $_SERVER['REQUEST_URI'] = "/en/myurlpath" before the manipulation.
      $_SERVER['REQUEST_URI'] = "/myurlpath" after the manipulation.

    This way your only “job” is to clean up any urls before WordPress is parsing them.