Internationalization
Making every user-facing string translatable, and the files that carry the translations
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.
Internationalization (i18n) guidelines
- We should use the gettext translation functions for all text.
- We should keep language files updated as we add or change 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/i18npackage. - Each theme or plugin should have a unique text domain to group translations.
- Use a consistent
text-domainin a project. A plugin should be consistent and always use the sametext-domain. - Modules should manage translations in their own
text-domain.
- Use a consistent
Translation functions
There are functions available for preparing 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, and more advanced ones 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 on the same line or on 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 (l10n)
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 its 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.
.potlists all translation strings found in the project for the specifiedtext-domain. This is the source of the.pofiles..pofiles are created specific to each language and follow a specific naming convention. These are human-readable files which list translation strings for each original string..mofiles are machine-optimized and generated from the.pofiles. They 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 specified string based on the user’s chosen language. These machine object files have better performance than PHP finding matches in a.pofile..l10n.phpfiles are equivalents of the.moas of WordPress 6.5. With recent updates, these files are more performant than.mofiles, and human-readable. Wherever possible we’re transitioning to these filetypes in place of the.mofiles (which can still be used as fallbacks on WordPress versions older than 6.5, but since we don’t require supporting any versions older than that, we can phase these files out as redundant)..jsonfiles are used by JavaScript translation methods in the same way the.mofiles are used in PHP. They should be generated from the.pofiles and not manually edited even though they are human-readable.- Note: The
make-jsoncommand appends a hash to the filename. This is expected and used as a versioning system. - There will be one
.jsonlanguage file per JavaScript file in use. In cases where a build generates one JS file there is one, but when there are multiple build destination files or assets, there may be more than one.jsonfile. Ideally we aren’t creating translation files for source files though, and we can specify directories to exclude in ourmake-potcommand.
- Note: The
Translation programming setup
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 the appropriately 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.
i18n CLI commands
There are WP-CLI commands that will help us manage these translation files and strings. See the WP-CLI i18n docs.
make-potwill create or update a.potfile.make-powill create a.pofile from the.potfile.update-poupdates an existing.pofile, pulling any new strings from the.potfile.make-mocreates an.mofile from a.pofile.make-phpcreates anl10n.phpfile from a.pofile.make-jsoncreates a.jsonfile from a.pofile.
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, and 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 and edit strings and 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.
Plugin translations
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.
Module translations
All modules with user-facing text should have strings set up for translations. By the nature of the modular system behind our plugins, 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 module’s assets are managed.
Currently, some modules 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.
Tools
- 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 manually translating strings.
Related
-
Crowdin translation workflow
Uploading source strings to Crowdin and getting translations back as a pull request
-
AI translation workflow
Automating po and json translation through Azure AI Translator and a central workflow
-
WordPress PHP
PHPCS setup, namespacing, bootstrapping and the defensive habits we expect in PHP
-
WordPress JavaScript
How JavaScript in WordPress differs from a standard build, and how to work with it