WordPress Internationalization
Introduction
Internationalization (often abbreviated as i18n - "i", 18 letters, "n") is the process of designing your WordPress themes and plugins so they can be easily translated into different languages. This is a crucial aspect of WordPress development if you want your product to reach a global audience.
In this guide, we'll learn how to internationalize WordPress themes and plugins, making them translation-ready. We'll explore WordPress's built-in functions for text translation, handle date/time formats, and understand the complete workflow from internationalization to localization.
Why Internationalization Matters
WordPress powers over 40% of all websites on the internet, with users spanning the globe speaking hundreds of different languages. By internationalizing your WordPress products, you:
- Make your theme/plugin accessible to non-English speaking users
- Increase your potential market and user base
- Follow WordPress best practices and standards
- Demonstrate professionalism in your development approach
The Difference Between i18n and l10n
Before diving deeper, let's clarify two commonly confused terms:
- Internationalization (i18n): The process of preparing your code for translation (what developers do)
- Localization (l10n): The actual translation process (what translators do)
As developers, our primary focus will be on internationalization - making our code ready for translation.
WordPress Text Domain
A text domain is a unique identifier that ensures WordPress can distinguish between different translations. It's typically the name of your theme or plugin.
For example:
// In a plugin called "awesome-plugin"
__('Welcome to my plugin', 'awesome-plugin');
The text domain should be:
- All lowercase
- Use hyphens between words
- Match your theme/plugin slug
Basic Translation Functions
WordPress provides several functions for making strings translatable:
1. Basic Translation Functions
__()
- Translate a string
// Basic usage
$translated_text = __('Hello World', 'my-text-domain');
echo $translated_text;
// Output (before translation): Hello World
// Output (after Spanish translation): Hola Mundo
_e()
- Translate and echo a string
// This translates and immediately outputs the text
_e('Hello World', 'my-text-domain');
// Output (before translation): Hello World
// Output (after Spanish translation): Hola Mundo
2. Functions with Context
Sometimes, the same word in English might translate differently based on context. For these cases, use:
_x()
- Translate with context
// "Post" could be a noun or verb depending on context
$post_noun = _x('Post', 'noun', 'my-text-domain');
$post_verb = _x('Post', 'verb', 'my-text-domain');
echo $post_noun; // Might translate to "Article" in some languages
echo $post_verb; // Might translate to "Publish" in some languages
_ex()
- Translate with context and echo
_ex('Book', 'noun', 'my-text-domain'); // As in "a book to read"
_ex('Book', 'verb', 'my-text-domain'); // As in "to book a reservation"
3. Functions for Plurals
_n()
- Singular and plural forms
$count = 5;
printf(
_n(
'You have %d notification',
'You have %d notifications',
$count,
'my-text-domain'
),
$count
);
// Output (when $count = 1): You have 1 notification
// Output (when $count = 5): You have 5 notifications
_nx()
- Singular and plural forms with context
$count = 3;
printf(
_nx(
'%d comment',
'%d comments',
$count,
'Comment count',
'my-text-domain'
),
$count
);
Loading Text Domains
For WordPress to find your translations, you need to load your text domain:
For Plugins
function my_plugin_load_textdomain() {
load_plugin_textdomain(
'my-plugin',
false,
dirname(plugin_basename(__FILE__)) . '/languages/'
);
}
add_action('plugins_loaded', 'my_plugin_load_textdomain');
For Themes
function my_theme_load_textdomain() {
load_theme_textdomain(
'my-theme',
get_template_directory() . '/languages'
);
}
add_action('after_setup_theme', 'my_theme_load_textdomain');
Escaping Translated Strings
Always escape output, even with translated text:
// Correct usage for HTML output
echo esc_html__('Hello World', 'my-text-domain');
// For attributes
echo '<a title="' . esc_attr__('Visit Homepage', 'my-text-domain') . '">';
// For URLs
echo esc_url(__('https://example.com', 'my-text-domain'));
WordPress provides specific escaping functions for translations:
esc_html__()
andesc_html_e()
- Escape and translate for HTMLesc_attr__()
andesc_attr_e()
- Escape and translate for HTML attributes
Internationalizing Variables
When working with variables in translatable strings, you have two approaches:
Using printf() or sprintf()
// Correct approach
$username = 'John';
printf(
/* translators: %s: username */
__('Welcome back, %s!', 'my-text-domain'),
$username
);
// Output: Welcome back, John!
For multiple variables
$first_name = 'John';
$last_name = 'Doe';
printf(
/* translators: %1$s: first name, %2$s: last name */
__('Welcome, %1$s %2$s!', 'my-text-domain'),
$first_name,
$last_name
);
// Output: Welcome, John Doe!
Localizing JavaScript
For JavaScript files, you need to use the wp_localize_script()
function:
function my_enqueue_scripts() {
wp_enqueue_script(
'my-script',
get_template_directory_uri() . '/js/script.js',
array('jquery'),
'1.0.0',
true
);
wp_localize_script(
'my-script',
'myPluginL10n',
array(
'welcome' => __('Welcome to my website', 'my-text-domain'),
'goodbye' => __('Thank you for visiting', 'my-text-domain'),
'error' => __('An error occurred', 'my-text-domain'),
)
);
}
add_action('wp_enqueue_scripts', 'my_enqueue_scripts');
Then in your JavaScript file:
// Using the localized strings
console.log(myPluginL10n.welcome);
alert(myPluginL10n.error);
// Output depends on the active language translation
Date and Time Internationalization
WordPress handles date/time formatting based on the locale:
// Outputs date formatted according to locale settings
echo date_i18n(
get_option('date_format')
);
// Custom format based on locale
echo date_i18n(
__('F j, Y', 'my-text-domain')
);
Translation Ready Checklist
- All user-facing strings are wrapped in translation functions
- Text domain is properly defined and consistent
- Text domain is loaded correctly
- Translation files are in the correct directory
- Comments for translators are added to complex strings
- JavaScript strings are properly localized
- Date/time strings use the
date_i18n()
function
Creating POT Files
The POT (Portable Object Template) file is the template for translations. You can generate it using WP-CLI or tools like Poedit.
Using WP-CLI:
wp i18n make-pot /path/to/your/project /path/to/your/project/languages/my-text-domain.pot --domain=my-text-domain
Real-world Example: Internationalizing a Contact Form
Let's see a complete example of internationalizing a simple contact form:
/**
* Display a contact form with internationalized strings
*/
function display_contact_form() {
$output = '<form id="contact-form" method="post">';
// Form title
$output .= '<h2>' . esc_html__('Contact Us', 'my-text-domain') . '</h2>';
// Name field
$output .= '<div class="form-group">';
$output .= '<label for="name">' . esc_html__('Your Name', 'my-text-domain') . '</label>';
$output .= '<input type="text" id="name" name="name" required>';
$output .= '</div>';
// Email field
$output .= '<div class="form-group">';
$output .= '<label for="email">' . esc_html__('Your Email', 'my-text-domain') . '</label>';
$output .= '<input type="email" id="email" name="email" required>';
$output .= '</div>';
// Message field
$output .= '<div class="form-group">';
$output .= '<label for="message">' . esc_html__('Your Message', 'my-text-domain') . '</label>';
$output .= '<textarea id="message" name="message" rows="5" required></textarea>';
$output .= '</div>';
// Submit button with dynamic count
$submissions = get_option('contact_form_submissions', 0);
$output .= '<button type="submit">';
$output .= sprintf(
_n(
'Send Message (%d submission today)',
'Send Message (%d submissions today)',
$submissions,
'my-text-domain'
),
$submissions
);
$output .= '</button>';
$output .= '</form>';
// Display terms notice with a link
$output .= '<p class="form-notice">';
$output .= sprintf(
/* translators: %s: URL to terms page */
__('By submitting this form, you agree to our <a href="%s">terms and conditions</a>.', 'my-text-domain'),
esc_url(get_permalink(get_page_by_path('terms')))
);
$output .= '</p>';
return $output;
}
The Full Translation Process
Here's a visualization of the complete translation workflow:
Summary
Internationalization is a crucial aspect of professional WordPress development. By following the practices outlined in this guide, you can ensure your themes and plugins are accessible to a global audience:
- Use WordPress translation functions like
__()
and_e()
- Always include a text domain with your translations
- Properly load the text domain in your theme or plugin
- Create translator comments for complex strings
- Use proper escaping functions for security
- Handle plurals with
_n()
and_nx()
- Localize JavaScript strings using
wp_localize_script()
- Use
date_i18n()
for date and time formatting
By making internationalization a standard part of your development workflow, you'll create more inclusive and globally accessible WordPress products.
Additional Resources
- WordPress Developer Documentation on i18n
- WordPress Coding Standards: Internationalization
- Poedit - A popular translation file editor
- Loco Translate - WordPress plugin for translation
Exercises
- Take an existing plugin or theme and identify all user-facing strings that should be internationalized.
- Create a simple plugin with internationalized strings and generate the POT file.
- Create translations for your plugin in at least one language.
- Practice using different translation functions like
_n()
and_x()
to understand their practical applications. - Internationalize a JavaScript file and test it with different language settings.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)