Chapter 13. Localizing WordPress Apps

Localization (or internationalization) is the process of translating your app for use in different locales and languages. In this chapter we’ll discuss the tools and methods available to you for localizing your 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 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, 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) are simple to do and often have other uses beyond 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 it 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 WordPress includes the following components:

  • A way to define a locale/language

  • A way to define text domains

  • 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 US English. And Mexican Spanish is different from the Spanish spoken in Spain.

Text Domains

The gettext specification uses text domains to organize the translation tables. In WordPress, this means that each plugin and theme should have its own unique text domain.

Technically, you can use anything for your text domain as long as it is unique and consistent and uses the proper syntax (all lowercase, using dashes but no underscores). In practice, the text domain for a plugin or theme should match the slug because most other plugins and tools will expect that. For example, plugins in the WordPress.org repository require that the text domain matches the plugin’s slug so GlotPress and other tools on the WordPress.org site function properly.

Here are some text domains being used in live projects:

  • All code in WordPress core uses the text domain default.

  • Paid Memberships Pro plugin uses the text domain paid-memberships-pro.

  • Memberlite uses the text domain memberlite.

Setting the Text Domain

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

Note

The gettext specification and WordPress functions use the concatenated term textdomain when referring to text domains. It is common, however, to spell out both words separately when writing about them.

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 text domain is loaded will not be translated.

Here are a few ways we could load our text domain 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 from which the .mo file should be loaded. 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 text domain from /plugins/schoolpress/languages/
    load_plugin_textdomain(
        'schoolpress',
    	FALSE,
    	dirname( plugin_basename(__FILE__) ) . '/languages/'
    );
}
add_action( 'init', 'schoolpress_load_textdomain', 1 );
?>

This code loads the correct language file from our languages folder based on the WPLANG setting in wp-config.php. To get the path to the current file, we use plugin_basename(__FILE__), and then dirname(...) to get to the path to the root plugin folder, since we are in the includes subfolder of our schoolpress plugin folder.

Note

You may also see a “Text Domain” set in the PHP comment header of a plugin or theme. This is used by WordPress to translate the plugin meta information even if the plugin itself is disabled. Since WordPress 4.6, this header setting is optional and defaults to the plugin or theme’s slug if not set.

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 );
?>
load_textdomain( $domain, $path )

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

Calling load_textdomain() directly is not recommended for plugins or themes, but could be useful for projects in which you want to use a single domain across many different plugins. Calling load_textdomain() directly also adds some flexibility 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 WordPress 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 locale 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.

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 other information to let translators know what context to use when translating the text.

Let’s 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 returns the translated text based on the domain and the language set in wp-config.php.

Note

The __() function is really an alias for the translate() function used in the background by WordPress. There’s no reason you couldn’t directly call translate(), 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 echoes 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 returns 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 to 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 msgfmt command-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, macOS, 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 store the localization files in a folder called languages inside the main app plugin. We add all 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/schoolpress-es_ES.po (Spanish/Spain translations)

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

When you’re 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 will only be used for this one project, they can probably be built as includes or module .php files in your main app plugin. If you might use the plugins on another project, they need to 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,1 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="[email protected]" 
--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="[email protected]"

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 scanning 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 .po file 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.

GlotPress

GlotPress is a tool to allow translators to collaborate on translations online. Instead of managing individual .po files for each locale, GlotPress provides a website UI for editing any string for any locale. Translations are stored in a database, and when a defined percentage of strings is translated for the plugin, GlotPress automatically generates the .po and .mo files. You don’t even need to download the generated language packs or bundle them with your plugin. WordPress will find and download them automatically based on your chosen locale.

Using GlotPress for Your WordPress.org Plugins and Themes

If you are hosting your plugin or theme in the WordPress.org repository, using GlotPress is as easy as properly wrapping your strings for translation and making sure that your text domain matches your plugin or theme’s slug. If you did this properly, a new translation project is generated for you at one of the following locations:

  • https://translate.wordpress.org/projects/wp-plugins<your-plugins-slug>/

  • https://translate.wordpress.org/projects/wp-themes<your-themes-slug>/

That’s it. If you’ve already bundled existing .po files for your plugin in a /languages/ folder, they should be imported into the GlotPress project as locales with those strings already translated.

Once your plugin or theme is integrated with GlotPress, you no longer need to bundle language files with your project. WordPress looks for language packs based on your locale and automatically downloads them for use. However, note that only locales that are 95% or more translated will be distributed automatically. So make sure you stay on top of the translations to make them complete. Alternatively, you can download .po and .mo files for incomplete translations and bundle them with your plugin per the instructions earlier in this chapter.

Creating Your Own GlotPress Server

If you host your plugin or theme on your own server, you can still take advantage of the GlotPress technology by setting up your own translation server. The GlotPress team has created a WordPress plugin that you can install and activate on any WordPress site. The GlotPress Plugin will set up an endpoint at /glotpress/ where you can create new translation projects for your own self-hosted plugins and themes.

Once your GlotPress server is set up, you can create a new project for your non-WordPress.org plugin. WordPress won’t download those translations automatically for your users, but you can export the .po and .mo files to include in your plugin files.

Note

It seems like it would be possible to use the translations_api hook to update WordPress to look at your GlotPress server to download available translations, but we don’t see indications that anyone is doing that. It could be an interesting exercise for a motivated reader of this book. Bundling the translation files with your distributed plugin seems like a more straightforward way to do things.

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

1 If not, locate and install the gettext package for your Linux distribution.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.220.57.222