Building Web Apps with WordPress (2014)

Chapter 14. Localizing WordPress Apps

Localization (or internationalization) is the process of translating your app for use in different locales and languages. This chapter will go over the tools and methods available to WordPress developers to localize their apps, themes, and plugins.

NOTE

You will sometimes see localization abbreviated as l10n and internationalization sometimes abbreviated as i18n.

Do You Even Need to Localize Your App?

The market for web apps is increasingly global. Offering your app in other languages can be a strong competitive advantage to help you gain market share against competition within your own locale/language and will also help to stave off competition in other locales/languages.

If you plan to release any of your code under an open source license, localizing it first is a good way to increase the number of developers who can get involved in your project. If your plugin or theme is localized, developers speaking other languages will be more likely to contribute to your project directly instead of forking it to get it working in their language.

If you plan to distribute a commercial plugin or theme, localizing your code increases your number of potential customers.

If your target market is the United States only and you don’t have any immediate plans to expand into other regions or languages, then you may not want to spend the time preparing your code to be localized. Also, remember that each language or regional version of your app will likely require its own hosting, support, customer service, and maintenance. For many businesses, this will be too high a cost to take on in the early days of an application. On the other hand, you will find that the basics of preparing your code for localization (wrapping string output in a _(), _e(), or _x() function) is simple to do and often has other uses outside of localization.

Finally, it’s important to note that sometimes localization means more than just translating your code. If your code interfaces with other services, you will need to make sure that those services work in different regions or be prepared to develop alternatives. For example, an important component of the Paid Memberships Pro plugin is integration with payment gateways. Before localizing Paid Memberships Pro, Jason made sure that the plugin integrated well with international payment gateways. Otherwise, people would have been able to use Paid Memberships Pro in their language, but it wouldn’t have worked with a viable payment gateway for their region.

How Localization Is Done in WordPress

WordPress uses the gettext translation system developed for the GNU translation project. The gettext system inside of WordPress includes the following components:

§  A way to define a locale/language

§  A way to translate strings in your code

§  .pot files containing all of the words and phrases to be translated

§  .po files for each language containing the translations

§  .mo files for each language containing a compiled version of the .po translations

Each of these components must be in place for your translations to work. The following sections explain each step in detail. At the end, you should have all of the tools needed to create a localized plugin and translated locale files.

Defining Your Locale in WordPress

To define your locale in WordPress, simply set the WPLANG constant in your wp-config.php files:

<?php

// use the Spanish/Spain locale and language files.

define('WPLANG', 'es_ES');

?>

NOTE

The term “locale” is used instead of language because you can have multiple translations for the same language. For example, British English is different from United States English. And Mexican Spanish is different from Spanish Spanish.

Prepping Your Strings with Translation Functions

The first step in localizing your code is to make sure that every displayed string is wrapped in one of the translation functions provided by WordPress. They all work pretty much the same way: some default text is passed into the function along with a domain and/or some other information to let translators know what context to use when translating the text.

We’ll go over the most useful functions in detail.

__($text, $domain = “default”)

This function expects two parameters: the $text to be translated and the $domain for your plugin or theme. It will return the translated text based on the domain and the language set in wp-config.php.

The $domain is a string set by you with the load_theme_textdomain() or load_plugin_textdomain() function (explained later). For example, the domain for our SchoolPress app is “schoolpress.” The domain for the Paid Memberships Pro plugin is “pmpro.” You can use anything as long as it is unique and consistent.

NOTE

The __() function is really an alias for the translate() function used in the background by WordPress. There’s no real reason you couldn’t call translate() directly, but __() is shorter and you’ll be using this function a lot.

Here is an example of how you would wrap some strings using the __() function:

<?php

// setting a variable to a string without localization

$title = 'Assignments';

// setting a variable to a string with localization

$title = __( 'Assignments', 'schoolpress' );

?>

_e($text, $domain = “default”)

This function expects two parameters: the $text to be translated and the $domain for your plugin or theme. It will echo the translated text based on the domain and the language set in wp-config.php.

This function is identical to the __() function except that it echoes the output to the screen instead of returning it.

Here is an example of how you would wrap some strings using the _e() function:

<?php

// echoing a var without localization

?>

<h2><?php echo $title; ?></h2>

<?php

// echoing a var with localization

?>

<h2><?php _e( $title, 'schoolpress' ); ?></h2>

In practice, you will use the __() function when setting a variable and the _e() function when echoing a variable.

_x($text, $context, $domain = “default”)

This function expects three parameters: the $text to be translated, a $context to use during translation, and the $domain for your plugin or theme. It will return the translated text based on the context, the domain, and the language set in wp-config.php.

The _x() function acts the same as the __() but gives you an extra $context parameter to help the translators figure out how to translate your text. This is required if your code uses the same word or phrase in multiple locations, which might require different translations.

For example, the word “title” in English can refer both to the title of a book and also a person’s title, like Mr. or Mrs. In other languages, different words might be used in each context. You can differentiate between each context using the _x() function.

In the following slightly convoluted example, we are setting a couple of variables to use on a class creation screen in SchoolPress:

<?php

$class_title_field_label = _x( 'Title', 'class title', 'schoolpress' );

$class_professor_title_field_label = _x( 'Title', 'name prefix', 'schoolpress' );

?>

<h3>Class Description</h3>

<label><?php echo $class_title_field_label; ?></label>

<input type="text" name="title" value="" />

<h3>Professor</h3>

<label><?php echo $class_professor_title_field_label; ?></label>

<input type="text" name="professor_title" value="" />

NOTE

The _x() and _ex() functions are sometimes referred to as “*ex*plain” functions because you use the context parameter to further explain how the text should be translated.

_ex($title, $context, $domain = “default”)

The _ex() function works the same as the _x() function but echoes the translated text instead of returning it.

Escaping and Translating at the Same Time

In Chapter 7, we talked about the importance of escaping strings that are displayed within HTML attributes or in other sensitive areas. When also translating these strings, instead of calling two functions, WordPress offers a few functions to combine two functions into one. These functions work exactly as you would expect them to by first translating and then escaping the text:

§  esc_attr__()

§  esc_attr_e()

§  esc_attr_x()

§  esc_html__()

§  esc_html_e()

§  esc_html_x()

Creating and Loading Translation Files

Once your code is marked up to use the translation functions, you’ll need to generate a .pot file for translators to use to translate your app. The .pot file will include a section like the following for each string that shows up in your code:

#: schoolpress.php:108

#: schoolpress.php:188

#: pages/courses.php:10

msgid "School"

msgstr ""

The preceding section says that on lines 108 and 188 of schoolpress.php and line 10 of pages/courses.php, the word “School” is used.

To create a Spanish-language translation of your plugin, you would then copy the schoolpress.pot file to schoolpress-es_ES.po and fill in the msgstr for each phrase. It would look like:

#: schoolpress.php:108

#: schoolpress.php:188

#: pages/courses.php:10

msgid "School"

msgstr "Escuela"

Those .po files must then be compiled into the .mo format, which is optimized for processing the translations.

For large plugins and apps, it is impractical to locate the line numbers for each string by hand and keep that up to date every time you update the plugin. In the next section, we’ll walk you through using the xgettext command-line tool for Linux to generate your .pot file and the msgfmtcommand-line tool to compile .po files into .mo files. Alternatively, the free program Poedit has a nice GUI to scan code and generate .pot.po, and .mo files and is available for Windows, Max OS X, and Linux.

Our File Structure for Localization

Before getting into the specifics of how to generate these files, let’s go over how we typically store these files in our plugins.

For our SchoolPress app, we’ll store the localization files in a folder called languages inside of the main app plugin. Each language will also have a directory to store other language-specific assets. We’ll add all of our localization code, including the call to load_plugin_textdomain(), in a file in the includes directory called localization.php. So our file structure looks something like this:

1.    ../plugins/schoolpress/schoolpress.php (includes localization.php)

2.    ../plugins/schoolpress/includes/localization.php (loads text domain and other localization functions)

3.    ../plugins/schoolpress/languages/schoolpress.pot (list of strings to translate)

4.    ../plugins/schoolpress/languages/schoolpress.po (default/English translations)

5.    ../plugins/schoolpress/languages/schoolpress.mo (compiled default/English translations)

6.    ../plugins/schoolpress/languages/en_US/ (folder for English/US language assets)

7.    ../plugins/schoolpress/languages/schoolpress-es_ES.po (Spanish/Spain translations)

8.    ../plugins/schoolpress/languages/schoolpress-es_ES.mo (compiled Spanish/Spain translations)

9.    ../plugins/schoolpress/languages/es_ES/ (folder for Spanish/Spain language assets)

When building a larger app with multiple custom plugins and a custom theme, localization is easier to manage if you localize each individual plugin and theme separately instead of trying to build one translation file to work across everything. If your plugins are only going to be used for this one project, they can probably be built as includes or module .php files in your main app plugin. If the plugins are something that you might use on another project, then they should be localized separately so the localization files can be ported along with the plugin.

Generating a .pot File

We’ll use the xgettext tool, which is installed on most Linux systems,[25] to generate a .pot file for our plugin.

To generate a .pot file for our SchoolPress app, we would open up the command line and cd to the main app plugin directory at wp-content/plugins/schoolpress. Then execute the following command:

xgettext -o languages/schoolpress.pot \

--default-domain=schoolpress \

--language=PHP \

--keyword=_ \

--keyword=__ \

--keyword=_e \

--keyword=_ex \

--keyword=_x \

--keyword=_n \

--sort-by-file \

--copyright-holder="SchoolPress" \

--package-name=schoolpress \

--package-version=1.0 \

--msgid-bugs-address="info@schoolpress.me" \

--directory=. \

$(find . -name "*.php")

Let’s break this down.

-o languages/schoolpress.pot

Defines where the output file will go.

--default-domain=schoolpress

Defines the text domain as schoolpress.

--language=PHP

Tells xgettext that we are using PHP.

--keyword=…

Sets xgettext up to retrieve any string used within these functions. Be sure to include a similar parameter for any of the other translation functions (like esc_attr__) you might be using.

--sort-by-file

Helps organize the output by file when possible.

--copyright-holder="SchoolPress”

Sets the copyright holder stated in the header of the .pot file. This should be whatever person or organization owns the copyright to the application, plugin, or theme being built.

NOTE

From the GNU.org website:

Translators are expected to transfer or disclaim the copyright for their translations, so that package maintainers can distribute them without legal risk. If [the copyright holder value] is empty, the output files are marked as being in the public domain; in this case, the translators are expected to disclaim their copyright, again so that package maintainers can distribute them without legal risk.

--package-name=schoolpress

Sets the package name stated in the header of the .pot file. This is typically the same as the domain.

--package-version=1.0

Sets the package version stated in the header of the .pot file. This should be updated with every release version of your app, plugin, or theme.

−−msgid-bugs-address="info@schoolpress.me”

Sets the email stated in the header of the .pot file to use to report any bugs in the .pot file.

--directory=.

Tells xgettext to start scannging from the current directory.

$(find . -name “*.php”)

This appears at the end, and is a Linux command to find all .php files under the current directory.

Creating a .po File

Again, the Poedit tool has a nice graphical interface for generating .po files from .pot files and providing a translation for each string. Hacking it yourself is fairly straightforward though: simply copy the .pot file to a .po file (e.g., es_ES.po) in your languages directory and then edit the .pofile and enter your translations on each msgstr line of the file.

Creating a .mo File

Once your .po files are updated for your locale, they need to be compiled into .mo files. The msgfmt program for Linux can be used to generate the .mo files using the command msgfmt es_ES.po --output-file es_ES.mo.

Loading the Textdomain

For each localized plugin or theme in your site, WordPress needs to know how to locate your localization files. This is done via the load_plugin_textdomain(), load_textdomain(), or load_theme_textdomain() function. All three functions are similar, but take different parameters and make sense in different situations.

Whichever function you use, it should be called as early as possible in your app because any strings used or echoed through translation functions before the textdomain is loaded will not be translated.

Here are a few ways we could load our textdomain in includes/localization.php.

load_plugin_textdomain($domain, $abs_rel_path, $plugin_rel_path)

This function takes three parameters. The first is the domain of your plugin or app (“schoolpress” in our case). You then use either the second or third parameter to point to the languages folder where the .mo file should be loaded from. The $abs_rel_path is deprecated, but still here for reverse-compatibility reasons. Just pass FALSE for this and use the $plugin_rel_path parameter:

<?php

function schoolpress_load_textdomain(){

    //load textdomain from /plugins/schoolpress/languages/

    load_plugin_textdomain(

        'schoolpress',

        FALSE,

        dirname( plugin_basename(__FILE__) ) . '/languages/'

    );

}

add_action( 'init', 'schoolpress_load_textdomain', 1 );

?>

The preceding code will load the correct language file from our languages folder based on the WPLANG setting in wp-config.php. We use plugin_basename(__FILE__) to get the path to the current file and dirname(...) to get the path to the root plugin folder since we are in theincludes subfolder of our schoolpress plugin folder.

load_textdomain($domain, $path)

This function can also be used to load the textdomain, but you’ll need to get the locale setting yourself.

Calling load_textdomain() directly is useful if you want to allow others to easily replace or extend your language files. You can use code like the following to load any .mo file found in the global WP languages directory (usually wp-content/languages/) first and then load the .mo file from your plugin’s local languages directory second. This allows developers to override your translations by adding their own .mo files to the global languages directory:

<?php

function schoolpress_load_textdomain() {

    // get the locale

    $locale = apply_filters( 'plugin_locale', get_locale(), 'schoolpress' );

        $mofile = 'schoolpress-' . $locale . '.mo';

        /*

    Paths to local (plugin) and global (WP) language files.

        Note: dirname(__FILE__) here changes if this code

    is placed outside the base plugin file.

    */

        $mofile_local  = dirname( __FILE__ ).'/languages/' . $mofile;

        $mofile_global = WP_LANG_DIR . '/schoolpress/' . $mofile;

        // load global first

        load_textdomain( 'schoolpress', $mofile_global );

        // load local second

        load_textdomain( 'schoolpress', $mofile_local );

}

add_action( 'init', 'schoolpress_load_textdomain', 1 );

?>

This version gets the local via the get_locale() function, applies the plugin_locale filter, and then looks for a .mo file in both the global languages folder (typically /wp-content/languages/) and the languages folder of our plugin.

load_theme_textdomain($domain, $path)

If you have language files for your theme in particular, you can load them through the load_theme_textdomain() function like so:

<?php

function schoolpress_load_textdomain() {

        load_theme_textdomain(

        'schoolpress', get_template_directory() . '/languages/'

        );

}

add_action( 'init', 'schoolpress_load_textdomain', 1 );

?>

Localizing Nonstring Assets

If you’ve gone through the previous steps, you will have everything you need to make sure any string used by your plugin, theme, or app is properly translated. However, you will sometimes have nonstring assets that still need to be swapped out depending on the locale being used.

For example, you might have images with words in them that should be swapped for alternative images with those words translated. Maybe your localized app uses different colors for different countries; you can swap CSS files based on the detected locale.

We often use .html email templates in our plugins that need to be translated. We could wrap the entire email in one big __() function, or we could create a directory of templates for each language. The latter option might mean more work for your translators because they’ll have to generate.html templates along with the .mo files, but it will give developers using your code a bit more flexibility.

Below we’ll write some code to load images and email templates for our plugin based on the local. Assume we have folders like this:

§  schoolpress/images/ (default images)

§  schoolpress/emails/ (default email templates)

§  schoolpress/languages/es_ES/

§  schoolpress/languages/es_ES/images/ (Spanish-version images)

§  schoolpress/languages/es_ES/emails/ (Spanish-version email templates)

We’ll make sure that the functions that load these assets have hooks that will allow us to override which directory is used to get them.

In the following code, we assume that the constant SCHOOLPRESS_URL points to the relative URL for the SchoolPress plugin folder, for example, /wp-content/plugins/schoolpress/:

<?php

// Gets the full URL for an image given the image filename.

function schoolpress_get_image( $image ) {

$dir = apply_filters(

   'schoolpress_images_url',

    SCHOOLPRESS_URL . '/images/'

);

        return $dir . $image;

}

?>

Now in our includes/localization.php folder, we can put some code in place that will filter schoolpress_images_url if a nondefault locale is used:

<?php

function localize_schoolpress_images_url( $url ) {

        $locale = apply_filters( 'plugin_locale', get_locale(), 'schoolpress' );

        if ( $locale != 'en_US' )

                $url = SCHOOLPRESS_URL . '/languages/' . $locale . '/images/';

        return $url;

}

add_filter( 'schoolpress_images_url', 'localize_schoolpress_images_url' );

?>

You could do the same thing for loading emails. In the following code, we assume the constant SCHOOLPRESS_PATH points to the server pathname for the SchoolPress plugin, for example, /var/vhosts/schoolpress.com/httpdocs/wp-content/plugins/schoolpress/:

<?php

// Gets the full path for an email template given the email filename.

function schoolpress_get_email( $email ) {

    $dir = apply_filters(

        'schoolpress_emails_path', SCHOOLPRESS_PATH . '/emails/'

        );

        return $dir . $image;

}

// Filters the schoolpress_emails_path value based on locale.

// Put this in includes/localization.php

function localize_schoolpress_emails_path( $path ) {

        $locale = apply_filters( 'plugin_locale', get_locale(), 'schoolpress' );

        if ( $locale != 'en_US' )

                $path = SCHOOLPRESS_PATH . '/languages/' . $locale . '/emails/';

        return $path;

}

add_filter( 'schoolpress_emails_path', 'localize_schoolpress_emails_path' );

?>

Depending on the use case of your web application, translating your app may be essential to its success. When building any custom theme or plugin, it’s good practice to write all of your code with localization in mind!


[25If not, locate and install the “gettext” package for your Linux distro.