title |
---|
WP-i18n |
i18n (Internationalization) in WordPress refers to the process of making themes, plugins, and core functionality translation-ready so they can support multiple languages. WordPress provides built-in functions and tools for localizing (l10n) content dynamically based on a user's language settings.
- We should use the gettext translation functions for all text.
- We should keep language files updated as adding/changing said text.
- Any text that is shown to a user or customer needs to be translatable.
- i18n (Internationalization) involves preparing code for translations, read the i18n for developers WordPress docs.
- l10n (Localization) is the subsequent process of translating an internationalized plugin by providing translations for supported locales.
- Functions are available natively for WordPress in PHP and (Since WordPress 5.0) Javascript equivalents in the
@wordpress/i18n
package. - Each theme or plugin should have a unique text domain to group translations.
- Use consistent
text-domain
in a project. A plugin should be consistent and always use the sametext-domain
. - Modules should manage translations in their own
text-domain
.
- Use consistent
There are functions available for perparing code for translation. These must be used in order to take advantage of the translation features of WordPress. There are basic functions for most simple text strings, more advanced for instances that involve variables, numeration, or html. We should not concatenate translated strings to construct phrases because sentence structure changes in different languages. In these cases we can use methods like printf
to construct the phrases. Here are some quick examples:
Method | Description | Docs |
---|---|---|
__( 'Text', 'text-domain' ) |
Basic i18n function that retrieves the translation of 'Text' in the specified text-domain. | __() |
_e( 'Text', 'text-domain' ) |
Equivalent of echo __() |
_e() |
_n( '%s person', '%s people', $count, 'text-domain' ) |
Translates and retrieves the singular or plural form based on the supplied number. | _n() |
printf( __( 'Search results for: %s', 'text-domain' ), $search_query ) |
Add variables as placeholders to the strings. | |
printf( __( 'Your city is %1$s, and your zip code is %2$s.', 'text-domain' ), $city, $zipcode ) |
You can use printf for multiple variables too. |
Best practices:
- Do not use variable names or constants for the text-domain portion of a gettext function.
- When in doubt, add a clarifying comment in the source code for translators. It has to start with the words translators: and be the last PHP comment before the gettext call (either in the same line or in the line immediately before). It will be pulled into the file and viewable to translators.
- Do not leave leading or trailing whitespace in a translatable phrase. This will be detected as a unique string and leads to duplication.
- You must add your Text domain as an argument to every
__()
,_e()
, etc, gettext call, or your translations won’t work. - Avoid changing strings if it is not needed, because this affects multiple i18n files.
Translation files are traditionally stored in a languages
directory at the root of the project. Find one in plugins and modules that are set up with translations.
All the strings that use the i18n methods will be added to a project .pot
file. They get translated and each language has it's own .po
file. This is used to generate an .mo
file for each language which is used by php, and a .json
file used by the js. As we add any new strings to our projects or change existing strings, these should be picked up by the .pot
file and make their way into each translation as we get new translated strings. When changing a string the associated translations will need to be updated too.
.pot
this file lists all translation strings found in the project found for the specifiedtext-domain
. This is the source of the.po
files..po
files are created specific for each language and follow a specific naming convention. These are human-readable files which list translation strings for each original string..mo
files are machine-optimized and generated from the.po
files and are binary files that WordPress uses to match translations to original strings on the fly in PHP. The translation functions essentially find the translation match for the speficied string based on the user's chosen language. These machine object files have better performance than php finding matches in a.po
file..l10n.php
files are equivalents of the.mo
as of WordPress 6.5. With recent updates, these files are more performant that.mo
files, and human-readable. Wherever possible we're transitioning to these filetypes in place of the.mo
files (which can still be used a fallbacks on WordPress versions older than 6.5, but since we don't require suppoting any versions older than that, we can phase these files out as redundant.).json
files are used by Javascript translation methods in the same way the.mo
files are used in PHP. They should be generated from the.po
files and not manually edited even though they are human-readable.- Note: The
make-json
command appends a hash to the filename. This is expected and used as a versioning system. - There will be one
.json
language file per javascript file in use. In cases where a build generates one js file there is one, but when there are multilple build destination files or assets, there may be more than one.json
file. Ideally we aren't creating translation files for source files though, and we can specify directories to exclude in ourmake-pot
command.
- Note: The
As we've set a text-domain, it must be loaded so WordPress knows where the (.mo
) translation files can be found. load_plugin_textdomain
sets this in PHP and load_script_textdomain
for Javascript. The plugins are set up with this step already, see bluehost here.
The translations in PHP "just work." There are no extra steps to get the translation when the proper translation methods are used and the text domain is set and used.
Translations for scripts require one more step however. The script handle and the text-domain need to be set via wp_set_script_translations
. The first parameter is the script handle, the second is the text-domain and the third is the directory that houses the .json
translation files (languages). Learn more about this set script translations step in the docs or see how we're doing it in the bluehost plugin. This step needs to be done for each text-domain, so when a module loads a script file, it should probably also set the script translation. This allows the @wordpress/i18n
library to match the original strings passed into the translation methods to be matched to the appropriatly translated string.
Script translations may also require matching the files with hashed filenames. See the load_script_translation_file
filter in the help-center module.
There are WP-CLI commands that will help us manage these translation files and strings. See the WP-CLI i18n docs.
make-pot
will create or update a.pot
file.make-po
will create a.po
file from the.pot
file.update-po
updates and existing.po
file, pulling any new strings from the.pot
file.make-mo
creates an.mo
file from a.po
file.make-php
creates al10n.php
file from a.po
file.make-json
creates a.json
file from a.po
file.
In the plugins (and some modules) we have a composer command that runs all the required i18n commands together. composer run i18n
will first update the .pot
, then update .po
with any new strings, generate the .mo
and .json
files. These file changes need to be committed as they are released with the plugin. They should be updated at least for every release version bump. The files are checked in each release to make sure any strings are updated (and ideally, the translations updated with them) and the command is run automatically in the set-version-bump
script.
Engineers need to make sure to use the translation functions for any text strings and if strings change in a PR, also update the translations for that string accordingly. As we add/edit strings, request new translations, we can even use AI or google translate to get something in place for the time being. We have a global team of engineers, and multiple folks who speak these languages, so let's not be afraid to ask for human help, beyond AI and Google translate, a human will detect local nuances and meanings to give the translations the correct voice and tone.
All plugins are set up for translations. They have a composer command to update translation files and this is run every release to keep them updated. We'll need to keep an eye on any changed strings to ensure they have a translation. Ideally we have a workflow that blocks a release or PR without sufficient translations.
All modules with user facing text should have strings set up for translations. By the nature of the modular system behind our plugin, the plugin cannot be fully translated unless all modules are translated. The modules with a build step should also load the text-domains and set the script translations wherever the modules assets are managed.
Currently, some modules that contain javascript components that are directly consumed by the plugin. Here the component has been set up to receive text via props so the plugin can manage the text translations (as is the case for hostgator). The module has default text in place (as the staging module does). In these cases, however, the module should manage the text translations. A module specific text-domain should be set up and the defaults need to be set up as translatable.
See details about our Crowdin workflow for translations.
- crowdin.com online interface to manage translation strings (integrates with AI) - we have a Newfold account.
- PoEdit app to download to manage translation strings.
- Google translate for manaully translating strings.