I just posted a short summary over at make.wordpress.org of all the internationalization (i18n) enhancements and bug fixes in the upcoming WordPress 6.2 release, many of which I worked on myself. Check it out:
Tag: Internationalization
-
Safely Using Strings Containing Markup in React with DOMParser
For the Web Stories WordPress plugin I came up with a solution to parse strings containing markup in a React application by leveraging the
DOMParser
interface. This is especially useful when dealing with translations where you would want to avoid any string concatenation.I’ve previously written quite a bit on JavaScript internationalization in WordPress 5.0+. However, one aspect I did not address at the time was how to use these new features with translations containing markup. That’s because it was simply not possible until recently, unless you would use a dangerous function like
dangerouslySetInnerHTML
. But since that would pose security risks, it is not advisable for use in this case.Thankfully, a new
createInterpolateElement
function was introduced to Gutenberg late last year that solves the problem in a safe way. It does so by sanitizing the input string using a simple parser that removes any unwanted markup. Here’s an example:import { __ } from '@wordpress/i18n'; import { createInterpolateElement } from '@wordpress/elementt'; import { CustomComponent } from '../custom-component.js'; const translatedString = createInterpolateElement( __( 'This is a <span>string</span> with a <a>link</a> and a self-closing <custom_component />.' ), { span: <span>, a: <a href="https://make.wordpress.org/"/>, custom_component: <CustomComponent />, } );
Code language: JavaScript (javascript)Any tag in the translated string that is part of the map in the second argument will be replaced by that component. So
span
will be replaced by an actualspan
tag, for example. If the translation contains any other markup not in the map, let’s say a some<img onClick={doBadStuff()} />
, it would simply be discarded. Awesome!Now, as you can see from the example above, this utility function is part of the
@wordpress/element
package and not@wordpress/i18n
, as one might have expected. But what if you don’t want to use the former, or perhaps can’t?An Alternative to
createInterpolateElement
To answer this question, I looked at the implementation of
createInterpolateElement
under the hood. It’s actually quite neat, but also a bit complex using a regex-based tokenizer. I wanted something simpler.The requirements were straightforward:
- It needs to be fast at parsing strings with some simple markup
- It needs to be secure
- It needs to work in modern browsers (no IE support)
My research quickly led me to the
DOMParser
interface, which allows parsing XML or HTML source code from a string into anHTMLDocument
. It is supported by all major browsers. But does it also work for this use case? I was keen to find out!From
DOMParser
to ReactSpecifically, I looked into using
DOMParser.parseFromString()
to parse a given string into anHTMLDocument
, traverse through that document and create actual React elements (usingReact.createElement
andReact.cloneElement
) for every found HTML element based on the provided map. Text elements could just be used as-is. This worked incredibly well from the get-go. Here’s an excerpt of the final code:const node = new DOMParser().parseFromString(children, 'text/html').body .firstChild; // Loops through the document and calls transformNode on each node. transform(node, mapping).map((element, index) => ( <Fragment key={index}>{element}</Fragment> )); function transformNode(node, mapping = {}) { const { childNodes, localName, nodeType, textContent } = node; if (Node.TEXT_NODE === nodeType) { return textContent; } const children = node.hasChildNodes() ? [...childNodes].map((child) => transform(child, mapping)) : null; if (localName in mapping) { return React.cloneElement(mapping[localName], null, children); } return React.createElement(localName, null, children); }
Code language: JavaScript (javascript)You can find the full code including documentation and tests on GitHub.
A key difference to
createInterpolateElement
is that elements missing from the map won’t be simply discarded, but inserted without any props/attributes being set, mitigating any security risks. It also means that void elements such as<br>
can be used in the translatable strings, which can come in handy at times. -
Improving WordPress Internationalization with ESLint
Avid readers will already know that I am very passionate about internationalization (I18N). Some of my most popular blog posts are about that topic:
- WordPress Internationalization Workflows
- Internationalization in WordPress 5.0
- The Text Domain in WordPress Internationalization
Internationalization is an important aspect in WordPress development as it lays the foundation for a project’s global success. Unfortunately, it is often done wrong, but things get better over time thanks to simplified APIs, improved documentation, and tooling. For example, the WordPress Coding Standards for PHP_CodeSniffer has been detecting incorrect usage of I18N functions for years now. However, there was no equivalent for this kind of detection in JavaScript source files — until today.
Being involved with the development of many JavaScript-heavy WordPress projects, I often see common mistakes when using the @wordpress/i18n package that could be easily caught by some kind of linter. To validate my thinking, I set out to fix this issue and contribute the solution to the WordPress community.
Extending The WordPress ESLint Plugin
First, I started writing down all the things that could possibly be developed to help improve the WordPress JavaScript I18N landscape. This includes things like detecting wrong usage of text domains, missing translator comments, and flagging usage of variables in translatable strings. I even thought about detecting strings that should probably be translatable, but currently aren’t. Tricky to do, but one can dream.
Then, I was looking for the best place to implement this. Luckily, WordPress and also our own projects already use a handy tool for this: ESLint. ESLint is the JavaScript-equivalent of PHPCS, and the @wordpress/eslint-plugin package is the one that can be used to enforce WordPress coding standards. For me, that was the perfect place to start.
By reading through ESLint’s great developer documentation I learned all about creating custom linter rules, and studying existing rules in the aforementioned package, as well as eslint-plugin-wpcalypso from WordPress.com, hel.
Before I knew it, I was knee-deep in writing ESLint rules, tests, and fixes for the issues my rules discovered. Hundreds of lines of code later, you can now use these new features in your projects!
The New I18N ESLint Ruleset
In total, I ended up creating six new ESLint rules around internationalization, and improving one existing rule. If you’re already using the recommended ruleset from the WordPress ESLint plugin (version 5.0.0 or higher!), you automatically benefit from these enhancements. Alternatively, you can also only extend the I18N ruleset if wanted. For example:
{ "extends": [ "plugin:@wordpress/eslint-plugin/i18n" ] }
Code language: JSON / JSON with Comments (json)It includes the following rules:
@wordpress/i18n-text-domain
The 18n-text-domain rule enforces passing valid text domains to translation functions (e.g. only string literals). It flags things like
__( 'Hello World' )
, but allows__( 'Hello World', 'awesome-sauce' )
if your project’s text domain isawesome-sauce
.Your desired project text domain can be specified in the ESLint config as follows:
{ "@wordpress/i18n-text-domain": [ "error", { "allowedTextDomain": "awesome-sauce" } ] }
Code language: JSON / JSON with Comments (json)@wordpress/i18n-translator-comments
If using translation functions with placeholders in them, they should have accompanying translator comments. The i18n-translator-comments rule flags the lack thereof.
@wordpress/i18n-no-variables
In WordPress development, you must call translation functions with valid string literals as arguments. They cannot be variables or functions for technical reasons. Use the i18n-no-variables rule to easily enforce this.
@wordpress/i18n-no-placeholders-only
Translatable strings that consist of nothing but a placeholder, e.g.
__( '%s' )
, cannot be translated. The i18n-no-placeholders-only rule prevents such usage.@wordpress/i18n-no-collapsible-whitespace
With the i18n-no-collapsible-whitespace rule you can prevent using complex whitespace in translatable strings. Relying on HTML to collapse such whitespace can make translation more difficult and lead to unnecessary retranslation.
@wordpress/i18n-ellipsis
Lastly, the i18n-ellipsis rule disallows using three dots in translatable strings. Three dots for indicating an ellipsis should be replaced with the UTF-8 character
…
(horizontal ellipsis, U+2026) as it has a more semantic meaning.@wordpress/valid-sprintf
The existing valid-sprintf rule enforces valid usage of the
sprintf
function exposed by the @wordpress/i18n package. I’ve extended it to catch and prevent a mix of ordered and non-ordered placeholders. Multiplesprintf
placeholders should be ordered so that strings can be better translated. -
Saving the Romansh Language with WordPress
Tgi che sa rumantsch sa dapli — if you know Romansh, you know more
Switzerland has four official languages: German, Italian, French, and Romansh. Growing up in the canton of Grisons, I got in touch with the latter early on. Unfortunately, it is a dying language. To do something against this, I decided to translate WordPress into Romansh. And I don’t even speak the language!
But WordPress would be the ideal platform for a Romansh translation. The world’s most popular content management system (CMS) has a market share of 35% and is also very common in Switzerland. That means many people are interacting with it on a daily basis.
It all began with a simple idea a couple of years ago, I think it was around WordCamp Europe 2015. After talking about this with some people, many showed interest and also thought it would be a cool idea. However, nothing concrete happened yet.
The First Steps
In order to move things forward, I got in touch with the WordPress Polyglots team to properly set up Rumantsch on the translation management platform. I figured that this was the biggest hurdle to overcome. Once the translation platform was ready, interested people could just start translating and actually make this happen. I was able to do some basic translations myself thanks to an online dictionary. However, for the more complex strings I needed help from people who actually speak the language.
Besides talking to friends and acquaintances who speak Romansh, I also got to know Gion-Andri Cantieni and his initiative Software rumantscha. I was pretty impressed when I learned that they have been successfully translating Firefox, Microsoft Office, and even the Contao CMS to Romansh for quite some time. This was even in the news, which showed me that it’s not a crazy idea at all to try to translate WordPress.
Now that we were a group of people, we were quickly able to translate about a third of WordPress to Rumantsch. At WordCamp Europe 2017, I shared the story about how we got there with the global WordPress community:
Getting Involved
Efforts stagnated a bit after that, but now I want to take another attempt at translating WordPress into the Romansh language. It’s quite fitting that this year marks the 100-year anniversary of Lia Rumantscha, the local institution that promotes the Romansh language and culture.
As of today, the Rumantsch translation of WordPress is around 35% complete. This is what it looks like in the WordPress admin:
To get it to 100%, I need your help!
First of all, if you’re interested in using WordPress in Rumantsch or want to support the translation efforts in any form, please let me know!
If you want to jump right into the action and start translating WordPress, all you need is a WordPress.org user account. Once signed up you can head to translate.wordpress.org right away to find all the projects that can be translated.
This includes WordPress core, but also the WordPress.org websites and even the WordPress mobile apps. The most important project to translate is certainly WordPress 5.0, the current WordPress release.
We’ve collected some helpful resources for translators at roh.wordpress.org/translatar. Yes, that’s right — WordPress en Rumantsch has its own website! In addition to that page, the Polyglots handbook has some very useful information as well.
Also make sure to join the WordPress Switzerland Slack workspace at wpch.slack.com using your WordPress.org email address (
<username>@chat.wordpress.org
). There we have a dedicated#polyglots
channel for this purpose.Have you got any questions so far? Please leave a comment, send an e-mail, or ping me on Twitter.
-
Internationalization in WordPress 5.0
In my previous blog post I explained the importance of the text domain in WordPress internationalization. Today, I want to have a look at the bigger picture of the (new) internationalization features in WordPress 5.0 and beyond. This includes, but is not limited to, enhanced JavaScript internationalization.
If you’re building a WordPress plugin or theme and want to make sure it can be fully localized, this post is for you.
WordPress JavaScript Internationalization
WordPress 5.0 shipped with a completely new editing experience called Gutenberg. This new editor is mainly written in JavaScript, which means a lot of internationalization now happens client-side instead of on the server. Although WordPress core has previously used functions like
wp_localize_script()
to make some of its more dynamic UIs translatable, a more robust solution was needed for such a complex addition like Gutenberg.JavaScript Localization Functions
New JavaScript I18N Support in WordPress 5.0 brings the same capabilities to JavaScript development for WordPress that we’re already used to from PHP. This starts with a new
wp-i18n
JavaScript package that provides localization functions like__()
,_x()
,_n()
,_nx()
, and evensprintf()
. These functions mirror their PHP equivalents and can be used in the same ways.To use this package, you need to add the
wp-i18n
script as a dependency when registering your JavaScript:wp_register_script( 'my-plugin-script', plugins_url( 'js/my-script.js', __FILE__ ), array( 'wp-i18n' ), '0.0.1' );
Code language: PHP (php)After that, the localization functions are available under the
wp.i18n
global variable in JavaScript. You can use them like this:const { __, _x, _n, sprintf } = wp.i18n; __( 'Hello World', 'my-plugin' ); _x( 'Glitter Box', 'block name', 'my-plugin' ); // Get the comment count from somewhere else in our script. const commentCount = wp.data.select( 'my/data-store' ).getCommentCount(); /* translators: %s: number of comments */ sprintf( _n( 'There is %s comment', 'There are %s comments', commentCount, 'my-plugin' ), commentCount );
Code language: JavaScript (javascript)That’s all you need to make your JavaScript components fully localizable.
If you’re familiar with the PHP translation functions in WordPress core, you’ll notice the absence of something like
esc_html()
oresc_html__()
. These aren’t needed in JavaScript because the browser is already capable of escaping unsafe characters.Note: although it’s discouraged to use HTML in translatable strings, sometimes this is necessary, e.g. for adding links (
Check out this link to <a href="%s">my website</a>.
). Right now, it’s not easily possible to do so, at least not without usinginnerHTML
/dangerouslySetInnerHTML
. However, this is actively being discussed on GitHub.Loading JavaScript Translations
Keep in mind that just using the
__()
family of functions isn’t enough for a WordPress plugin or theme to be fully internationalized and localized. We also need to tell WordPress to load the translations for our scripts. This can be achieved by using the newwp_set_script_translations()
function introduced in WordPress 5.0.That function takes three arguments: the registered script handle (
my-plugin-script
in the previous example), the text domain (my-plugin
), and optionally a path to the directory containing translation files. The latter is only needed if your plugin or theme is not hosted on WordPress.org, which provides these translation files automatically.Note: If you’re registering multiple scripts that all use
wp.i18n
, you have to callwp_set_script_translations
for each one of them.wp_register_script( 'my-plugin-script', plugins_url( 'js/my-script.js', __FILE__ ), array( 'wp-i18n' ), '0.0.1' ); wp_register_script( 'my-awesome-block', plugins_url( 'js/my-block.js', __FILE__ ), array( 'wp-i18n' ), '0.0.1' ); wp_set_script_translations( 'my-plugin-script', 'my-plugin' ); wp_set_script_translations( 'my-awesome-block', 'my-plugin' );
Code language: PHP (php)The reason for this is performance. Translations are only loaded when your script is actually enqueued. If that is the case, WordPress loads the translation files into memory and provides them to
wp.i18n
via inline JavaScript. That means WordPress requires one translation file per script handle with each file only containing strings relevant for that script.Imagine writing a JavaScript-heavy WordPress plugin with lots of different packages that can also be used independently of each other. You don’t want to load all translations if you just need the ones for a single package.
JavaScript Translation Files
We have now covered loading the JavaScript translation files, but what exactly is so special about them? Well, this time we’re not dealing with PO or MO files, but with JSON files instead. Since JSON can be read very easily in JavaScript, it’s a convenient format to store translations in.
Also, the
wp-i18n
package uses a library under the hood that is largely compatible with the Jed JavaScript gettext library, which requires Jed-style JSON translation data. As mentioned in the previous section, WordPress.org provides these translation files automatically. But if you want to ship your own, you need to create such JSON files yourself.A very simple Jed-style JSON translation file looks like this:
{ "domain": "messages", "locale_data": { "messages": { "": { "domain": "messages", "plural_forms": "nplurals=2; plural=(n != 1);", "lang": "de_DE" }, "Source": [ "Quelle" ], "Enter the information for this recommendation.": [ "Gib die Informationen zu dieser Empfehlung ein." ], "%s comment": [ "%s Kommentar", "%s Kommentare" ], "block name\u0004Recommendation": [ "Empfehlung" ] } } }
Code language: JSON / JSON with Comments (json)If you’re familiar with PO translation files already, this format contains similar information like information about the locale (
de_DE
) and its plural forms. All the strings are in themessages
object, with the originals as keys, and the translations being the value. If a string has an additional context, it is prepended by it, with\u0004
acting as a delimiter.Note: An important detail here is the text domain, which right now needs to be
messages
and not the one you actually use in the code. There’s a WordPress Trac ticket for this though, so it might be supported in the future.JavaScript Translation File Names
PO and MO translation files in WordPress usually have the format
$textdomain-$locale.po
, e.g.my-plugin-de_DE.po
. For the JSON files things are a bit different now.You might remember that we need to pass the script handle name to
wp_set_script_translations()
. This handle needs to be in the file name as well, in the form$textdomain-$locale-$handle.json
.So for our
my-plugin-script
script handle, the translation file name needs to bemy-plugin-de_DE-my-plugin-script.json
.For technical reasons, WordPress also looks for files in the form
$textdomain-$locale-$md5.json
, where$md5
is the MD5 hash of the JavaScript file name including the extension. In the earlier example,my-plugin-script
points tojs/my-script.js
. The MD5 hash ofmy-script.js
is537607a1a008da40abcd98432295d39e
. So the alternative file name for our translation file ismy-plugin-de_DE-537607a1a008da40abcd98432295d39e.json
.Generating JavaScript Translation Files
Since WordPress requires one translation file per script handle, with each file only containing strings relevant for that script, this quickly means dealing with plenty of JSON files. Luckily, there’s no need to write these by hand.
The recommended way to generate the JSON translation files is by using WP-CLI. The latest version, WP-CLI 2.1.0, provides a dedicated
wp i18n make-json
command for this.The
wp i18n make-json
command extracts all the JavaScript strings from your regular PO translation files and puts them into individual JSON files.Note: WP-CLI 2.1.0 been released on December 18. Make sure you’re using the latest version by running
wp cli update
. You can check your current version usingwp cli version
.Let’s say in your plugin folder
my-plugin
you have three source files:my-plugin.php
,js/my-script.js
andjs/my-block.js
. You use WP-CLI to extract the strings and generate the translation catalogue (POT) like this:wp i18n make-pot my-plugin my-plugin/languages/my-plugin.pot
From there you can translate your plugin as usual and create the needed PO and MO files. Let’s say we add a German translation to
my-plugin/languages/my-plugin-de_DE.po
first. After that, you can simply runwp i18n make-json my-plugin/languages
to generate the JavaScript translation files. The result will be as follows:- A new
my-plugin/languages/my-plugin-de_DE-537607a1a008da40abcd98432295d39e.json
file contains the translations formy-script.js
. - A new
my-plugin/languages/my-plugin-de_DE-dad939d0db25804f91959baeec56ea8a.json
file contains the translations formy-block.js
. - The
my-plugin/languages/my-plugin-de_DE.po
now only contains the translations that are needed on the server side.
If you don’t want to modify the PO file, pass the
--no-purge
argument to the WP-CLI command, as explained in the documentation.Note: There are a few known issues in these WP-CLI commands with some edge cases. We’re continuously working on improving the tooling as we learn about how people use them.
Tooling
These new processes introduced with WordPress 5.0 and Gutenberg can feel a bit complex at the beginning. To make lives easier, I want to share some tips and tricks for your project’s configuration.
Webpack Configuration
If you reference the global variables like
wp.i18n
in your project everywhere, you don’t benefit from your code editor’s power to show things like type hints. To change that, I recommend installing the@wordpress/i18n
package as a (development) dependency usingnpm
/yarn
. After that, you can useimport { __ } from '@wordpress/i18n;
throughout your project.Normally, this would make Webpack bundle the library with your code. Since WordPress already exposes the library via the
wp.i18n
global, there’s no need for code duplication. To prevent this, add the following to your Webpack configuration:externals: { '@wordpress/i18n': { this: [ 'wp', 'i18n' ] } }
Code language: JavaScript (javascript)This way you’ll benefit from both your IDE’s powers as well as the already available
wp.i18n
global. Just make sure you addwp-i18n
as a dependency when callingwp_register_script()
.Babel Integration
In the previous section I mentioned using
wp i18n make-pot
to create the necessary translation catalogue from which you can create the actual localizations. Depending on your developer workflow, you might want to look into using a build tool for Babel called@wordpress/babel-plugin-makepot
to create the POT file. The latter approach integrates with Babel to extract the I18N methods.To do so, run
npm install --save-dev @wordpress/babel-plugin-makepot
and add the following plugin to your Babel configuration:[ '@wordpress/babel-plugin-makepot', { output: 'languages/my-plugin-js.pot', }, ]
Code language: JavaScript (javascript)Note: You still want to create a POT file for the rest of your PHP files, not just your JavaScript files. You can still do that using WP-CLI. Just skip the JavaScript string extraction and merge the resulting POT files like this:
wp i18n make-pot my-plugin my-plugin/languages/my-plugin.pot --skip-js --merge=my-plugin/languages/my-plugin-js.pot
In this scenario,
languages/my-plugin-js.pot
would only be of temporary nature, so you could remove it again afterwards.Available Hooks and Filters
WordPress provides filters like
load_textdomain
andgettext
to allow overriding the path to translation files or individual translations.In WordPress 5.0.2 we added the following filters to allow filtering the behavior of
wp_set_script_translations()
so you can do the same for JavaScript translations. The following filters are available:pre_load_script_translations
: Pre-filters script translations for the given file, script handle and text domain. This way you can short-circuit the script translation logic to return your own translations.load_script_translation_file
: Filters the file path for loading script translations for the given script handle and text domain..load_script_translations
: Filters script translations for the given file, script handle and text domain. This way you can override translations after they have been loaded from the translation file.
In addition to that, pull request #12517 to the Gutenberg project aims to add
i18n.gettext
,i18n.gettext_with_context
,i18n.ngettext
, andi18n.ngettext_with_context
filters to the@wordpress/i18n
package. To override an individual translation, you could use them like this:wp.hooks.addFilter( 'i18n.gettext', 'myplugin/filter-gettext', function( translation, text, domain ) { if ( 'Source' === text && 'foo-domain' === domain ) { return 'New translation'; } return translation; } );
Code language: JavaScript (javascript)WordPress PHP Internationalization
With so many mentions of JavaScript in this post, you might be wondering if we also changed something on the PHP side of things. The answer to this is: no.
However, now is a good time to do some sort of I18N spring cleaning for your plugin or theme. Here is some helpful information for that:
- Make sure
Text Domain
is set in your main plugin file / theme stylesheet and that you use that very same text domain throughout the project. - If your WordPress plugin or theme is hosted on WordPress.org and requires WordPress 4.6 or higher (indicated via the
Tested up to
header in the readme), you don’t need to callload_plugin_textdomain()
in it. - You can run
wp i18n make-pot --debug
to see which of your translatable strings should be improved.
Further Reading
- Dev Note About New JavaScript Internationalization Support
- Internationalization Best Practices in the Plugin Developer Handbook
- Internationalization Section in the Gutenberg Handbook
Thanks to Omar Reiss, Gary Jones, and Dominik Schilling for their feedback and proofreading of this post.
- A new
-
The Text Domain in WordPress Internationalization
In this post I want to address a common question / misunderstanding about the role of the text domain when internationalizing WordPress plugins and themes. This topic has been addressed in the past, but it comes up again from time to time. Time to re-address it!
Some Background
Over the last few months I helped build and shape a new command for WP-CLI that makes it easier for developers to fully internationalize and localize their WordPress plugins and themes. It’s meant as a successor to the
makepot.php
script that tries to achieve the same and is the currently used by thousands of WordPress developers as well as the WordPress.org translation platform.Unfortunately,
makepot.php
is outdated, buggy, and not really future-proof (think JavaScript internationalization). That’s why I proposed replacing it with the new WP-CLI command on WordPress.org.By running
wp i18n make-pot /path/to/my/wordpress/wp-content/my-plugin
you can create a so-called translation catalog with the.pot
file extension. This catalog contains all the strings from your plugin that have been internationalized using the available gettext functions like__()
,_n()
, and_x()
.Check out the plugin developer handbook for a more thorough list of localization functions.
Where The Text Domain Comes Into Play
Let’s take
__( 'Translate me', 'my-plugin' )
as an example.The first argument of this function call is the actual text that should be translatable, the second argument is your text domain. One requirement for plugin developers is that the text domain must match the
slug
of the plugin.If your plugin is a single file called
my-plugin.php
or it is contained in a folder calledmy-plugin
, the text domain should bemy-plugin
. If your plugin is hosted on WordPress.org, it must be the slug of your plugin URL (wordpress.org/plugins/<slug>
).In the WP-CLI command we automatically try to guess your plugin’s slug (and thus the text domain) from the folder name. After that, it only extracts gettext calls with that text domain. Any other text domain will be ignored. This means it finds and extracts
__( 'Translate me', 'my-plugin' )
, but skips__( 'Translate me', 'another-plugin' )
.Don’t Repeat Yourself
Now, if you have lots of strings, you might want to save yourself some typing and use a variable or a constant instead of writing
'my-plugin'
every time. After all, repetition is bad and using a variable makes sure you don’t make any spelling mistakes.However, you’re actually still repeating the same variable over and over again, so you don’t really save any time. Also, variables are useful when a value needs to change. But the text domain of a plugin never really changes, especially when it is hosted on WordPress.org where you cannot change it once you’ve submitted the plugin.
If the text domain does change for whatever reason, you can do simple string replacements to make this change. There’s no need for a variable. Also, if you fear spelling mistakes, the WordPress Coding Standards for PHP_CodeSniffer has got you covered as they can detect incorrect text domains.
Most importantly, the WordPress plugin developer handbook explicitly forbids using variables for text domains:
Do not use variable names or constants for the text domain portion of a gettext function. Do not do this as a shortcut:
WordPress Plugin Handbook__( ‘Translate me.’ , $text_domain );
But why are variables not allowed as text domains? Let’s have a look at how this whole process works to better understand this.
How Localization Works in WordPress
Let’s say we have a WordPress site set up in German (
de_DE
) and running our plugin (my-plugin
) from the previous examples. When WordPress encounters a function call like__( 'Translate me', 'my-plugin' )
, the following happens:- If translations for that text domain have already been loaded, WordPress tries to translate the given string.
- If translations haven’t been loaded yet, WordPress looks for a file
my-plugin-de_DE.mo
in the folderwp-content/languages/plugins
and loads the translations from there if found.
Since all these PHP files are executed, we could actually use something like
__( ‘Translate me.’ , $text_domain );
. Given that$text_domain = 'my-plugin'
, this works exactly the same.String Extraction
To really answer the question of why variables as text domains are discouraged, we need to understand the process of how we actually get to this
plugin-de_DE.mo
file.It all starts with
wp i18n make-pot
(ormakepot.php
, for that matter).As mentioned before, that command looks for all instances of
__()
and the like in your plugin to extract translatable strings. During that process, the code isn’t executed, but only parsed. That means it has no idea what the value of$text_domain
is in__( 'Translate me', $text_domain )
. It just knows that it’s a variable.We could just as well omit the variable entirely and write
__( 'Translate me' )
as it provides no additional value. But can we?A closer look at the
makepot.php
script reveals that the second argument holding the text domain is actually completely ignored. Let’s say we have a plugin that’s hosted on WordPress.org and contains the following code:__( 'Translate me', 'my-plugin' ); __( 'Translate me too! Please?', $text_domain ); __( 'Translate me too!', MY_PLUGIN_TEXTDOMAIN );
Code language: PHP (php)In this case, all three strings will be extracted and made available for translation on translate.wordpress.org. This seems to support the theory that the text domain doesn’t need to be a string at all.
There is a caveat though.
Multiple Text Domains
Let’s say your plugin bundles a third-party library like TGM Plugin Activation. By default this library contains lots of gettext calls like
__( 'Install Plugins', 'tgmpa' )
. When runningmakepot.php
, this string would be extracted as well. However, TGMPA provides its own language files and everything, so you don’t want to duplicate efforts there.There’s no other way to solve this without limiting the string extraction to a specific text domain. And for this, the text domain needs to be a string, not a variable.
Note: You will also run into the these issues with tools like node-wp-i18n, as they use
makepot.php
under the hood. The same applies to Poedit, a popular translation software for WordPress projects. Since gettext wasn’t intended to be used with multiple domains inside a single project/file, thexgettext
command line utility doesn’t support limiting the text domain either.A similar situation arises when adding customized WooCommerce shop templates to your WordPress theme. Usually you don’t need to add these to your theme unless you really need to change the markup.
Since these templates are coming from the WooCommerce plugin, all localizable strings use the
woocommerce
text domain. And when you don’t change any of these strings you might consider just keeping the text domain so WordPress will still translate these.However, not changing the WooCommerce text domain is a bad idea. The reasons are simple:
- Strings with a different text domain than your theme’s might not be extracted in the future.
- It’s unreliable.
When WooCommerce changes its templates in a new version, your strings might suddenly not be localized anymore. - You take control away from users.
Users and translators have no way to translate your customized shop templates. - Context might change.
When you heavily customize the WooCommerce templates, some of the strings in them might not be 100% accurate anymore. At this point you have to rephrase and use your own text domain anyway.
For the same reasons you shouldn’t use WordPress core strings, without your project’s text domain, in your plugin or theme either.
Conclusion
To distinguish between strings coming from WordPress core and the different plugins and themes on your site, WordPress uses a so-called text domain.
While it might sound convenient to use a variable for the text domain in order to not repeat it all the time, there are some serious drawbacks to that method when a plugin or theme contains strings with multiple text domains.
As mentioned at the beginning of the article, I proposed replacing
makepot.php
on WordPress.org with the new WP-CLI command to extract strings from themes and plugins. If that proposed change is made, any string with a text domain that doesn’t match the project’s slug or isn’t a string literal will be ignored.However, this wouldn’t be an overnight change and we probably would soften that requirement in the beginning until all developers have caught up and fixed their text domains.
Nevertheless, if your plugin or theme is affected, you should make some changes today. Update your plugins and themes now to ensure all internationalized strings use a string literal text domain which matches the plugin’s slug, so that string extraction will continue to work for these in the future.
-
WordPress Internationalization Workflows
I had the exciting opportunity to hold a talk at this year’s WordCamp Tokyo about a topic that is dear to my heart: internationalization.
In particular, I talked about current internationalization workflows in WordPress and how things are changing. Amongst others, I covered things like Gutenberg with its focus on JavaScript internationalization, the new WP-CLI i18n-command, and Traduttore.
Some of these things are still under development in an attempt to make internationalizing and localizing WordPress projects faster and easier. Nonetheless, the talk is a good overview of the status quo.
The video is available on WordPress.tv, and the most recent version of my slides can be found on Speakerdeck: