How to Translate a WordPress Theme or Plugin

WordPress has a built-in localisation system based on GNU gettext. The workflow: wrap translatable strings in __() or _e(), generate a .pot template file, create language-specific .po files, compile them to binary .mo files, and load them in your theme or plugin.

Problem: How do you make a WordPress theme or plugin fully translatable so that all user-facing strings can be converted into other languages without modifying the source code?

Solution: Wrap all strings in __(), _e(), or esc_html__() with your text domain, generate a .pot template with WP-CLI (wp i18n make-pot), load the domain in after_setup_theme or plugins_loaded, and provide translated .po/.mo files for each language.

Step 1 — wrap strings in translation functions:

// __() returns the translated string
$label = __( 'Read more', 'my-theme' );

// _e() echoes the translated string
_e( 'Posted on', 'my-theme' );

// esc_html__() returns a translated and HTML-escaped string
echo esc_html__( 'Submit', 'my-theme' );

// Load the theme's text domain
add_action( 'after_setup_theme', function() {
    load_theme_textdomain( 'my-theme', get_template_directory() . '/languages' );
} );

Step 2 — generate the .pot template file using WP-CLI:

wp i18n make-pot . languages/my-theme.pot --domain=my-theme

Step 3 — create and compile a translation. Open the .pot file in Poedit, translate the strings, and save — Poedit creates both the .po and .mo files automatically. Place them in the languages/ folder:

languages/
├── my-theme.pot          # template (commit to Git)
├── my-theme-zh_CN.po     # Chinese source (commit to Git)
├── my-theme-zh_CN.mo     # compiled binary (can be gitignored)
└── my-theme-ja.po

For a non-standard locale (e.g. per-URL language switching), load the text domain with a specific locale via the locale filter:

add_filter( 'locale', function( $locale ) {
    if ( ! is_admin() && strpos( $_SERVER['REQUEST_URI'], '/zh-hans' ) !== false ) {
        return 'zh_CN';
    }
    return $locale;
} );

NOTE: The .mo (machine object) file is the binary that PHP reads at runtime. The .po (portable object) file is the human-editable source. Always keep both in sync — commit the .po to version control and regenerate the .mo from it. Tools like Loco Translate let editors update translations from the WordPress admin without touching the command line.