Software localization

Beyond translation: Building a scalable WordPress i18n workflow

WordPress powers the web, but translating it well takes more than plugins. Discover how to build a scalable localization workflow using gettext, best practices, and the Phrase plugin.

A woman in a light sweater sits in a home office, focused on her laptop, representing a developer or content manager working on WordPress localization tasks in a calm, professional environment.

WordPress localization should be straightforward. After all, this is the CMS behind more than 43% of all websites. But while WordPress offers some built-in support for internationalization (i18n), the reality is that adapting your site for global audiences requires stepping outside of core WordPress functionality.

Whether it’s translating content or handling locale-specific formats like dates, currencies, and layout direction, the i18n WordPress experience is pretty much all about selecting and then working with the right plugin(s).

In this article, we’ll look at how internationalization works in WordPress and some of the plugins that can help you create an effective WordPress i18n workflow.

What is WordPress localization?

WordPress localization is the process of adapting a WordPress site for specific languages, regions, and cultural norms. That includes translating content, adjusting date and number formats, using the right currencies, and supporting the correct text direction.

To do this effectively, your WordPress theme and plugins first need to go through internationalization, meaning the code is structured in a way that supports translation, typically using WordPress’s gettext-based system.

Let’s look at what that means in practice. The first thing to know is that we’re actually dealing with three different things when we talk about WordPress localization:

  • Localizing WordPress itself: This covers the admin dashboard, login screen, and other built-in interface elements. WordPress handles most of this out of the box; you just choose your site language and the core translations kick in.
  • WordPress theme and plugin internationalization: Whether you’re building a custom theme or working with off-the-shelf ones, you’ll want to make sure any text used in your templates or plugins can be translated. That means avoiding hardcoded strings and using gettext (a standard system for handling translation-ready strings in open-source projects) placeholders instead. More on that below.
  • Localizing site content: Posts, pages, menus, widgets and anything else created via the WordPress editor also needs to be translated. This is typically where you’ll use plugins to manage multilingual content without touching code.

If it’s starting to sound complex, then there’s good news and bad news. Localization is inherently a bit involved. You’re juggling content, code, and context across different languages, and WordPress spreads that work across several layers.

So, what about that good news? The whole thing is quite logical, once you understand the flow. And there are solid tools to help, from popular content-focused plugins like TranslatePress and Polylang to more workflow-oriented options like the Phrase WordPress plugin, which can help streamline how translations move between your site and your translation team.

Why go to the trouble of localization?

If it’s involved and complex, then does it make sense to spend development cycles, budget, and your content colleagues’ time on WordPress localization?

The short answer is “Yes” because more than half the web is in a language other than English.

However, perhaps the most obvious reason to invest in WordPress i18n is reach. You’re opening your site up to the roughly 81% of the world’s population who don’t speak English at all. And even if English isn’t your starting point, there’s still work to do because most WordPress themes and plugins ship with English strings by default.

Then there’s SEO. Localized content helps you serve search users in their own language, increasing the chances your content will show up in the right markets and actually get clicked.

So localizing your WordPress site isn’t just about inclusivity or completeness. It’s a business decision and often a growth one.

A technical primer in WordPress localization and internationalization

When it comes to preparing your WordPress themes and plugins, the i18n phase relies on GNU gettext, a system used across many open-source projects to manage translation workflows.

Here’s how it works in WordPress:

  • Any text you want to make translatable needs to be wrapped in PHP functions that mark it for translation.
  • WordPress (or tools like WP-CLI or Poedit) can then extract those strings into a .pot (Portable Object Template) file.
  • Translators use the .pot file to create .po (Portable Object) files for each language.
  • These are then compiled into .mo (Machine Object) files, which WordPress reads at runtime depending on the active locale.

Typically, you won’t need to worry too much about .mo files because WordPress generates them for you. You and your colleagues will spend most of your time working in .pot and .po files. 

And that’s where workflows begin to get complex. Every change to the interface requires a new translation in each of the locales you support, so you’ll need to coordinate between your development team and translators, track which strings are new or modified, and make sure nothing gets lost. This is where the Phrase WordPress plugin can help and we’ll look at that later.

Example: localizing an article details section

So, what does the standard WordPress i18n flow look like in practice? Let’s use a byline box as an example. It tells us who wrote an article, along with other useful information.

In WordPress, our PHP for this byline component might look something like this:

<div class=”post-meta”>

  <div class=”author”>

    <?php

    // __() returns a translatable string.

    // Use it with printf() to insert dynamic content, like the author’s name.

    printf(

      /* translators: %s is the author’s name */

      __(‘Written by %s’, ‘your-theme’),

      esc_html(get_the_author())

    );

    ?>

  </div>

  <div class=”post-details”>

    <time datetime=<?php echo get_the_date(‘Y-m-d’); ?>”>

      <?php echo get_the_date(); ?>

    </time>

    <span class=”separator”></span>

    <?php

    // Translate the label and inject the category list.

    printf(

      /* translators: %s is the category list */

      __(‘Filed under %s’, ‘your-theme’),

      get_the_category_list(‘, ‘)

    );

    ?>

    <span class=”separator”></span>

    <?php

    // _n() handles singular/plural forms based on the count.

    $reading_time = 3;

    printf(

      _n(‘%d min read’, ‘%d mins read’, $reading_time, ‘your-theme’),

      $reading_time

    );

    ?>

    <?php if (current_user_can(‘edit_post’, get_the_ID())) : ?>

      <span class=”separator”></span>

      <?php

      // _e() echoes the translated string directly.

      _e(‘Edit’, ‘your-theme’);

      ?>

    <?php endif; ?>

  </div>

</div>Code language: JavaScript (javascript)

There’s a lot going on here, so let’s pull out the localization-specific elements:

  • __() – Returns a translatable string
    Use this when you need the string as a value. For example, when inserting it into another function like printf(). In the example, we use it for sentences like “Written by %s” or “Filed under %s”, where a variable (author name, category list) needs to be inserted into the translated text.
  • _e() – Echoes a translatable string directly.
    This is a shorthand for echo __(‘…’) and is useful when you just want to print a static string like “Edit” directly to the page, with no formatting or manipulation.
  • _n() – Handles singular and plural forms.
    When you’re displaying text that depends on a number—like “1 min read” vs “3 mins read”—this function lets translators provide both forms, and WordPress will pick the right one based on the count.
  • Translator comments (/* translators: … */)
    These comments don’t appear to users of your site but they are picked up by translation tools when generating .pot files. They explain what placeholders like %s refer to, so translators know how to interpret the sentence.

With that in place, we can give our French readers a byline box like this:

Best practices

Once you’re used to them, the basics are pretty straightforward. To recap, rather than hardwiring strings, you:

  1. Wrap strings in __() or _e()
  2. Generate a .pot file as a template
  3. Ask your translators to return .po files for each locale. 

And with the basics in place, there are a few habits that can either help or hinder your WordPress localization workflow, including:

✅ Interpolate, don’t concatenate

Concatenating translated fragments with dynamic values leads to broken sentences, poor context, and incorrect word order in other languages. Instead, interpolate dynamic values using printf() or similar.

Avoid doing this:

echo __('You have', 'your-theme') . ' ' . $count . ' ' . __('new messages.', 'your-theme');Code language: PHP (php)

And do this instead:

printf(__('You have %d new messages.', 'your-theme'), $count);Code language: PHP (php)

✅ Provide translator comments for interpolated values

If you’re using format specifiers (%s, %d, etc.), include a translator comment so it’s clear what each placeholder represents.

/* translators: %s is the author’s name */

printf(__('Written by %s', 'your-theme'), get_the_author());Code language: JavaScript (javascript)

These comments are extracted into the .pot file and are visible in translation tools. Without them, translators are left guessing.

✅ Use _x() for disambiguation

It’s not unusual for a particular word to have multiple meanings depending on where it appears. “Post” might refer to a blog entry in one place and an action (like “to post a comment”) somewhere else. For English speakers, the difference is clear from context. But translators don’t always see the UI, so the context needs to be made explicit in the code.

That’s what _x() is for. It works just like __(), but with an added argument to provide disambiguating context for translators.

_x('Post', 'noun, as in blog post', 'your-theme');Code language: JavaScript (javascript)

Localizing posts, pages, and more

While theme and plugin internationalization handles your site’s structure and interface, most of your actual content lives in posts, pages, and custom post types. Unlike template text, this content isn’t wrapped in gettext functions. Instead, it is stored directly in the database and needs a different approach.

For content localization, there are three main options:

  • Multilingual plugins: Tools like WPML, Polylang, or TranslatePress create parallel content structures in your database. You write in your primary language, then either translate manually or connect to a translation service. These plugins typically manage permalinks, SEO metadata, and even media attachments on a per-language basis.
  • Multisite approach: For full separation between languages, you can set up a WordPress multisite network with one site per language. This gives you maximum control but comes with the overhead of managing multiple sites, themes, and plugin configurations.
  • Subdirectory structure: Some simpler setups duplicate content manually into language-specific subfolders (e.g. /en/, /fr/). While this avoids plugins, it’s best suited to static sites and introduces maintenance challenges at scale.

Whichever route you choose, content localization introduces new layers of complexity. Will every language have access to the same content, or will some regions need tailored messaging? How will you manage updates? Do you hold back new content until all translations are ready, or publish as each language version becomes available, even if that means some languages fall out of sync?

And who’s responsible for translation: in-house editors, external agencies, or automated tools? Getting clear on these questions early will help you avoid bottlenecks and choose tooling that supports your publishing workflow, not complicates it.

Managing WordPress localization at scale with Phrase

Once you’ve internationalized your theme and plugin strings, as well as setting up a method of localizing your site content itself,  the real work begins: maintaining translations as your site evolves. 

This differs depending on what you’re localizing. For gettext strings, tools like Poedit or Loco Translate could be good enough. You update the .po files, compile them to .mo, and upload them to your server. For site content, how you handle localized versions will depend on the method you choose (plugin, multi-site, or sub-directories).

But once your project spans multiple locales, changes regularly, or involves other people, the cracks in manual processes start to show. It’s easy to overwrite something, lose track of which strings need updating, or end up with out-of-sync translations.

For interface strings, the Phrase WordPress plugin helps automate this without changing how gettext works under the hood. It connects your site to Phrase, so you can sync posts, pages, taxonomies, and custom fields selectively without manually editing or uploading .po files.

Let’s compare the two ways of working.

Workflow stepManual (.po/.mo with Poedit)With Phrase Plugin & TMS
Adding new stringsRe-run extraction, regenerate .pot, manually update .po files for each locale.Syncs automatically or selectively via the plugin.
Updating translationsRequires passing .po files between devs and translators, tracking changes manually.Collaborate in a shared interface with version control and comments.
Spotting missing/duplicate stringsOften requires manual checking or custom scripts.Automatically flagged in the Phrase interface.
Deploying updated translationsCompile .mo files locally, upload them to the server via FTP or Git.Pulls updated translations into your repo or CI/CD pipeline.
Scaling to multiple localesTime-consuming and error-prone as file volume grows.Centralised management across all locales.
Working with non-devsTranslators must understand .po syntax or use separate tools like Poedit.Translators work directly in the Phrase UI without touching code.

If you’re also managing localized content (such as blog posts, landing pages, or site copy) the Phrase plugin handles that as well. You can push and pull translations for posts and pages, see translation status at a glance, and connect Phrase to your preferred multilingual plugin for language switching and URL management.

It’s the same philosophy: work with the grain of WordPress, but make the workflows less manual, less error-prone, and more scalable.

“What slows teams down isn’t the act of translating. After all, localization is as much a part of the site’s lifecycle as content updates or code deploys. What’s harder is not knowing what’s changed, what’s missing, or who owns what. Most WordPress projects don’t fall behind because of language—they fall behind because the translation workflow isn’t treated like part of the release process.”

– Francesca Sorrentino, Director of Localization, Phrase

Don’t forget to localize the behind the scenes stuff, too

Beyond translating text, a few technical elements can make or break the quality of a localized WordPress site:

  • Images, video, and alt text:  Use locale-specific media where needed. Don’t forget to translate or adapt alt text, not just the visible content.
  • Right-to-left (RTL) layout support: Use is_rtl() to detect RTL languages. Ensure your theme supports the dir attribute and adjust layout styles accordingly.
  • Date, time, and number formatting:  Use date_i18n(), get_locale(), or pll_current_language() to adapt formatting based on locale.
  • Locale-aware CSS and asset loading: Conditionally load styles, fonts, or scripts if different locales require different layouts or resources.

These details often go unnoticed, until they’re missing. Handling them well is the difference between localizing and translating your site.

So far, we’ve seen that WordPress localization is technically possible without using plugins but that a combination of tools and techniques will help improve your workflow.

But choosing that combination is tricky if you’re not quite sure of what role each plugin will play. So, here’s a quick overview:

  • Polylang: A flexible, well-established plugin for managing multilingual content. Works well with both manual and semi-automated translation workflows. Pairs nicely with Phrase if you need more structured translation management.
  • WPML: A comprehensive, all-in-one multilingual plugin with strong commercial support. Good for complex sites with lots of custom content types, though it can feel heavy for smaller projects.
  • TranslatePress: Offers a visual, front-end editing interface that makes it easy for editors or marketers to translate pages directly on the site. Great for smaller teams or less technical workflows.
  • Loco Translate: Focused on gettext string translation, right from the WordPress admin. Ideal for translating plugin and theme strings quickly without leaving the dashboard.
  • Phrase: A developer-first platform for managing both gettext and content translations at scale. Especially useful when you need collaboration, translation memory, or integration with version control or CI/CD.

Whichever tools you choose, the key is to match them to your workflow, making sure they support the kind of content you manage, the way your team works, and the level of control you need as your site grows.

Streamlining WordPress i18n

Internationalization works best when it’s planned from the start. From gettext strings to post content and layout quirks, the sooner you set up a structured approach, the easier it is to keep things consistent as your site evolves.

Choose tools that support your workflow—not just technically, but operationally. And localize with intent. The aim isn’t just to translate: it’s to offer a version of your site that feels right for every person using your site.

Boost website reach with a robust WordPress translation plugin

Translate your WordPress website, blog posts, or landing pages into multiple languages with Phrase’s translation plugin for WordPress