Introduction and overview

This page will primarily be of interest to those that are wanting to make static text in their own template files or modules translatable using ProcessWire's translation tools. ProcessWire uses a GNU gettext-like system for managing language translation of strings in your code (without actually using gettext). It is designed to work very much like the WordPress internationalization (i18n) system (which does use gettext), and some of this page is likewise based on the WordPress i18n documentation. However, there are some differences that will be highlighted.

Demonstration Video

Below is a brief video demonstrating how simple it is to use ProcessWire's language translation tools in your own templates (switch to 720p and the full screen version to see it better):

Translatable strings

In order to make a string translatable in your template or module code, you just have to wrap the original string in a $this->_() or __() function call:

$out = $this->_("Live long and prosper");  // syntax within a class
$out = __("Live long and prosper!"); // syntax outside of a class

Marking strings for translation

The strings for translation are wrapped in a call to one of a set of special functions. The most commonly used ones are $this->_() and __(). These functions just returns the translation of their argument:

echo "<h2>" . $this->_('Site Information') . "</h2>";

Syntax inside a class vs. syntax outside of a class

In ProcessWire, the functions __() and $this->_() are equivalent, but __() will work in all contexts while $this->_() will only work within the context of a class. So why have $this->_() at all? Because it has a speed advantage that can only be realized within the context of a class. As a result, the following rules apply:

  • When your translation needs occur outside of a class–such as in a template file–you must use the __('string') function call.
  • When your translation needs occur within a Processwire class–such as in a plugin module–it is preferable (though not required) to use $this->_('string').

The rules above also apply to the other translation functions outlined later in this page.

Placeholders

Lets say that you needed to output a string like this:

echo "Created $count pages."

Perhaps your first thought is to try this:

echo __("Created $count pages.");

It won't work! Strings for translation are extracted from the ProcessWire PHP files, so people performing translation will see the phrase: Created $count pages… However in ProcessWire, the __() function will be called with an argument like Created 3 pages. and ProcessWire won't find a suitable translation and will return its argument: Created 3 pages… regardless of how many pages there actually were. Meaning, it isn't translated correctly.

The solution is to use the printf family of functions. Especially helpful are printf and sprintf. Here is what the right solution to the page count problem will look like:

printf(__("Created %d pages."), $count);

Or, if you are building output into a variable (like $out), you'd use sprintf:

$out = sprintf(__("Created %d pages."), $count); 

Notice that the string for translation is just the template Created %d pages., which is the same both in the source and at run-time.

If you have more than one placeholder in a string, it is recommended that you use argument swapping. In this case, single quotes (') are mandatory : double quotes (") will tell php to interpret the $s as the s variable, which is not what we want.

$out = sprintf(__('Your city is %1$s, and your zip code is %2$s.'), $city, $zipcode);

Here the zip code is being displayed after the city name. In some languages displaying the zip code and city in opposite order would be more appropriate. Using %s prefix in the above example, allows for such a case. A translation can thereby be written:

$out = sprintf(__('Your zip code is %2$s, and your city is %1$s.'), $city, $zipcode);

Plurals

Let's get back to the 'created pages' example: sprintf(__("Created %d pages."), $count);. What if we create only 1 page? The output will be: Created 1 pages., which is definitely not correct English, and would certainly be incorrect for many other languages as well.

In ProcessWire you can use the _n() or $this->_n() function.

$out = sprintf(_n("Created %d page.", "Created %d pages.", $count), $count);

_n() and $this->_n() accept 3 arguments:

  • singular — the singular form of the string
  • plural — the plural form of the string
  • count — the number of objects, which will determine if the singular or the plural form to be returned (there are languages, which have far more than 2 forms)

The return value of the functions is the correct translated form, corresponding to the given count.

Context

Sometimes one term is used in several contexts and although it is one and the same word in English it has to be translated differently in other languages. For example the word Post can be used both as a verb (Click here to post your comment) and as a noun (Edit this post). In such cases the _x() or $this->_x() function should be used. It is similar to __(), but it has an additional second argument–the context:

$label = _x('Comment', 'noun'); // or $this->_x('Comment', 'noun') in a class
...
// some other place in the code
echo _x('Comment', 'column name');

Using this method in both cases we will get the string Comment for the original version, but the translators will see two Comment strings for translation, each in the different contexts.

To summarize contexts, you should use the _x() or $this->_x() functions when two or more identical translatable strings will appear in more then one place in the same file.

Comment descriptions

Do you think translators will know how to translate a string like: __('g:i:s a')? In this case you can add a clarifying comment in the source code. The comment must begin with "//" and be on the same line as the translation function call. Here is an example:

$date = __('g:i:s a'); // Date string in PHP date() format

In this way you can write a "personal" message to the translators, so that they know how to deal with the string. It is also recommended that you use comment descriptions in long translatable strings (i.e. those that are more than a sentence) so that you can summarize what it is for. The translator will be provided with the string to translate either way, so it is always good to use comment descriptions any time you think they might be helpful. Note that comment descriptions in ProcessWire are different from those in WordPress.

Comment notes

You can also use secondary comment descriptions, called "notes". These appear as secondary notes below the input field the translator is working with. This is a way to provide additional details that you may want separated from the main comment description. This is also what ProcessWire uses to identify context or plurals to the translators (which it does automatically). Here is an example of how you might use notes:

echo __("Welcome Guest"); // Headline for guest user // Keep it short (2-3 words)

Translation rules

ProcessWire's file translation parser works very much like the gettext parser in that it pulls the strings directly from the PHP files (outside of program execution), rather than identifying them at runtime. This is necessary because it is simply not possible to identify every translatable string at runtime. It would require every scenario to play out at execution (i.e. every error message, every success message, etc. in the same request). Given that, ProcessWire has to be able to identify your translatable strings directly in the PHP source code of your files. The format must be consistent and well-formed, as outlined below. Note that these rules may vary somewhat from those of WordPress and other gettext-implementations.

Translation function calls must be on one line and in one pair of quotes

A translation function call must exist on a single line and be within a single pair of quotes. Here are examples that will NOT work:

$out = __("Something " . "and something else"); // bad
$out = __("Now is the time for all good men " . // bad
          "to come to the aid of their party."); 

And here are examples that WILL work:

$out = __("Something and something else"); // good 
$out = __("It's time for you \nto get to the party.");  // good 

Note the "\n" between the words "you" and "to" above. It is okay to include a PHP carriage return character in your strings like this if you need it. What's not okay is to have an actual line break in your code.

To reiterate the above examples: if you have a long string of text that you are sending to a translation function, you need to keep it all on one line. Embrace horizontal scrolling in your code editor. :)

There may only be one translation function call per line

Your translation function calls should be limited to one per line. The parser will not recognize more than one translation call per line so any additional calls after the first will be ignored by the parser. This is to ensure that you can adequately use comment descriptions (as mentioned earlier), as well as to ensure consistent and readable code. Here is an example that will NOT work:

$out = __("Something") . " " . __("and something else");  // bad

And here are examples that WILL work:

$out = __("Something") . " " .     // good
       __("and something else");  

…or…

$a = __("Something");     // good
$b = __("and something else");
$out = "$a $b"; 

Empty strings

Don't try to internationalize an empty string. It doesn't make any sense, because the translators won't see any context.

Best practices

While ProcessWire doesn't use gettext, it is based on many of the same conventions and what applies for gettext generally applies for translation in ProcessWire. Below is a summary of best practices from the gettext manual and this page.

  • Decent English style—minimize slang and abbreviations.
  • Entire sentences—in most languages word order is different than that in English.
  • Split at paragraphs—merge related sentences, but do not include whole page of text in one string.
  • Use format strings instead of string concatenation—sprintf(__('Replace %s with %s'), $a, $b); is always better than __('Replace ') . $a . __(' with ') . $b;
  • Avoid markup and unusual control characters—do not include tags that surround your text and do not leave URLs for translation, unless they could have version in another language.
  • Do not leave leading or trailing whitespace in a translatable phrase.
  • Keep your translation phrases on 1 line and between 1 pair of quotes.
  • Use only 1 translation function call per line in your source code.
  • Use $this->_('string') in your class files, and __('string') everywhere else.

Technical details

Below are some additional technical details that may be of interest to some, but are not required reading.

Using Textdomains

If you've worked with gettext before, you may be familiar with the term "textdomain", which refers to a group of related translations. Textdomains are used to ensure that only the necessary translations are kept in memory at the same time, and that there aren't namespace collisions of unrelated translations. While ProcessWire does not use gettext, it does use textdomains, though in a little bit different way.

In ProcessWire, each PHP file is considered it's own textdomain and the textdomain is nothing more than the filename (including path) from the root of the ProcessWire installation. The textdomain is not loaded by ProcessWire until a function call from a given PHP file requests a translation for a phrase. The textdomain consists of all translation phrases for the current language in one PHP file, and textdomains are internally stored in JSON files by ProcessWire.

The developer using translation function calls does not have to think about textdomains, as it is something that ProcessWire figures out behind the scenes. However, if a developer does want to override the textdomain from the curent file for a given translation, they can do so by specifying the PHP filename (including path) as the second argument to a __() call, like this:

echo __('Save', '/site/templates/common.php'); 

You will see an example of this in ProcessWire's admin theme in the file /wire/templates-admin/topnav.inc:

$title = __($title, '/wire/templates-admin/default.php'); 

We do that in the default admin theme because we want to pull translations from default.php without keeping duplicate copies of translatable phrases in topnav.inc. You can see we're actually using $title rather than a static string in the function call above. We're doing that because we only need the translation capability of the __() function, which ultimately doesn't care if you give it a dynamic or static string. We've setup some predefined translations in default.php that we know are expected, like 'Pages', 'Setup', etc., and that's what is being translated in the above function call.

The __() function will also accept an actual object instance as a textdomain as well. It will figure out what file it came from on it's own. So if you know that there is a translatable phrase in the $pages API object that you want to make use of, you can put your phrase in the context of the $pages textdomain (/wire/core/Pages.php) just like this:

echo __('Page saved', $pages); 

Note that the above is a contrived example used for demonstration purposes only, as $pages doesn't actually have any translatable phrases in it.

Comments

  • mr-fan

    mr-fan 3 years ago 21

    Just to mention that there is a comfortable addon:
    Quick hint for translation:

    ProcessLanguageTranslatorPlus
    https://github.com/kixe/ProcessLanguageTranslatorPlus

    With this Addon you could choose any "untranslated" file in a List including corefiles...so it is much more easier to find untranslated languagefiles for used addons.

    best regards

  • Can

    Can 2 years ago 00

    Really love everything PW or better to say Ryan does! :D

    Always great new things to discover like using Markdown in translated Strings.
    Stumbled upon this really awesome feature while checking out /core/Inputfield.php

    You only need to wrap your translatable string in $sanitizer->entitiesMarkdown($str) like so

    $sanitizer->entitiesMarkdown( __("Mark**down** [test](http://local.dev)") )

    works like a charm! and will def make my code a little more nicer as I used if/else for some strings because I needed some formatting.

    THANK YOU RYAN! :D

  • Yannick Albert

    Yannick Albert 10 months ago 00

    Because it has a speed advantage

    Is there a huge different between $this->_ and __?

    • Can

      Can 10 months ago 00

      Interesting question which I'm not able to answer exactly but when Ryan suggests to use $this->_ whenever you're in a class than it seems fast enough to be mentioned ;-)

Post a Comment

Your e-mail is kept confidential and not included with your comment. Website is optional.