I've been trying to achieve a seemingly simple permalink structure customization without success. The goal is to always have /aa-bb/ in front of each possible permalink, where aa is a language code and bb a country code. Whatever comes after this should behave exactly like it would normally. So where normally you'd have /post-title/, afterwards you'd have /en-gb/post-title/ and it would lead to the same post (including all variations such as pages, CPTs, archives, categories, search etc.).

The complete flow:

  1. I generate a list of valid language/country values = complete
  2. I retrieve the value from the URL to determine the language/country = complete
  3. During the template_redirect action hook I redirect to the default language/country if no valid value is present = complete
  4. Set up a permalink rewrite structure to process these URLs as if the first part didn't exist = mystery

All methods for rewriting WP permalinks seem to do something slightly different from this goal. Is this something that would be better done with htaccess? WPML seems to do it successfully with their language codes but I'm not sure how.

Using add_rewrite_rule sure works, but only covers one permalink type at a time so I'm afraid of falling short if I'd manually add all variations.

2 Answers 2


I'm working on a similar solution right now - the website must have language codes in all URLs (except the default language), but only pages are translatable in a way WPML/Polylang plugins do it. For news (blog) we just show posts in particular language (they are separate, not translations of each other). All the other content is mixed in all languages. Also, the UI is displayed in a language set from URL too.

Here's what I did to get those rewrite rules prepended with language codes:

function prepend_default_rewrite_rules( $rules ) {

    // Prepare for new rules
    $new_rules = [];

    // Set up languages, except default one
    $language_slugs = ['ar', 'ku'];

    // Generate language slug regex
    $languages_slug = '(?:' . implode( '/|', $language_slugs ) . '/)?';

    // Set up the list of rules that don't need to be prefixed
    $whitelist = [

    // Set up the new rule for home page
    $new_rules['(?:' . implode( '/|', $language_slugs ) . ')/?$'] = 'index.php';

    // Loop through old rules and modify them
    foreach ( $rules as $key => $rule ) {

        // Re-add those whitelisted rules without modification
        if ( in_array( $key, $whitelist ) ) {

            $new_rules[ $key ] = $rule;

        // Update rules starting with ^ symbol
        } elseif ( substr( $key, 0, 1 ) === '^' ) { 

            $new_rules[ $languages_slug . substr( $key, 1 ) ] = $rule;

        // Update other rules
        } else {

            $new_rules[ $languages_slug . $key ] = $rule;


    // Return out new rules
    return $new_rules;
add_filter( 'rewrite_rules_array', 'prepend_default_rewrite_rules' );

Haven't tested it to full extent - just came up with the solution after studying the WP_Rewrite class. So it's a work in progress. Hope it helps though.

P.S.: Also, I would be happy to see your solution for "I retrieve the value from the URL to determine the language/country = complete" - that's what I'm currently working on :)

  • 1
    Nice! So you are filtering all existing rewrites and adding the language-code in front of it? The way I'm getting the lang/country code from the URL is simply this: $url_langregion = explode( '/', $_SERVER['REQUEST_URI'] )[1];
    – Berend
    Commented Jun 27, 2017 at 11:17
  • Very nice, indeed! :) I just found one error in the rule for the home page. This would be correct: $new_rules['(?:' . implode( '|', $language_slugs ) . ')/?$'] = 'index.php';. basically, implode the $language_slugs with only |, not /|.
    – rassoh
    Commented Mar 28, 2019 at 10:57

I believe that you could use polylang plugin. I made one of my project like you mentioned with that plugin.

  • I'm already using WPML so adding a translation plugin is not an option.
    – Berend
    Commented Jun 27, 2017 at 11:20

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.