How to Make Your WordPress Theme or Plugin Translation-Ready

Making a theme or plugin translation-ready means wrapping every user-facing string in one of WordPress’s i18n functions. When done correctly, translators can provide .po/.mo files without touching any code.

Problem: How do you make a WordPress theme or plugin ready for translation into other languages?

Solution: Wrap all user-facing strings in __(), _e(), or esc_html__() with your text domain, load the domain in after_setup_theme (themes) or plugins_loaded (plugins), and generate a .pot file for translators.

The core functions you need:

// Basic string — use for labels, headings, button text
__( 'Read more', 'textdomain' )

// Echo directly
_e( 'Read more', 'textdomain' )

// Singular/plural form
sprintf(
    _n( '%d comment', '%d comments', $count, 'textdomain' ),
    $count
)

// String with context (disambiguates identical strings with different meanings)
_x( 'Draft', 'post status', 'textdomain' )

// Safe output in attributes — escaping + translation in one call
esc_attr_e( 'Search...', 'textdomain' )
esc_html_e( 'Submit',    'textdomain' )

Load the text domain in functions.php (theme) or the main plugin file:

// Theme
add_action( 'after_setup_theme', function() {
    load_theme_textdomain( 'textdomain', get_template_directory() . '/languages' );
} );

// Plugin
add_action( 'init', function() {
    load_plugin_textdomain( 'textdomain', false, dirname( plugin_basename( __FILE__ ) ) . '/languages' );
} );

Generate a .pot template file for translators using WP-CLI:

wp i18n make-pot . languages/textdomain.pot --domain=textdomain

NOTE: Never concatenate translated strings to form sentences — word order differs across languages. Instead, use sprintf() with named placeholders so translators can reorder them: sprintf( __( 'Posted by %s on %s', 'td' ), $author, $date ).