Resolving Broken localization strings and incorrect text domains Bypassing Common Theme Conflicts in Multi-Language Site Networks
Diagnosing Localization String Issues: The `gettext` Workflow
When localization strings fail to appear correctly on a multi-language WordPress site, especially within a network of sites, the root cause often lies in how WordPress’s `gettext` system interacts with themes, plugins, and the WordPress core. The `gettext` system is the de facto standard for internationalization (i18n) and localization (l10n) in PHP applications, including WordPress. It relies on a set of functions like __, _e, _n, _x, and esc_html__() to mark strings for translation. These strings are then extracted into Portable Object (PO) files, compiled into Machine Object (MO) files, and loaded by WordPress based on the active language.
A common symptom is seeing untranslated strings, strings from the wrong language, or strings that are simply not present in the generated PO files. This often points to issues with the text domain declaration or conflicts in how themes and plugins register their translatable strings.
Understanding Text Domains and Their Scope
Every translatable string in WordPress is associated with a text domain. This text domain acts as a unique identifier, allowing the translation system to distinguish strings from different plugins, themes, or WordPress core. For a string to be correctly translated, it must be wrapped in a `gettext` function and declared with the correct text domain. The text domain is typically the slug of the plugin or theme (e.g., my-plugin, my-theme).
The text domain is crucial for several reasons:
- Uniqueness: Prevents translation collisions between different software components.
- File Loading: WordPress uses the text domain to locate the corresponding MO file (e.g.,
wp-content/languages/plugins/my-plugin-fr_FR.moorwp-content/themes/my-theme/languages/fr_FR.mo). - Context: Helps translators understand the origin and context of a string.
In a WordPress multisite network, each site can have its own language settings. However, the actual translation files are typically managed at the network level or within individual plugin/theme installations. If a text domain is declared incorrectly or inconsistently across sites or within the code, it can lead to strings not being found or being loaded from the wrong language files.
Identifying Incorrect Text Domain Declarations
The most common mistake is using a generic text domain (like default or wordpress) for custom strings within a theme or plugin, or failing to declare the text domain at all. Another frequent issue is using a text domain that doesn’t match the actual slug of the plugin or theme, leading to WordPress being unable to find the correct language files.
Let’s examine a typical scenario within a custom plugin. A plugin might have its main file named my-custom-plugin.php. The correct text domain for this plugin would be my-custom-plugin.
Consider this incorrect example:
// Incorrect: Using a generic text domain __( 'My Custom Setting', 'default' ); _e( 'Save Changes', 'wordpress' );
And here’s the correct way to declare strings within that plugin:
// Correct: Using the plugin's text domain __( 'My Custom Setting', 'my-custom-plugin' ); _e( 'Save Changes', 'my-custom-plugin' );
Similarly, for themes, if the theme’s directory is my-awesome-theme, the text domain should be my-awesome-theme.
// Correct for a theme: __( 'Welcome to our site!', 'my-awesome-theme' );
The Role of `load_plugin_textdomain` and `load_theme_textdomain`
To ensure WordPress can find and load your translation files, you must explicitly load the text domain. This is done using the load_plugin_textdomain function for plugins and load_theme_textdomain for themes. These functions should be hooked into the appropriate action, typically plugins_loaded for plugins and after_setup_theme for themes.
For Plugins:
/**
* Load plugin textdomain.
*/
function my_custom_plugin_load_textdomain() {
load_plugin_textdomain( 'my-custom-plugin', false, dirname( plugin_basename( __FILE__ ) ) . '/languages/' );
}
add_action( 'plugins_loaded', 'my_custom_plugin_load_textdomain' );
In this example:
'my-custom-plugin'is the text domain.falseindicates that the text domain is not a network-activated plugin (for network-activated plugins, this would betrue).dirname( plugin_basename( __FILE__ ) ) . '/languages/'specifies the relative path to the directory containing the MO files (e.g.,wp-content/plugins/my-custom-plugin/languages/).
For Themes:
/**
* Load theme textdomain.
*/
function my_awesome_theme_load_textdomain() {
load_theme_textdomain( 'my-awesome-theme', get_template_directory() . '/languages' );
}
add_action( 'after_setup_theme', 'my_awesome_theme_load_textdomain' );
Here:
'my-awesome-theme'is the text domain.get_template_directory() . '/languages'points to the theme’s language directory (e.g.,wp-content/themes/my-awesome-theme/languages/).
Crucially, the text domain used in load_plugin_textdomain/load_theme_textdomain MUST match the text domain used when wrapping strings with gettext functions.
Troubleshooting Theme Conflicts in Multisite
Multisite environments introduce complexity. Themes and plugins can be activated network-wide or on a per-site basis. This can lead to conflicts if different sites within the network are using different versions of a theme or plugin, or if language files are not consistently available.
Common Conflict Scenarios:
- Multiple Plugins with Same Text Domain: If two plugins (or a plugin and the theme) declare the same text domain and attempt to load their language files from different locations, the last one loaded might overwrite the settings or prevent the other from loading correctly.
- Incorrect Language File Paths: The path specified in
load_plugin_textdomainorload_theme_textdomainmight be incorrect relative to the plugin/theme’s main file or the WordPress root. - Network vs. Site-Specific Activation: A plugin activated network-wide might expect language files in a specific location (e.g.,
wp-content/languages/plugins/), while a site-specific activation might look within the plugin’s own directory. - Theme Switching: If a user switches themes on a subsite, the localization strings from the previous theme will disappear unless the new theme also correctly declares and loads its text domain.
Diagnostic Steps for Broken Localization
When faced with broken localization, a systematic approach is key:
1. Verify Text Domain Consistency
Action: Search your theme and all active plugins (especially those on the affected subsite) for the load_plugin_textdomain and load_theme_textdomain calls. Ensure the text domain string passed to these functions is consistent and matches the expected slug of the component.
# Example command using grep to find text domain declarations in a theme directory
grep -r "load_theme_textdomain(" wp-content/themes/your-theme-slug/
# Example command using grep to find text domain declarations in a plugin directory
grep -r "load_plugin_textdomain(" wp-content/plugins/your-plugin-slug/
Also, search for all instances of __(', _e(', _n(', etc., and verify that the second argument (the text domain) is correct and matches the text domain declared in the `load_…_textdomain` function.
# Find all gettext calls in a theme and check their text domains
grep -rE "__\(|_e\(|_n\(" wp-content/themes/your-theme-slug/ --color=auto | less
# Filter for specific text domains
grep -rE "__\(.*'my-theme-slug'\)|_e\(.*'my-theme-slug'\)" wp-content/themes/your-theme-slug/
2. Inspect Language File Paths and Existence
Action: Check the path specified in the load_..._textdomain function. Navigate to that directory on your server and verify that the correct MO file exists for the language of the affected site (e.g., fr_FR.mo). The naming convention is typically {text-domain}-{locale}.mo.
For example, if your text domain is my-plugin and the site’s locale is fr_FR, you should find a file like:
wp-content/plugins/my-plugin/languages/my-plugin-fr_FR.mo- OR (for network-wide languages)
wp-content/languages/plugins/my-plugin-fr_FR.mo
If the MO file is missing, you’ll need to generate it from the corresponding PO file (which is usually generated by tools like WP-CLI’s i18n make-pot or Poedit). Ensure the PO file itself is also present and correctly formatted.
# Example: Check if the MO file exists for a plugin ls wp-content/plugins/my-plugin/languages/my-plugin-fr_FR.mo ls wp-content/languages/plugins/my-plugin-fr_FR.mo # Example: Check if the MO file exists for a theme ls wp-content/themes/my-theme/languages/my-theme-fr_FR.mo ls wp-content/languages/themes/my-theme-fr_FR.mo
3. Isolate the Conflict
Action: Temporarily deactivate other plugins one by one on the affected subsite to see if the localization issue resolves. If it does, the last deactivated plugin is likely the culprit, possibly due to a text domain conflict or an override.
If deactivating plugins doesn’t help, try switching to a default WordPress theme (like Twenty Twenty-Three) on the affected subsite. If the strings appear correctly with a default theme, the issue lies within your custom theme.
4. Utilize WP-CLI for Debugging
WP-CLI is an invaluable tool for managing WordPress sites, including localization tasks.
Commands to Use:
- Generate POT files: This helps verify that your strings are being correctly identified and marked for translation.
# For a plugin wp i18n make-pot . languages/my-plugin.pot --slug=my-plugin --domain=my-plugin # For a theme wp i18n make-pot . languages/my-theme.pot --slug=my-theme --domain=my-theme
Inspect the generated .pot file. It’s a PO template file that lists all translatable strings. If your strings are missing, the `gettext` calls in your code are likely incorrect or using the wrong text domain.
Action: Check the contents of the generated POT file. Ensure your strings are present and associated with the correct text domain.
# Example snippet from a POT file msgid "My Custom Setting" msgstr "" "Project-Id-Version: My Plugin 1.0.0\n" "POT-Creation-Date: 2023-10-27 10:00+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Language-Team: LANGUAGE <[email protected]>\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Generated-By: WP-CLI i18n make-pot\n" #: path/to/your/plugin/file.php:123 msgid "My Custom Setting" msgstr ""
5. Network Language Settings
Action: In a multisite network, navigate to Network Admin > Settings > Network Language. Ensure the correct network language is selected. While this doesn’t directly fix theme/plugin localization, it sets the default language for the network, which can influence fallback mechanisms.
Also, check Network Admin > Plugins and Network Admin > Themes to see which are activated network-wide. This impacts where WordPress might look for language files (e.g., wp-content/languages/ for network-activated items).
Advanced Considerations: Custom Languages and Overrides
In rare cases, you might encounter issues with custom language codes or complex override scenarios. WordPress’s localization system is robust but can be tripped up by non-standard configurations.
If you’re using custom locales (e.g., en_US-custom), ensure these are correctly registered and that your MO files are named and placed accordingly. The standard WordPress localization functions might not automatically pick these up without additional hooks or filters.
Furthermore, be aware of plugins that might hook into the localization process to override or modify translations. While powerful, these can also introduce unexpected behavior if not configured correctly.
By systematically checking text domain declarations, language file paths, and isolating conflicts, you can effectively resolve most broken localization string issues on your multi-language WordPress multisite network.