WordPress powers websites in dozens of languages, and users worldwide expect your plugins to support their language. WordPress plugin i18n testing translation validation ensures that your translations are complete, accurate, and function correctly across different languages. Yet many developers treat internationalization as an afterthought, discovering translation issues only when multilingual users report broken strings or missing translations. This comprehensive guide covers the complete i18n testing workflow: setting up translation files, validating translations with WP-CLI, testing with Poedit, integrating with GlotPress, and catching common translation bugs before deployment.
Table of Contents
- Understanding Plugin Internationalization
- Setting Up Translation Infrastructure
- Validating Strings with WP-CLI
- Using Poedit for Translation Testing
- GlotPress Integration and Community Translations
- Automated i18n Validation
- Common Translation Bugs
- Frequently Asked Questions
Understanding Plugin Internationalization
Internationalization (i18n) is the process of making code language-agnostic, while localization (l10n) is adapting translated content for specific languages and regions. A properly internationalized plugin displays its interface strings from translation files rather than hardcoding them, allowing anyone to provide translations.
WordPress plugin i18n testing translation requires understanding the complete flow:
- Source code contains original strings wrapped in
__(),_e(), or_x()functions - Text domain identifies which plugin owns which strings (prevents conflicts)
- POT file is the source translation template listing all translatable strings
- PO files contain translations for specific languages (e.g., es_ES.po for Spanish)
- MO files are compiled, binary versions of PO files that WordPress loads
- Translation plugins like WPML or Polylang provide user interfaces for switching languages
Many developers understand steps 1-2 but skip 3-6, leading to untranslated strings or broken translations in production.
Setting Up Translation Infrastructure
Begin by declaring your text domain in your plugin header:
<?php
/**
* Plugin Name: My Awesome Plugin
* Plugin URI: https://example.com/my-plugin
* Description: Does awesome things
* Version: 1.0.0
* Text Domain: my-awesome-plugin
* Domain Path: /languages
* Requires at least: 5.0
* Tested up to: 6.4
*/
// Load translations
add_action( 'init', function() {
load_plugin_textdomain(
'my-awesome-plugin',
false,
dirname( plugin_basename( __FILE__ ) ) . '/languages'
);
} );
The text domain my-awesome-plugin must be unique across all plugins on your site. Use your plugin slug consistently.
Create a /languages directory in your plugin root. This directory holds all translation files:
my-awesome-plugin/
├── my-awesome-plugin.php
├── languages/
│ ├── my-awesome-plugin.pot (source template)
│ ├── es_ES.po (Spanish)
│ ├── es_ES.mo (Spanish compiled)
│ ├── fr_FR.po (French)
│ └── fr_FR.mo (French compiled)
└── src/
Now mark all user-facing strings for translation:
// Simple string - retrieves translated version
echo __( 'Hello World', 'my-awesome-plugin' );
// Simple string - displays translated version
_e( 'Save Settings', 'my-awesome-plugin' );
// String with context (disambiguates)
_x( 'Post', 'noun', 'my-awesome-plugin' );
// Plural handling
printf(
_n( '%d item', '%d items', $count, 'my-awesome-plugin' ),
$count
);
// String attributes - for HTML attributes
$title = esc_attr__( 'Click to expand', 'my-awesome-plugin' );
// Sprintf with translation
sprintf(
__( 'Welcome, %s!', 'my-awesome-plugin' ),
esc_html( $user_name )
);
Never hardcode user-facing strings:
// WRONG - Not translatable
echo "Hello World";
echo '<button>' . $button_text . '</button>';
// RIGHT - Translatable
echo __( 'Hello World', 'my-awesome-plugin' );
echo '<button>' . esc_html__( $button_text, 'my-awesome-plugin' ) . '</button>';
Validating Strings with WP-CLI
WP-CLI provides powerful commands for generating POT files and validating translations:
# Install WP-CLI (if not already installed)
curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar
chmod +x wp-cli.phar
sudo mv wp-cli.phar /usr/local/bin/wp
# Navigate to WordPress installation
cd /path/to/wordpress
# Generate POT file from your plugin
wp i18n make-pot /path/to/my-awesome-plugin languages/my-awesome-plugin.pot --domain=my-awesome-plugin
# Validate translation files
wp i18n validate-pot languages/my-awesome-plugin.pot
# Create translation file from template
wp i18n make-json languages/my-awesome-plugin.pot --target=languages/es_ES.po
# Check for issues in translations
wp i18n validate-po languages/es_ES.po
The make-pot command scans your PHP code for translatable strings and generates a POT template. Let's create a more automated workflow:
#!/bin/bash
# generate-translations.sh
PLUGIN_DIR="$(pwd)"
PLUGIN_SLUG=$(basename "$PLUGIN_DIR")
DOMAIN="$PLUGIN_SLUG"
LANGUAGES_DIR="$PLUGIN_DIR/languages"
echo "Generating POT file for $DOMAIN..."
# Create languages directory if it doesn't exist
mkdir -p "$LANGUAGES_DIR"
# Generate POT file
wp i18n make-pot "$PLUGIN_DIR" "$LANGUAGES_DIR/$DOMAIN.pot" \
--domain="$DOMAIN" \
--exclude=node_modules,vendor,tests \
--include="**/*.php" \
--allow-root
if [ $? -eq 0 ]; then
echo "✓ POT file generated successfully"
# Validate POT file
echo "Validating POT file..."
wp i18n validate-pot "$LANGUAGES_DIR/$DOMAIN.pot" --allow-root
if [ $? -eq 0 ]; then
echo "✓ POT file is valid"
# Update existing PO files
for po_file in "$LANGUAGES_DIR"/*.po; do
if [ -f "$po_file" ]; then
lang=$(basename "$po_file" .po)
echo "Updating $lang.po..."
msgmerge --update "$po_file" "$LANGUAGES_DIR/$DOMAIN.pot"
# Compile to MO
msgfmt "$po_file" -o "${po_file%.po}.mo"
echo "✓ Compiled ${po_file%.po}.mo"
fi
done
echo "✓ All translations updated"
else
echo "✗ POT file validation failed"
exit 1
fi
else
echo "✗ Failed to generate POT file"
exit 1
fi
Run this script before each release to ensure POT and PO files are in sync:
chmod +x generate-translations.sh
./generate-translations.sh
Using Poedit for Translation Testing
Poedit is the most popular desktop application for editing translation files. It provides a visual interface and validates translations automatically.
Download Poedit from https://poedit.net. Open your POT file in Poedit:
- File → Open → select
my-awesome-plugin.pot - Edit → Project Properties
- Set source code charset (UTF-8)
- Add languages you want to support
- Save the project
Poedit displays each translatable string:
English (source): "Save Settings"
[Translation field] [empty or translated text]
[Status indicator] [✓ = translated, ! = needs work]
For each string, you can:
- Provide translations
- Add context or notes
- Mark strings as fuzzy if translation needs review
- Add developer comments
- View plural forms
Poedit validates translations in real-time:
- Detects mismatched string substitutions (%s, %d, etc.)
- Warns about inconsistent terminology
- Checks for encoding issues
- Validates PO file syntax
Generate language-specific PO files from your POT:
# Using Poedit GUI:
File → New Catalog from POT File → select my-awesome-plugin.pot
Then set language and save as es_ES.po
# Or using msgmerge:
msgmerge --blank es_ES.po my-awesome-plugin.pot > es_ES_updated.po
After translating, compile to MO:
# Poedit does this automatically, or use command line:
msgfmt es_ES.po -o es_ES.mo
GlotPress Integration and Community Translations
GlotPress is the community translation platform used by WordPress.org. If you want community contributors to translate your plugin, register your plugin on wordpress.org and enable GlotPress.
First, ensure your plugin is on wordpress.org and your text domain matches your plugin slug. Then:
- Visit https://translate.wordpress.org/
- Find your plugin in the list
- Enable translation for your plugin
- Upload your POT file
- Community members can then provide translations
To automatically pull translations from GlotPress into your plugin:
#!/bin/bash
# update-glotpress-translations.sh
PLUGIN_SLUG="my-awesome-plugin"
DOMAIN="$PLUGIN_SLUG"
LANGUAGES_DIR="./languages"
GLOTPRESS_API="https://translate.wordpress.org/api/projects/plugins/$PLUGIN_SLUG"
mkdir -p "$LANGUAGES_DIR"
# Get list of available translations
translations=$(curl -s "$GLOTPRESS_API" | grep -oP '"language_code":"\K[^"]+')
for lang in $translations; do
echo "Downloading $lang translation..."
# Download PO file
curl -s -o "$LANGUAGES_DIR/$lang.po" \
"$GLOTPRESS_API/$lang/default/export-translations"
# Compile to MO
if command -v msgfmt &> /dev/null; then
msgfmt "$LANGUAGES_DIR/$lang.po" -o "$LANGUAGES_DIR/$lang.mo"
echo "✓ Compiled $lang.mo"
fi
done
echo "✓ All translations updated from GlotPress"
Add this to your release workflow to automatically include community translations.
Automated i18n Validation
Catch translation issues in CI/CD pipelines before deployment:
# .github/workflows/i18n-validation.yml
name: i18n Validation
on: [push, pull_request]
jobs:
i18n:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.2'
- name: Install WP-CLI
run: |
curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar
chmod +x wp-cli.phar
sudo mv wp-cli.phar /usr/local/bin/wp
- name: Install gettext tools
run: sudo apt-get install -y gettext
- name: Generate POT file
run: |
wp i18n make-pot . languages/my-awesome-plugin.pot \
--domain=my-awesome-plugin \
--exclude=node_modules,vendor,tests,build
- name: Validate POT file
run: wp i18n validate-pot languages/my-awesome-plugin.pot
- name: Validate PO files
run: |
for po_file in languages/*.po; do
echo "Validating $po_file..."
wp i18n validate-po "$po_file"
done
- name: Check for untranslated strings
run: |
untranslated=0
for po_file in languages/*.po; do
count=$(msggrep -c '^\s*msgstr ""\s*$' "$po_file")
if [ "$count" -gt 0 ]; then
echo "Warning: $po_file has $count untranslated strings"
untranslated=$((untranslated + count))
fi
done
if [ "$untranslated" -gt 0 ]; then
echo "Total untranslated: $untranslated"
fi
- name: Create test WordPress install
run: |
wp core download --path=/tmp/wordpress
wp config create --dbname=test --dbuser=root --path=/tmp/wordpress
wp db create --path=/tmp/wordpress
wp core install --url=http://localhost --title=Test --admin_user=admin --admin_password=password [email protected] --path=/tmp/wordpress
- name: Test translation loading
run: |
cp -r . /tmp/wordpress/wp-content/plugins/my-awesome-plugin
wp plugin activate my-awesome-plugin --path=/tmp/wordpress
wp shell --path=/tmp/wordpress << 'EOF'
load_plugin_textdomain('my-awesome-plugin');
echo __('Save Settings', 'my-awesome-plugin');
EOF
This workflow ensures POT files are valid, PO files compile correctly, and translations load in WordPress.
Are your plugin translations complete and valid? WP HealthKit's plugin auditing system identifies missing i18n functions, untranslated strings, and translation infrastructure issues. Upload your plugin to get detailed internationalization audit results and recommendations for improving translation support.
Common Translation Bugs
Even careful developers introduce translation bugs. Here are the most common issues and how to avoid them:
Missing text domain - the most common bug:
// WRONG - No text domain
__( 'This string is not translatable across the site' )
// RIGHT - Text domain specified
__( 'This string is translatable', 'my-awesome-plugin' )
Variable text domain - translators can't extract strings:
// WRONG - Dynamic text domain
$domain = 'my-awesome-plugin';
__( 'Untranslatable', $domain )
// RIGHT - Literal text domain
__( 'Translatable', 'my-awesome-plugin' )
Complex string concatenation:
// WRONG - Translators can't see the full string
echo __( 'Hello', 'my-awesome-plugin' ) . ' ' . $name;
// RIGHT - Full string is translatable
printf( __( 'Hello, %s!', 'my-awesome-plugin' ), $name );
Missing sanitization with translation:
// WRONG - XSS vulnerability
echo __( $user_input, 'my-awesome-plugin' );
// RIGHT - Sanitized output
echo esc_html__( $user_input, 'my-awesome-plugin' );
// Or better yet, validate before translation
$allowed = array( 'hello', 'goodbye', 'thanks' );
if ( in_array( $user_input, $allowed ) ) {
echo __( $user_input, 'my-awesome-plugin' );
}
Incorrect plural handling:
// WRONG - Plural forms won't work in other languages
if ( $count === 1 ) {
echo __( 'One item', 'my-awesome-plugin' );
} else {
echo __( 'Multiple items', 'my-awesome-plugin' );
}
// RIGHT - Uses WordPress plural handling
_n( '%d item', '%d items', $count, 'my-awesome-plugin' )
// WRONG - Doesn't interpolate count
_n( 'One item', 'Many items', $count, 'my-awesome-plugin' )
// RIGHT - Count is interpolated
printf( _n( '%d item', '%d items', $count, 'my-awesome-plugin' ), $count )
Inconsistent terminology:
// WRONG - Same concept, different strings
__( 'Save', 'my-awesome-plugin' ) // Button
__( 'Saving', 'my-awesome-plugin' ) // Status
__( 'Saved', 'my-awesome-plugin' ) // Confirmation
// These will have different translations, causing confusion
// RIGHT - Use context to ensure consistency
_x( 'Save', 'button', 'my-awesome-plugin' )
_x( 'Save', 'status', 'my-awesome-plugin' )
_x( 'Save', 'confirmation', 'my-awesome-plugin' )
// Translators see the context and provide consistent translations
Hardcoded direction for RTL languages:
// WRONG - Assumes LTR layout
echo '<div style="float: left;">Menu</div>';
// RIGHT - Uses WordPress direction handling
echo '<div style="float: ' . ( is_rtl() ? 'right' : 'left' ) . ';">Menu</div>';
// Or use RTL-aware CSS
echo '<div class="menu-item">Menu</div>';
// Then in CSS: .menu-item { float: left; } [dir="rtl"] .menu-item { float: right; }
Fuzzy translations in MO files:
# Poedit marks translations as "fuzzy" when it's unsure
# Fuzzy strings are ignored by WordPress
# Before releasing, remove fuzzy markings:
# View fuzzy strings
msggrep -F fuzzy languages/es_ES.po
# Remove fuzzy flag
sed -i 's/#, fuzzy//' languages/es_ES.po
# Recompile
msgfmt languages/es_ES.po -o languages/es_ES.mo
Additional Resources
Internationalization (i18n) testing is often neglected because it's not required for a plugin to function in English. Many developers build plugins that work perfectly in English but have problems in other languages due to improper i18n implementation. The problems often don't surface until users in other countries start using the plugin and report that text isn't translating correctly, plural handling is broken, or the user interface looks broken with translated text.
The core issue is that i18n implementation seems optional when you're only using English. Your plugin works fine without it, so developers skip proper implementation and focus on features. But proper i18n implementation from day one is far easier than retrofitting it later. And when you do it properly, your plugin instantly becomes usable worldwide, opening it to millions of potential users who don't speak English.
Real-world i18n problems are subtle and often go unnoticed until translations exist. A hardcoded string won't translate. A plural form handled incorrectly will produce "1 items" instead of "1 item". Dates formatted wrong will confuse users. These problems create a poor experience for users in other languages, leading to poor ratings and negative reviews. By implementing i18n properly from the start and validating it systematically, you ensure that your plugin provides an excellent experience regardless of the user's language.
Frequently Asked Questions
What's the difference between POT, PO, and MO files?
POT is the template containing only source strings (English). PO files contain translations for specific languages—they're human-editable text files. MO files are compiled binary versions of PO files that WordPress actually loads. Always commit POT and PO files to version control, but regenerate MO files during builds.
Should I include translation files in my plugin distribution?
Only include MO files (compiled translations). POT and PO files are for translators and can be large. Generate MO files during your build/release process so distribution packages are lean. Some developers exclude all translation files and pull them from GlotPress at runtime.
How do I test my translations in WordPress before release?
Use language switcher plugins or add this to wp-config.php:
define( 'WPLANG', 'es_ES' ); // Test Spanish
Then verify that translatable strings appear in the translated language. Use Query Monitor or browser DevTools to confirm correct text domain is loaded.
What if my plugin has many contexts for the same string?
Use _x() for strings with context:
_x( 'Read', 'verb', 'my-awesome-plugin' ) // Read a book
_x( 'Read', 'status', 'my-awesome-plugin' ) // Status: read
Each context gets its own translation entry, allowing accurate localization even for strings with multiple meanings.
Can I provide partial translations?
Yes. Untranslated strings automatically fall back to the source language. However, incomplete translations provide a poor user experience. Consider requiring 80-90% translation before enabling a language.
How do I handle translating plugin settings and custom post types?
Register custom post types and settings with proper labels:
register_post_type( 'my-custom-type', array(
'label' => __( 'Custom Type', 'my-awesome-plugin' ),
'labels' => array(
'all_items' => __( 'All Items', 'my-awesome-plugin' ),
'new_item' => __( 'New Item', 'my-awesome-plugin' ),
),
) );
register_setting( 'my-group', 'my-setting', array(
'label' => __( 'My Setting', 'my-awesome-plugin' ),
) );
The labels are automatically scanned by make-pot and included in your POT file.
WP HealthKit automates this entire process across its 17 verification layers, catching issues that manual review would miss. Whether you're a solo developer or managing an agency portfolio, automated scanning saves hours of manual review time.
Testing Internationalization Thoroughly
I18n testing requires checking translation strings are properly extracted, testing the plugin in multiple languages, and verifying that UI layouts work with translated text. Automated tools can verify that strings are properly wrapped in translation functions, but manual testing reveals UI issues that automated tools can't detect.
Translation strings might be technically correct but produce poor UX when translated. An English phrase might be 20 characters but translate to 80 characters in another language. A label might have room for 15 characters in English but need 40 in Spanish. If you don't test actual translations, you might ship interfaces that break visually in other languages.
Additionally, different languages have different pluralization rules. English uses "1 item" vs "2 items". Polish uses different rules entirely. Your plugin must handle pluralization correctly for the languages you support. Using proper WordPress translation functions (esc_html__(), _n()) handles these complexities, but you must test that plural forms work correctly in each language.
The best approach is having native speakers test your plugin in their languages. They find issues you'd never discover in English. They catch untranslatable terms, poor terminology choices, and UI layout problems. By involving translators in testing, you ensure your plugin provides excellent experience worldwide.
Internationalization Tools and Services
WordPress provides translation infrastructure through translate.wordpress.org. By registering your plugin there, you enable volunteer translators to contribute translations. This removes the burden of translation from you while enabling people worldwide to use your plugin.
Tools like GlotPress and PoEdit help manage translation strings. GlotPress integrates with WordPress, allowing collaborative translation. PoEdit provides a visual editor for .po files used by translators. Using these tools streamlines the translation process.
Additionally, consider providing a glossary of terms specific to your plugin. Some terms don't translate directly and might confuse translators. By documenting how certain terms should be translated, you ensure consistency and accuracy across all languages. This small effort dramatically improves translation quality.
Pluralization Rules and Language-Specific Grammar
Different languages handle pluralization entirely differently. English has simple singular and plural forms, but Polish has four plural forms, Russian has three, and some languages have context-based rules that depend on the number ending. WordPress's _n() function supports this through plural handling in translation files, but many developers hardcode English pluralization logic into their code. This becomes glaringly obvious when you test a plugin with Hebrew or Arabic, which flow right-to-left and have completely different number agreements. WP HealthKit's i18n validation specifically checks for hardcoded pluralization that fails in non-English contexts.
Testing Tools and Automation Approaches
Genuine i18n testing requires either native speakers or automated tools that check for common mistakes like missing text domains or inconsistent string keys. Many developers test with a dummy translation file that simply rotates characters to make every string obviously translated, revealing layout issues immediately. This technique catches problems where translations are significantly longer or shorter than English, which breaks carefully tuned layouts. Continuous integration can enforce that no new code is committed without proper i18n tags, preventing technical debt accumulation.
Conclusion
WordPress plugin i18n testing translation validation is an essential part of professional plugin development. By setting up proper text domains, using WP-CLI to maintain translation files, testing with Poedit, integrating with GlotPress, and validating translations in CI/CD pipelines, you ensure your plugins work seamlessly for users worldwide.
The most important principle is treating internationalization as a core feature, not an afterthought. From the first line of code, wrap user-facing strings in translation functions. Automate POT generation and validation. Test translations before each release. Support community translators through GlotPress.
Make plugin internationalization a standard part of your development workflow. Use WP HealthKit to audit your plugins for i18n issues and ensure translation infrastructure is properly configured. Start your comprehensive plugin audit today to get detailed internationalization recommendations and validation results.
For additional context, review our guide on plugin internationalization and translations, explore our open source contributions, and visit our plugin ecosystem audit features for complete plugin quality analysis.
Building Globally Accessible Plugins
Internationalization isn't just about translation—it's about respecting a global audience. By implementing i18n properly, testing in multiple languages, and supporting translators, you make your plugin accessible worldwide. Users in every country deserve to use plugins in their native languages. Your plugin's success depends partly on being usable globally. By prioritizing internationalization, you open your plugin to millions of potential users who would otherwise be unable or unwilling to use English-language plugins. The effort is worthwhile for the reach and impact it provides. WP HealthKit checks that your plugin is properly internationalized, verifying that user-facing strings are wrapped in translation functions, that translation files exist, and that the plugin can be successfully translated. Our analysis identifies strings that should be translatable but aren't, and reports potential translation issues.
By implementing proper i18n from the start, you open your plugin to global users. Translation is often an afterthought, but planning for it upfront makes internationalization seamless. Users worldwide deserve to use your plugin in their native languages. By prioritizing i18n, you honor that.
Upload your plugin to WP HealthKit to verify proper internationalization and identify translation opportunities. Internationalization is often treated as optional because plugins work fine in English without it. But this thinking limits your plugin's reach. Millions of people worldwide don't use WordPress in English. They want to use your plugin in their native languages. By implementing proper i18n and enabling translations, you open your plugin to global users. Translation services make this easier than ever—you don't need to translate yourself. You simply make translation possible for others. This simple decision multiplies your plugin's potential audience. Test your plugin with languages that have different character sets, text lengths, and number formats. Arabic and Hebrew use right-to-left text, requiring special layout consideration. Chinese and Japanese use many more characters for the same concept, affecting UI layout. Internationalization opens your plugin globally. Translation-ready plugins reach millions more users.