Software localization

Creating a WordPress Multilingual Site: Beginning Custom Theme Localization with gettext

Find out how to make a WordPress custom theme adaptable to multiple locales and localize it by learning the gettext system along the way.
Software localization blog category featured image | Phrase

Let's talk WordPress theme development and i18n. We'll start building out our custom theme for our clients, Najla and Taha. In part 1 we built a theme prototype of a catalog website for their North African handmade crafts business, Handmade's Tale. Our clients liked what they saw but felt that the site looked a bit too generic for their taste. They wanted a simple, custom theme to better reflect their brand, and they wanted to make sure that things still work in the languages their international customers read.

Note » You don't need to have read part 1 to follow along with this one. However, if you want to learn how to localize WordPress content like pages, posts, and contact forms, part 1 is a good read.

So in this article, we'll cover the following aspects of building a localized, custom WordPress theme.

  • HTML language attributes
  • Displaying the localized site name & tagline
  • Understanding the gettext i18n system
  • The WordPress PHP localization functions

Note » I assume you have basic knowledge of WordPress theme coding, including file structure, PHP, basic WordPress functions, and HTML.

We'll go into gettext in depth and list the majority of the WordPress localization PHP functions. So we won't be writing a lot of code in this part, but we'll build a solid understanding of WordPress i18n, which is necessary to get started with WordPress theme localization. It also sets us up to flesh our theme out more in the next part of this series.

Our (Basic) Design

For now, we just want to set up the foundations of our theme, so our design will be quite simple.

Demo app design | Phrase

WordPress and Plugins

Let’s get started!

Note » WordPress and some plugins have been updated in the meantime. The updates are minor releases and point releases and shouldn't have any effect on our work in this series to date. However, do note the new version numbers if you're building along with us.

  • WordPress (5.0.3)
  • Plugins
    • Polylang (2.5.1) ~ handles content & URL localization
    • Loco Translate (2.2.0) ~ facilitates the translation of theme and plugin strings
    • Contact Form 7 (5.1.1) ~ makes it easy to build contact forms
    • CF7 Smart Grid Design Extension (2.7.1) ~ gives us more flexibility over CF7 forms from within the WordPress admin console (however, we’re really just installing it here since it’s a requirement of the Contact Form 7 Polylang extension)
    • Contact Form 7 Polylang extension (2.3.0) ~ allows us to use Polylang to localize CF7 forms

Note » We'll also use the WordPress CLI (2.1.0) in this part in order to work with translation files. This will be optional, but we thought we'd let you know in case you want to install everything at once.

Polylang

The Polylang plugin is handling a lot of the i18n work for us and therefore deserves special mention. We'll be relying on the localization features that Polylang gives us as we build our theme. If you're not familiar with the basics of Polylang, you may want to check out part 1 of this series where we cover Polylang usage in detail.

File Organization

We'll have a basic WordPress theme directory structure. Most of the following should be familiar to you. If not, come along for the ride anyway, and check out this article's companion GitHub repo to see how the files will start working together.

/

├── wp-content/

|   ├── themes/

|   |   ├── handmadestale/

|   |   |   ├── classes/

|   |   |   |   ├── HandmadesTale_Language_Switcher_Widget.php

|   |   |   |   └── HandmadesTale_Walker_Nav_Menu.php

|   |   |   ├── img/

|   |   |   |   └── logo.png

|   |   |   ├── footer.php

|   |   |   ├── functions.php

|   |   |   ├── header.php

|   |   |   ├── index.php

|   |   |   ├── language-switcher.php

|   |   |   ├── loop.php

|   |   |   ├── page.php

|   |   |   ├── single.php

|   |   |   ├── style.css

|   |   |   └── stylesheet-links.php

|   |   └── ...

|   └── ...

└── ...

Note » We'll flesh out much of the above files in the next part in this series. We'll lay the foundations to build on in this article.

Scaffolding

We'll start with the bare minimum we need for a WordPress theme: a style.css file and an index.php file. As is customary, we'll break up the reusable header

and footer of our index into header.php and footer.php files respectively.

/*!

 * Theme Name: Handmade's Tale

 * Description: Custom theme for Handmade's Tale website

 * Version: 1.0.0

 * Author: Mohammad Ashour

 * Author URI: http://www.ashour.ca

 *

 * License: MIT

 * License URI: http://opensource.org/licenses/mit-license.php

 */

You'll want to put in your own author info in style.css of course.

<?php get_template_part('header'); ?>

<main role="main" aria-label="Content">

    <!-- section -->

    <section>

        <!-- Content will go here -->

    </section>

    <!-- /section -->

</main>

<?php get_template_part('footer'); ?>

So far we have a pretty straight-forward index.php file. Now let's start at the top and take a look at our included header.

<!doctype html>

<html <?php language_attributes(); ?>>

    <head>

        <meta charset="<?php bloginfo('charset'); ?>">

        <title>

            <?php wp_title(''); ?>

            <?php if (wp_title('', false)) {

                echo ' | ';

            } ?>

            <?php bloginfo('name'); ?>

        </title>

        <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">

        <meta name="viewport" content="width=device-width, initial-scale=1.0">

        <meta name="description" content="<?php bloginfo('description'); ?>">

        <?php wp_head(); ?>

    </head>

    <body <?php body_class(); ?>>

        <!-- wrapper -->

        <div class="wrapper">

            <!-- header -->

            <header class="header clear" role="banner">

                <h1 class="site-heading">

                    <a

                        class="site-heading__link"

                        href="<?php echo home_url(); ?>"

                    >

                        <img

                            class="site-heading__logo"

                            src="<?php echo get_bloginfo('template_url') ?>/img/logo.png"

                        />

                        <?php bloginfo('name'); ?>

                    </a>

                </h1>

                <!-- nav -->

                <nav class="nav pure-g" role="navigation">

                    <div class="pure-u-2-3">

                        <?php wp_nav_menu(array(

                            'theme_location' => 'main-menu',

                            'container_class' => 'menu-main-menu-container pure-menu pure-menu-horizontal',

                            'items_wrap' => '<ul id="%1$s" class="pure-menu-list %2$s">%3$s</ul>',

                            'walker' => new HandmadesTale_Walker_Nav_Menu,

                        )); ?>

                    </div>

                </nav>

                <!-- /nav -->

            </header>

There shouldn't be anything too unfamiliar to you in this header.php. However, there are some localization considerations here, so let's take a look at them.

Note » We'll get to the included stylesheet-links partial in an upcoming part in this series.

HTML Language Attributes

In the second line of header.php we call WordPress' language_attributes() function.

<html <?php language_attributes(); ?>>

This function will basically echo out the following.

dir="rtl" lang="ar"

When we add these attributes to our opening <html> tag, we let the browser know the language of the content and its direction. Most modern browsers will respect these attributes and attempt to do their best to render the page with respect to our given localization.

Displaying the Localized Site Name & Description (Tagline)

In header.php above we use the bloginfo() function supplied by WordPress to echo out our site's character set, name, and description in our <meta> and <title> tags.

<meta name="description" content="<?php bloginfo('description'); ?>">

By using bloginfo() instead of hard-coding these values, we tap into Polylang's translated site name and description ("tagline"), which we translated in part 1. This means bloginfo() will echo out our Arabic site name and description when the current locale is Arabic, for example.

Note » As a reminder, the name and description ("tagline") were localized in Polylang's Languages > String Translations section of the WordPress admin.

Getting the Localized Home Page URL

We can simply use WordPress' home_url() function to get the home page URL for the current locale: WordPress and Polylang will take care of the details for us.

Note » The localized URLs produced by home_url() will depend on the specific configuration you set in Polylang's Languages > Settings section of the WordPress admin.

If we wanted to explicitly specify the locale of the home page URL, we could use Polylang's pll_home_url() function, which takes a language code parameter. So the following will always return the French version of the site's home page URL, regardless of the current locale.

<?php echo pll_home_url('fr'); ?>

Note » If you're coding along: the navigation menu walker in header.php is

simply for > display purposes. Covering the menu code is a bit outside the scope

of this article. However, you can peruse the menu walker code on Github. Do

note, that you'll want to re-set the menu in the Appearance > Menu section of

the WordPress admin after you enable your custom theme.

Understanding gettext

Let's take a quick look at our footer.php file to round out our scaffolding.

 <!-- footer -->

            <footer class="footer" role="contentinfo">

                <!-- copyright -->

                <p class="footer__copyright">

                    &copy;

                    <?php echo esc_html(date('Y')); ?>

                    Copyright <?php bloginfo('name'); ?>.

                    <?php echo __('Powered by', 'handmadestale'); ?>

                    <a href="//wordpress.org" target="_blank">

                        <?php echo __('WordPress'); ?>

                    </a>.

                </p>

                <!-- /copyright -->

            </footer>

            <!-- /footer -->

        </div>

        <!-- /wrapper -->

        <?php wp_footer(); ?>

    </body>

</html>

Notice the __() function here.

<?php echo __('Powered by', 'handmadestale'); ?>

Instead of hard-coding the string 'Powered by' in our theme, we wrap it in __(). This function is part of a family of WordPress PHP functions that we use to register and display translatable strings. We need to use one of these functions whenever we have a string in our theme that could be localized. WordPress, and the i18n/l10n functions it provides, use a common localization technology called gettext. Wand we should have a basic understanding of gettext to work with WordPress theme development localization.

Note » Don't confuse WordPress' __() with PHP's built-in _(). When developing WordPress themes and plugins, you'll want to use the WordPress localization functions.

Gettext is an i18n system that works in a series of steps:

  1. Our code files are scanned by a program that looks for gettext function calls. When developing for WordPress, these would be the __() function and its ilk (see below for a more complete list of functions).
  2. The program generates a POT (Portable Object Template) file, which lists strings registered in gettext calls and serves as a master template for localizations.
  3. Each supported locale gets its own PO (Portable Object) file derived from the POT master template, e.g. ar.po would be the Arabic PO file.
  4. Each PO file is populated by translators who add a localized version of each string in the file (although a PO file does not to be translated 100% for the system to work).
  5. Translated PO files are then compiled, each to an MO (Machine Object) file, e.g. ar.mo would be the compiled result of ar.po. MO files are optimized for performant runtime usage.
  6. At runtime the environment locale is set. The gettext system loads the MO file corresponding to the environment locale, and returns its translated strings when a gettext function is called.

Note » At runtime, if no file exists for the active locale, or if no translation exists for a give string, the function will return the string it was passed as-is, e.g. __('Powered by') will return 'Powered by' if no translation is found for the string in the active locale.

Using gettext with Our Theme

Many major programming languages have built-in gettext support. PHP, which WordPress is built with of course, is no exception. However, WordPress seems to largely use its own from-scratch gettext implementation and provides us with PHP functions that tap into its gettext system. Let's take a look at how to work with POT, PO, and MO files in WordPress. We'll look at WordPress' i18n PHP functions a bit later.

Generating Our Theme's POT File

Using the WordPress CLI

The first thing we have to do is create a POT file for our theme. We can do this with the WordPress Command Line Interface (CLI). Assuming we have the WordPress CLI installed, we can run the following command from our within our theme's root directory in the terminal to generate a POT file.

$ wp i18n make-pot .

This command will scan the current (theme root) director and generate a POT file with the same name as our theme's directory.

Using the Loco Translate Plugin

We can also use the Loco Translate plugin we installed in part 1 to generate the POT file. After we navigate to Loco Translate > Home in the admin panel we should see our theme's name appear under the Active Theme section of the screen.

Active Themes menu | Phrase

Once we click the theme's name, we should get our theme's home screen on Loco Translate. If we haven't created a POT file for our theme before, a Create template button should be present.

Create template | Phrase

Clicking the Create template button will take us to the New translations template creation screen. All we have to do here is click the new Create template button and Loco Translate will create our theme's POT file.

Creating POT file | Phrase

Note » You can use Poedit to work with POT, PO, and MO files if you prefer a desktop app with a user interface or if you have any issues with the other listed methods.

Translating: Generating our PO & MO Files with Loco Translate

The Loco Translate plugin allows for PO and MO file creation and has even a built-in UI for translators to update a locale's PO files. To create a PO file, we first must have a POT file (we created one using Loco Translate above). Once that is in place, we can navigate to Loco Translate > Home and select our theme. We can then click on the New language button. This will take us to the Initializing new translations screen.

Adding new language | Phrase

First we need to select the language we want to localize for and choose the location for the PO file. As we're authoring the theme, we can select the Author option for the file location which will store the file under our theme directory. This will make it easier to install the theme along with its localizations should we want to do so in the future. When we're ready to create the PO file we can click Start translating. We can repeat this process for any locale we want to create a PO file for.

We can now begin translating and saving our MO file within Loco translate. The plugin makes this quite simple. We simply open the screen for the locale we want to translate by navigating to Loco Translate > [Theme Name] > [Locale]. We then select the Source text or string we want to translate and provide its translation in the screen's lower box. After we click the Save button our PO and MO files for the respective locale will be updated, and our translation will appear on our site where we expect it.

Adding and saving Arabic translation | Phrase

Note » Since we're authoring the theme, we can safely ignore the notice on the the translation screen about our files being overridden. This warning applies to the case of us saving our PO and MO files in someone else's theme or plugin. In that case our own locale files could get overridden when we update the respective theme or plugin if it has its own translation files.

Loading Our Current Locale's Translations (or Loading Our Theme's Text Domain)

In order for WordPress to pick up our translation files, we must load them in. This step is easy to forget and we won't see any translated text if we don't do it. We just need to add the following line in our functions.php file.

load_theme_textdomain('handmadestale');

This function will load the MO file (if it finds it) of the current/active locale, e.g. if Arabic is the active locale it will try to load an ar.mo file for our theme.

WordPress PHP Localization Functions

We now have a handle on the WordPress gettext translation process. Let's dive deeper into the PHP functions that WordPress provides to tap into its gettext system. We use these functions regularly to internationalize our theme templates.

By the way, __() is not the only WordPress i18n function. There are several

for us to use. These functions do two things:

  1. Register strings for translation with the gettext system, making them available in our POT file when we create it
  2. Output the translated string for the current locale (found in the locale's MO file), falling back on the given string if no translated string is found

Let's reexamine the __() function.

<?php echo __('Created by', 'handmadestale'); ?>

What the above code will do is look for a string in the current locale with the key 'Created by'. If the current locale was French, for example, we may have a translation of 'Created by' that reads 'Créé par'. If __() finds that translation, it will return it. Otherwise, it will return the string we fed it, 'Created by', as a fallback.

The Text Domain

What about the second parameter to functions like esc_html_e() and __()? In the above examples, we passed in the value 'handmadestale' for that param. This is known as the text domain, and we can think of it as a namespace of our translatable strings. There may be more than one instance of a string that is registered in different parts of our website. For example, 'Date posted' could be registered both in a third-party plugin and in our own theme. To differentiate between the two we use a text domain.

Using a text domain also comes in handy when we translate, as we can isolate our own theme's strings for translation, for example. And while the text domain parameter is optional in WordPress' localization functions—if we omit it a default text domain is used—it is probably best to always provide a text domain param. The default domain is reserved for WordPress core.

The Functions

Let's run down the available WordPress localization functions at the time of writing.

Important Do not pass in variables for the $text or $domain parameters in WordPress i18n functions. The gettext system that generates translation files will analyze your files statically, not at runtime. So it will look for literal strings, e.g. __('Hello world', 'myawesomedomain'). The following just won't work: __($foo, $bar).

Regular Localization Functions

__(string $text, string $domain = 'default'): string — Retrieves the translated text

Note » You may know that the __() function is an alias for the translate() function. However, do not use translate() directly in your code because translate() won't be picked up during gettext analysis and translation file generation.

_e(string $text, string $domain = 'default'): void — Displays the translated text

Note » The e functions basically do the same thing as their non-e equivalents, except that they echo the result instead of returning it.

_n(string $single, string $plural, int $number, string $domain = 'default'): string — Returns the translated single or plural string depending on the given number (we'll go into greater detail regarding translation in upcoming articles in this series)

Escaping for Attributes Localization Functions

esc_attr__(string $text, string $domain = 'default'): string — Returns the translated text after escaping it for safe use in an HTML attribute

esc_attr_e(string $text, string $domain = 'default'): void — Displays the translated text after escaping it for safe use in an HTML attribute

Escaping for HTML (Outside of Attributes) Localization Functions

esc_html__(string $text, string $domain = 'default'): string — Returns the translated text after escaping it for safe use within HTML

esc_html_e(string $text, string $domain = 'default'): void — Displays the translated text after escaping it for safe use within HTML

Note » You can peruse all the above functions on the official WordPress Codex.

Note » You can get all of the code in this article from its companion Github repo.

In the upcoming parts of this series, we'll make use of these functions and explore the concept of context as it relates to WordPress gettext functions. You read about the context in the official WordPress docs, so we wanted you to know that we got you covered. We just can't fit it all in one article is all.

Check out the Official Docs

There is a lot of good information for localizing WordPress themes in the official document, I18n for WordPress Developers. It's definitely recommended reading and has some good best practices.

Til Next Time

Armed with the knowledge of gettext and WordPress' PHP i18n functions, you should be able to localize most of your WordPress theme's templates. If you’re writing your own custom themes for WordPress, consider using Phrase to translate them. Phrase works with POT, PO, and MO files out of the box, and provides a pro feature set for i18n developers and translators. Phrase can sync to your Github repo to detect when locale files change. It also provides tools for searching for translation strings and proofreading your translations. Phrase even includes collaboration tools so that you can save time as you work with your translators. Check out Phrase’s full feature set, and try it for free for 14 days. You can sign up for a full subscription or cancel at any time.

We're still in the middle of building out our theme, so we don't have anything to show our clients yet. Stay tuned for the next part of this series, where we'll flesh out more of our theme and get into more nuanced WordPress i18n. Happy coding 😊