SponsoredHosinger Special Offer

WP+ WordPress Plugin Development

Build WordPress plugins from scratch: actions and filters, shortcodes, settings pages, custom post types, meta boxes, AJAX, REST endpoints, custom tables, cron and blocks.

Translating a Plugin

WordPress Plugin Development Lesson 24 of 27 ~2 min read

Overview

Internationalising a plugin means wrapping every user-facing string in a translation function with your text domain, so the plugin can be translated into any language — for PHP strings, JavaScript strings and block metadata alike.

PHP strings

FunctionUse
__()Return a translated string
esc_html__() / esc_attr__()Return, translated and escaped
esc_html_e() / esc_attr_e()Print, translated and escaped
_x()Disambiguate: "Book" (noun) vs "Book" (verb)
_n()Singular and plural forms
<?php
printf(
    /* translators: 1: number of books, 2: genre name */
    esc_html( _n( '%1$s book in %2$s', '%1$s books in %2$s', $count, 'gdc-books' ) ),
    esc_html( number_format_i18n( $count ) ),
    esc_html( $genre_name )
);
Watch out: The text domain must be a literal string — 'gdc-books' — never a variable or a constant. Translation tools read your code as text and cannot resolve a constant.

Loading translations

For plugins hosted on WordPress.org, translations from translate.wordpress.org load automatically as long as the Text Domain header matches the plugin slug. To also ship your own .mo files, load them on init:

<?php
function gdc_books_load_textdomain() {
    load_plugin_textdomain( 'gdc-books', false, dirname( plugin_basename( __FILE__ ) ) . '/languages' );
}
add_action( 'init', 'gdc_books_load_textdomain' );
Tip: Do not call translation functions before init — for example in a constant definition at the top of the main file. Since WordPress 6.7 that triggers a "translation loading was triggered too early" notice, because the user's language is not known yet.

JavaScript strings

import { __, sprintf, _n } from '@wordpress/i18n';

const label = __( 'Add to favourites', 'gdc-books' );

const summary = sprintf(
    /* translators: %d: number of books */
    _n( '%d book saved', '%d books saved', count, 'gdc-books' ),
    count
);

Then tell WordPress where the JavaScript translations live, right after enqueuing the script:

<?php
wp_set_script_translations( 'gdc-books-favourites', 'gdc-books', GDC_BOOKS_PATH . 'languages' );

Blocks registered from block.json with a textdomain field get this automatically.

Generating the .pot file

# From the plugin folder, with WP-CLI installed
wp i18n make-pot . languages/gdc-books.pot --exclude=node_modules,vendor,blocks/book-list/src

# After translators deliver .po files, build the JS translation files
wp i18n make-json languages --no-purge

Practice

  • Wrap every string in GDC Books, including admin notices and REST error messages.
  • Generate gdc-books.pot and check that strings from PHP, JavaScript and block.json all appear.
  • Switch your site language in Settings → General and confirm WordPress's own strings change while yours wait for a translation.
Create a free account to save which lessons you've finished. Save my progress