Skip to content

Bilingual WordPress Without a Plugin: Building the /en Prefix With Rewrite Rules

by Lucas Almeida 5 min read

DigitalRadar has been bilingual from the start, but until October the language came from a URL parameter: ?lang=en, stored in a cookie. It worked for people browsing — and it did not work for Google. Crawlers do not carry cookies. To a search engine there was one URL with two possible contents, which is the definition of duplicate content.

The fix was putting the English site under an /en prefix, without a translation plugin. This post is the path I took — including the approach that looks obvious and breaks the site.

What the prefix has to deliver

/                   → PT home       /en/                 → EN home
/post-slug/         → PT post       /en/english-slug/    → EN post
/category/news/     → PT archive    /en/category/news/   → EN archive

With three non-negotiable conditions: pagination working on both sides, canonical pointing at the right URL, and old URLs redirecting without losing what is already indexed.

The approach that breaks (and is the most common)

The natural shortcut is to intercept the request early, strip /en from REQUEST_URI and let WordPress process the rest normally:

// DO NOT do this
$_SERVER['REQUEST_URI'] = preg_replace('#^/en#', '', $_SERVER['REQUEST_URI']);

It works for the first five minutes and then breaks in three places:

  • Pagination. paginate_links() builds its links from REQUEST_URI. Modify it and page 2 of an English archive points at the unprefixed URL.
  • $wp->request. Many themes and plugins use this property to build the canonical. Without the prefix, the English page’s canonical points at the Portuguese one.
  • redirect_canonical. WordPress compares the requested URL with the one it considers correct and, finding a mismatch, redirects — stripping exactly your prefix.

The approach that works: duplicate the rules

Instead of touching the incoming URL, I teach WordPress to understand prefixed URLs. Every rewrite rule gets a prefixed copy that injects a language query var:

function dr_rewrite_lang_rules( $rules ) {
    $new = array();
    foreach ( $rules as $regex => $query ) {
        $prefixed = 'en/' . ltrim( $regex, '^' );
        if ( 0 === strpos( $query, 'index.php?' ) ) {
            $new[ $prefixed ] = 'index.php?dr_lang=en&'
                . substr( $query, strlen( 'index.php?' ) );
        }
    }
    $new['en/?$'] = 'index.php?dr_lang=en';   // language home
    return $new + $rules;                      // prefixed ones first
}
add_filter( 'rewrite_rules_array', 'dr_rewrite_lang_rules' );

Three details make this work:

  1. No new capture groups. Only the prefix changes in the regex, so the $matches[1], $matches[2] references in the queries stay aligned.
  2. Internal order is preserved. The foreach walks the rules in their original order, so the hierarchy WordPress built still holds inside the prefixed block.
  3. $new + $rules. PHP array union keeps the first array’s keys, placing the prefixed — more specific — rules ahead of the originals.

The language home rule (en/?$) goes after the loop so it lands last among the prefixed ones: it is the most generic and must not swallow /en/anything.

Links: what follows the content and what follows the request

This distinction is what prevents broken links. A post’s permalink follows the content’s language, not the visitor’s — an English post carries /en even while you browse in Portuguese:

add_filter( 'post_link', function ( $url, $post ) {
    return 'en' === dr_post_lang( $post ) ? dr_prefix_url( $url ) : $url;
}, 10, 2 );

Category and tag archives, by contrast, exist in both languages on the same route, so they follow the request’s language. The same goes for home_url() — carefully, because REST, login and feeds call it too. My filter excludes wp-json, wp-admin, wp-login, feeds, anything with a file extension and — a detail that took me a while to catch — URLs containing a fragment, so the JSON-LD @id values stay identical across languages.

The 301s, which is where traffic gets lost

Three redirects guarantee one URL per piece of content:

  • ?lang=en (the old scheme) → the clean /en URL;
  • an English post opened without the prefix → /en/english-slug/;
  • a Portuguese post opened under /en → the clean URL.

One trap here: if your home page is a static page marked as the default language, the second rule sends /en/ back to / and the English home disappears. The guard is simple and mandatory:

if ( is_singular() && ! is_front_page() ) { /* ... */ }

And to stop WordPress’s redirect_canonical from dropping the prefix, I cancel the redirect whenever it tries to strip /en.

What the prefix fixed for free

Two gains I had not planned. Caching got simpler: previously the server cache had to vary each copy by a language cookie; now each language has its own URL and caching works on its own. And hreflang on the home page and archives became automatic — the same route with and without /en is the pair, with no manual link between posts.

Frequently asked questions

Can you run a bilingual WordPress site without a plugin?

Yes, if you accept one post per language linked by a custom field. The work is in URLs, hreflang and filtering the listings — not in translation itself.

Why not rewrite REQUEST_URI?

Because pagination, $wp->request and WordPress’s canonical are all built from it. Changing it makes those three point at the unprefixed URL.

Do I need to flush rewrite rules after changing them?

Yes. The rules are stored in the database. Either open Settings → Permalinks and save, or call flush_rewrite_rules() once, gated by a version constant.

Can the English post slug match the Portuguese one?

It should not. The prefix separates the routes, but a proper English slug is what makes the page rank for English queries — which is the reason the translation exists.

At DigitalRadar, the bilingual engine is our own code and we explain how it works. Stay on the radar.

Lucas Almeida
DigitalRadar Newsroom

Desenvolvedor e fundador da Elion Estúdio, Lucas Almeida é especialista em tecnologias emergentes, games e análise de mercado. Com experiência em SEO e growth marketing, foca em conteúdo aprofundado que conecta o universo da tecnologia com o dia a dia das pessoas.

Reported from the sources cited and verified before publishing. See our Editorial Policy.

Leave a comment

Your email address will not be published. Required fields are marked *