Subscribe to Our Mailing List and Stay Up-to-Date! Subscribe

WordPress Plugin Internationalization: Making Plugins Translation-Ready

Making your WordPress plugin available in multiple languages opens doors to a global audience. Internationalization (i18n) prepares your code for translation, while localization (l10n) provides the actual translations. Let’s make your plugin speak every language.

Understanding i18n vs l10n

Internationalization (i18n): The process of preparing your plugin code to support multiple languages. This involves wrapping all user-facing strings in translation functions.

Localization (l10n): The process of translating those strings into specific languages. Translators can work on l10n without touching your code.

The “18” in i18n represents the 18 letters between “i” and “n” in internationalization. Similarly, l10n has 10 letters between “l” and “n”.

Setting Up Your Text Domain

Every plugin needs a unique text domain identifier. Define it in your plugin header:

/**
 * Plugin Name: My Awesome Plugin
 * Text Domain: my-awesome-plugin
 * Domain Path: /languages
 */

The text domain should match your plugin slug. Load translations in your main plugin file:

function myplugin_load_textdomain() {
    load_plugin_textdomain(
        'my-awesome-plugin',
        false,
        dirname(plugin_basename(__FILE__)) . '/languages'
    );
}
add_action('plugins_loaded', 'myplugin_load_textdomain');

This tells WordPress where to find translation files for your plugin.

Translation Functions

WordPress provides several functions for marking strings as translatable:

Basic Translation:

// Returns translated string
$text = __('Hello World', 'my-awesome-plugin');

// Echoes translated string
_e('Welcome to my plugin', 'my-awesome-plugin');

Translation with Context:

Use context when the same word might need different translations:

// "Post" as in a blog post
$label = _x('Post', 'noun', 'my-awesome-plugin');

// "Post" as in submit button
$button = _x('Post', 'verb', 'my-awesome-plugin');

Plural Forms:

Different languages handle plurals differently. Never hardcode plural logic:

// Wrong approach
$text = $count . ' item';
if ($count != 1) {
    $text .= 's';
}

// Correct approach
$text = sprintf(
    _n('%d item', '%d items', $count, 'my-awesome-plugin'),
    $count
);

Plural with Context:

$text = sprintf(
    _nx('%d product', '%d products', $count, 'e-commerce', 'my-awesome-plugin'),
    $count
);

Escaped Translation Functions

Always escape output for security. WordPress combines translation and escaping:

// For HTML content
echo esc_html__('Plugin Settings', 'my-awesome-plugin');
esc_html_e('Save Changes', 'my-awesome-plugin');

// For HTML attributes
$title = esc_attr__('Click to expand', 'my-awesome-plugin');
echo '<div title="' . esc_attr__('Help text', 'my-awesome-plugin') . '">';

// With context
$label = esc_html_x('Post', 'noun', 'my-awesome-plugin');

// For admin pages
echo '<h1>' . esc_html__('Dashboard', 'my-awesome-plugin') . '</h1>';

Translating Dynamic Content

Use placeholders for variable content:

// Single placeholder
$message = sprintf(
    __('Welcome, %s!', 'my-awesome-plugin'),
    $user_name
);

// Multiple placeholders with ordering
$message = sprintf(
    __('You have %1$d new messages and %2$d notifications', 'my-awesome-plugin'),
    $message_count,
    $notification_count
);

// Named placeholders (WordPress 5.5+)
$message = sprintf(
    __('Hello %(name)s, you have %(count)d messages', 'my-awesome-plugin'),
    array(
        'name' => $user_name,
        'count' => $message_count
    )
);

Translator Comments

Provide context for translators using special comments:

/* translators: %s: user display name */
$greeting = sprintf(__('Welcome back, %s', 'my-awesome-plugin'), $name);

/* translators: 1: number of posts, 2: post type name */
$status = sprintf(
    __('Found %1$d %2$s', 'my-awesome-plugin'),
    $count,
    $post_type
);

These comments appear in translation files, helping translators understand context.

Avoiding Common Mistakes

Never concatenate translatable strings:

// Wrong - breaks translation
echo __('Click', 'my-awesome-plugin') . ' ' .
     '<a href="#">' . __('here', 'my-awesome-plugin') . '</a>';

// Correct - translatable as complete phrase
printf(
    __('Click <a href="#">here</a>', 'my-awesome-plugin')
);

Don’t split sentences:

// Wrong
echo __('Total:', 'my-awesome-plugin') . ' ' . $count;

// Correct
printf(__('Total: %d', 'my-awesome-plugin'), $count);

Avoid hardcoded HTML in translations:

// Wrong
__('<strong>Important:</strong> Save your work', 'my-awesome-plugin');

// Better - allows translator to reorder
sprintf(
    __('%sImportant:%s Save your work', 'my-awesome-plugin'),
    '<strong>',
    '</strong>'
);

Creating POT Files

POT (Portable Object Template) files contain all translatable strings from your plugin. Generate them using WP-CLI:

wp i18n make-pot . languages/my-awesome-plugin.pot

Or use Poedit to scan your plugin directory. The POT file serves as the template for all language translations.

Translation File Structure

Organize translation files in a languages/ directory:

my-awesome-plugin/
├── languages/
│   ├── my-awesome-plugin.pot        (template)
│   ├── my-awesome-plugin-es_ES.po   (Spanish - editable)
│   ├── my-awesome-plugin-es_ES.mo   (Spanish - compiled)
│   ├── my-awesome-plugin-fr_FR.po   (French - editable)
│   └── my-awesome-plugin-fr_FR.mo   (French - compiled)

PO files contain the actual translations. MO files are compiled binary versions that WordPress loads.

Creating Translations with Poedit

  1. Open Poedit and select “Create New Translation”
  2. Choose your POT file as the source
  3. Select target language
  4. Translate strings in the interface
  5. Save as .po file (Poedit auto-generates .mo)

Poedit provides context from translator comments and shows source code references.

JavaScript Internationalization

Translate strings in JavaScript files:

// Enqueue script and make it translatable
function myplugin_enqueue_scripts() {
    wp_enqueue_script(
        'myplugin-frontend',
        plugins_url('js/frontend.js', __FILE__),
        array('jquery'),
        '1.0',
        true
    );

    wp_set_script_translations(
        'myplugin-frontend',
        'my-awesome-plugin',
        plugin_dir_path(__FILE__) . 'languages'
    );
}
add_action('wp_enqueue_scripts', 'myplugin_enqueue_scripts');

In your JavaScript file:

const { __ } = wp.i18n;

// Simple translation
const message = __("Click to continue", "my-awesome-plugin");

// With sprintf
const greeting = sprintf(__("Welcome, %s!", "my-awesome-plugin"), userName);

Generate JSON files for JavaScript translations:

wp i18n make-json languages --no-purge

Date and Time Localization

Use WordPress date functions for proper localization:

// WordPress handles localization automatically
echo date_i18n(get_option('date_format'), strtotime($date));

// Time format
echo date_i18n(get_option('time_format'), current_time('timestamp'));

// Custom format
echo date_i18n('F j, Y', strtotime($date));

Number Formatting

Use number_format_i18n() for locale-aware number formatting:

// Formats according to user's locale
echo number_format_i18n(1234.56); // 1,234.56 in English, 1.234,56 in German

// With specific decimals
echo number_format_i18n(1234.56789, 2); // 1,234.57

RTL Language Support

Add RTL stylesheet support for languages like Arabic and Hebrew:

function myplugin_enqueue_styles() {
    wp_enqueue_style('myplugin-style',
        plugins_url('css/style.css', __FILE__));

    // Load RTL stylesheet if needed
    if (is_rtl()) {
        wp_enqueue_style('myplugin-rtl',
            plugins_url('css/rtl.css', __FILE__));
    }
}
add_action('wp_enqueue_scripts', 'myplugin_enqueue_styles');

Or use automatic RTL generation:

wp_style_add_data('myplugin-style', 'rtl', 'replace');

Testing Translations

Test your plugin in different languages:

  1. Change WordPress language in Settings > General
  2. Install a language pack from Settings > General
  3. Use a plugin like Loco Translate for quick testing
  4. Verify all strings are translatable
  5. Check RTL layout if supporting those languages

WordPress.org Translation Platform

For plugins hosted on WordPress.org:

  1. Submit your plugin with POT file included
  2. Translators use translate.wordpress.org
  3. Language packs are automatically generated
  4. Users receive translations through WordPress updates

Contributors can help translate at: https://translate.wordpress.org/projects/wp-plugins/your-plugin-slug

Translation Workflow Best Practices

During Development:

  • Wrap all user-facing strings immediately
  • Use consistent text domain throughout
  • Add translator comments for context
  • Never use variables as text domain

Before Release:

  • Generate POT file
  • Test with at least one translation
  • Verify all strings are translatable
  • Check for missing text domains

After Release:

  • Update POT file with each version
  • Notify translators of new strings
  • Keep existing string IDs stable
  • Provide context for new additions

Performance Considerations

Translation loading has minimal performance impact when done correctly:

// Load translations only when needed
if (is_admin()) {
    load_plugin_textdomain('my-awesome-plugin', false, dirname(plugin_basename(__FILE__)) . '/languages');
}

// Cache expensive translations
$translated = wp_cache_get('myplugin_cached_string', 'myplugin');
if (false === $translated) {
    $translated = __('Expensive string', 'my-awesome-plugin');
    wp_cache_set('myplugin_cached_string', $translated, 'myplugin', HOUR_IN_SECONDS);
}

Complete Example

Here’s a complete translatable plugin snippet:

function myplugin_display_stats($user_id) {
    $posts = count_user_posts($user_id);
    $comments = get_comments(array('user_id' => $user_id, 'count' => true));

    $output = '<div class="user-stats">';

    /* translators: %s: user display name */
    $output .= '<h3>' . sprintf(
        esc_html__('Statistics for %s', 'my-awesome-plugin'),
        get_userdata($user_id)->display_name
    ) . '</h3>';

    /* translators: %d: number of posts */
    $output .= '<p>' . sprintf(
        esc_html(_n('%d post published', '%d posts published', $posts, 'my-awesome-plugin')),
        number_format_i18n($posts)
    ) . '</p>';

    /* translators: %d: number of comments */
    $output .= '<p>' . sprintf(
        esc_html(_n('%d comment written', '%d comments written', $comments, 'my-awesome-plugin')),
        number_format_i18n($comments)
    ) . '</p>';

    $output .= '</div>';

    return $output;
}

Internationalization ensures your plugin reaches users worldwide. With proper i18n implementation, translators can easily localize your plugin without modifying a single line of code.

  1. Internationalization Documentation
  2. I18n for WordPress Developers
  3. translate.wordpress.org
  4. Poedit Translation Tool
  5. WP-CLI i18n Commands

Call to Action

Supercharge your development! ACF Copilot Pro generates ACF field groups with AI, exports to PHP, and accelerates custom field workflows—try it free!