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.potOr 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
- Open Poedit and select “Create New Translation”
- Choose your POT file as the source
- Select target language
- Translate strings in the interface
- Save as
.pofile (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-purgeDate 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.57RTL 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:
- Change WordPress language in Settings > General
- Install a language pack from Settings > General
- Use a plugin like Loco Translate for quick testing
- Verify all strings are translatable
- Check RTL layout if supporting those languages
WordPress.org Translation Platform
For plugins hosted on WordPress.org:
- Submit your plugin with POT file included
- Translators use translate.wordpress.org
- Language packs are automatically generated
- 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.
External Links
- Internationalization Documentation
- I18n for WordPress Developers
- translate.wordpress.org
- Poedit Translation Tool
- 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!

