PTC

WordPress Plugin Translations Not Showing? Fix Missing Translations

Translations missing after a PTC run? Four checks find almost every cause. Check the string reached PTC, the .mo filename matches what WordPress expects, and the text domain is consistent across your code. This guide walks each check in the order to run them.

The same diagnostic order works whether you translated with PTC (Private Translation Cloud), GlotPress, or any other gettext-based tool. The failure modes are inherent to the WordPress i18n pipeline, not to any particular translator.

Confirm the string reached PTC

The first question is whether the string is in your PTC project. If a string was not extracted into your .pot file, PTC never saw it. The .po file has no translation for it. The running plugin shows the source-language fallback.

How to check:

  1. Open the affected string in the running plugin and copy its exact text.
  2. Go to your PTC dashboard, open the Translations tab, and search for the string.
  3. If it is not there, PTC never received it.

Three common causes follow.

Cause 1: the string is missing from your POT file

wp i18n make-pot only extracts strings inside the recognized i18n functions. That means __(), _e(), _n(), _x(), and the escaped variants, with the right text domain. Strings inside the wrong domain are skipped. Hardcoded strings outside an i18n function are skipped.

Re-run extraction and verify the string is now in the .pot:

wp i18n make-pot . languages/my-plugin.pot --domain=my-plugin
grep "Save Changes" languages/my-plugin.pot

If grep returns the string with an msgid, the extraction works. Re-upload the new .pot to PTC and re-translate. If grep returns nothing, the string is hardcoded and is not wrapped in __(). That is your bug. Wrap the string and re-extract.

Cause 2: the text is not wrapped in a gettext function

Every translatable string in your code needs a wrapper. Use __(), esc_html__(), _e(), _n(), or _x() with the correct text domain:

__( 'Hello, world!', 'your-plugin' );

Cause 3: JavaScript strings were not marked as translatable

If your plugin includes translatable text in JavaScript:

  1. In the PTC dashboard, go to Settings > Monitored Files.
  2. Edit the relevant resource file and enable “Is this a WordPress project with localizable JavaScript?”

This tells PTC to scan JavaScript files for translatable strings. PTC regenerates translations and opens a new merge request with the updated files.

Then make sure WordPress knows where to load the .json translation files PTC produced. Call wp_set_script_translations() with the correct path:

// For plugins
wp_set_script_translations(
    'script-handle',
    'text-domain',
    plugin_dir_path( __FILE__ ) . 'languages'
);

// For themes
wp_set_script_translations(
    'script-handle',
    'text-domain',
    get_template_directory() . '/languages'
);

This tells WordPress to load .json files from your plugin or theme folder.

Confirm WordPress is loading your .mo file

If the string is in PTC and the .mo file exists in your plugin's languages/ directory but the rendered plugin still shows English, WordPress is failing to load the .mo.

Diagnostic 1: is the site locale set correctly? Go to Settings > General > Site Language in wp-admin and confirm it matches your target language. For example, set it to “Español” to test Spanish. If the site is in English, your Spanish .mo will never load. This is not a bug.

Diagnostic 2: is load_plugin_textdomain() actually running? Add a temporary debug line:

add_action( 'init', function() {
    $loaded = load_plugin_textdomain(
        'my-plugin',
        false,
        dirname( plugin_basename( __FILE__ ) ) . '/languages'
    );
    error_log( 'my-plugin textdomain loaded: ' . var_export( $loaded, true ) );
} );

Reload a page and check your PHP error log. loaded: true means WordPress found and loaded a .mo file for the current locale. loaded: false means it did not. The cause is usually a filename mismatch (see the next section) or a wrong directory path.

Diagnostic 3: is the hook timing right? load_plugin_textdomain() must run on the init hook. That is the canonical hook for translations to apply to strings echoed during normal request processing.

Diagnostic 4: are community translations overriding your bundled files? By default, WordPress prioritizes translation files stored in /wp-content/languages/. If your plugin or theme has community-provided translations on WordPress.org, those files can override the .mo files bundled with your project. To prevent this, use the load_textdomain_mofile filter to force-load your bundled file path before WordPress checks the global directory. Multi-text-domain projects (a plugin that embeds another plugin) need each domain loaded separately. Each domain needs its own load_plugin_textdomain() or load_theme_textdomain() call.

Make sure the MO filename matches the WordPress lookup pattern

WordPress's .mo file lookup follows a strict naming convention. Even one character off and WordPress silently falls back to English.

The convention depends on where you place the file:

Location Pattern Example
Plugin's /languages/ {text-domain}-{locale}.mo my-plugin-de_DE.mo
Theme's /languages/ {locale}.mo de_DE.mo
Global dir (/wp-content/languages/...) {text-domain}-{locale}.mo my-theme-de_DE.mo

A few worked examples:

  • my-plugin-es_ES.mo for Spanish (Spain)
  • my-plugin-fr_FR.mo for French (France)
  • my-plugin-pt_BR.mo for Portuguese (Brazil)
  • my-plugin-zh_CN.mo for Simplified Chinese
  • my-plugin-ja.mo for Japanese (some locales have no region suffix)

Common mistakes:

  • Wrong text-domain prefix. If your plugin's Text Domain header is my-plugin but the .mo is named myplugin-es_ES.mo (no hyphen), WordPress will not find it. The prefix must match the Text Domain value exactly.
  • Wrong locale code. es.mo instead of es_ES.mo. WordPress uses regional locale codes. es_ES, es_MX, and es_AR are different files. A bare language code only works when no regional file exists.
  • Hyphen vs underscore. Locale codes use underscore (es_ES), never hyphen (es-ES). This trips up developers who copy locale codes from browser Accept-Language headers or BCP 47 sources, where hyphen is standard.
  • Wrong directory. The Domain Path header must point at the directory holding your .mo files. If Domain Path is /languages and your .mo files are in /lang/, WordPress will not find them.

Check by listing the files PTC produced against the locale your site is using:

ls plugins/my-plugin/languages/
# my-plugin-es_ES.mo
# my-plugin-es_ES.po
# my-plugin-fr_FR.mo
# my-plugin-fr_FR.po

If the names look right but translations still do not load, run the load_plugin_textdomain() diagnostic above to confirm WordPress is finding the directory.

Check that your text domain is consistent across your code

This is the silent killer. Every __(), _e(), _n(), _x() call in your code passes a text domain as its last argument. If even one call passes the wrong domain, that one string will not be translated. Every other string in the plugin will work fine.

// Correct - matches the plugin's Text Domain
$correct = __( 'Save Changes', 'my-plugin' );

// Wrong - text domain typo; this string is never translated
$wrong = __( 'Save Changes', 'myplugin' );  // missing hyphen

// Wrong - copy-pasted from another plugin
$wrong = __( 'Save Changes', 'other-plugin' );

// Wrong - WordPress core domain; works for core strings but not yours
$wrong = __( 'Save Changes', 'default' );

Find inconsistencies by grepping your codebase:

grep -rE "__\(|_e\(|_n\(|_x\(" --include="*.php" .

Look at the last argument of every match. They should all be your plugin's text domain. If you find typos or copy-paste errors, fix them and re-run wp i18n make-pot to refresh the .pot.

For JavaScript code using @wordpress/i18n, the same rule applies. wp_set_script_translations() in PHP must also pass the matching domain:

wp_set_script_translations( 'my-plugin-editor', 'my-plugin', '...' );
//                                                ^^^^^^^^^^^
//                                                Must match the JS calls

Catch the same issues before they ship with AI Visual QA

The diagnostic loop above is reactive. A translation is missing, you go hunting for the cause. PTC's AI Visual QA catches the same issues before they ship. It inspects the rendered plugin in every target language after every translation update and reports back what is missing or wrong.

For a hardcoded English string outside __() (Cause 1 above), AI Visual QA sees the untranslated string in the rendered plugin. PTC generates a ready-to-paste prompt for Cursor or Claude Code to wrap the string in __(). For a text-domain mismatch (the previous section's check), it sees the string untranslated when other strings on the same screen are translated, and reports the inconsistency.

If you have not enabled AI Visual QA yet, see the WordPress themes and plugins tutorial for setup.

When the four checks all pass and translations are still missing

If you have run all four checks and translations still are not showing, email PTC support with:

  • Your plugin's text domain.
  • One of the .mo filenames (e.g., my-plugin-es_ES.mo).
  • A screenshot of the PTC dashboard showing the affected string with its translation.
  • The site language setting from Settings > General.

Most “missing translation” cases collapse to one of the causes above. Edge cases (specific WordPress versions, MultiSite locale resolution, conflict with another i18n plugin) sometimes need a second look.

Keep future releases in sync with continuous localization

Once your translations load correctly, set up the continuous localization workflow so future releases stay in sync with your translations - automatically, on every push to main.