| name | wordpress-i18n |
| description | This skill should be used when the user asks to "internationalize a plugin", "add translations", "set up i18n", "create a POT file", "translate strings", "load text domain", "use wp_set_script_translations", "add translator comments", or mentions "i18n", "l10n", "gettext", "text domain", "translation", "localization", "multilingual", "Poedit", "wp i18n make-pot", "wp_localize_script", "wp_set_script_translations", "__()". |
| license | MIT |
| metadata | {"author":"Chris Kelley (hello@iwritecode.io)","version":"1.0.0"} |
WordPress Internationalization (i18n) & Localization (l10n)
This skill covers making WordPress plugins and themes translatable, generating translation files, and handling JavaScript translations.
Core Concepts
| Term | Meaning |
|---|
| i18n | Internationalization — preparing code for translation |
| l10n | Localization — translating strings for a specific locale |
| Text domain | Unique identifier matching your plugin/theme slug |
| POT file | Portable Object Template — source strings catalog |
| PO file | Portable Object — translated strings for one locale |
| MO file | Machine Object — compiled PO for runtime use |
| JSON | JavaScript translation files (WP 5.0+) |
PHP Translation Functions
Basic Functions
$label = __( 'Settings', 'myplugin' );
_e( 'Save Changes', 'myplugin' );
$message = sprintf(
_n( '%d item found', '%d items found', $count, 'myplugin' ),
$count
);
$label = _x( 'Post', 'noun — a blog post', 'myplugin' );
_ex( 'Post', 'verb — to publish', 'myplugin' );
$label = _nx( '%d item', '%d items', $count, 'cart items', 'myplugin' );
Escaping + Translation Combos
Always use these when outputting translated strings in HTML:
echo esc_html__( 'Untrusted translated text', 'myplugin' );
esc_html_e( 'Untrusted translated text', 'myplugin' );
echo esc_attr__( 'Button label', 'myplugin' );
esc_attr_e( 'Button label', 'myplugin' );
printf(
wp_kses(
__( 'Configure <a href="%s">settings</a>.', 'myplugin' ),
[ 'a' => [ 'href' => [] ] ]
),
esc_url( $settings_url )
);
Function Reference
| Function | Returns | Escapes | Context | Plural |
|---|
__() | string | No | No | No |
_e() | void (echoes) | No | No | No |
_n() | string | No | No | Yes |
_x() | string | No | Yes | No |
_ex() | void (echoes) | No | Yes | No |
_nx() | string | No | Yes | Yes |
esc_html__() | string | HTML | No | No |
esc_html_e() | void (echoes) | HTML | No | No |
esc_attr__() | string | Attr | No | No |
esc_attr_e() | void (echoes) | Attr | No | No |
esc_html_x() | string | HTML | Yes | No |
esc_attr_x() | string | Attr | Yes | No |
Translator Comments
Required before any string with placeholders. The comment must be on the line immediately before the function call:
$greeting = sprintf( __( 'Hello, %s!', 'myplugin' ), $user->display_name );
$range = sprintf(
__( 'From %1$s to %2$s', 'myplugin' ),
$start_date,
$end_date
);
$summary = sprintf(
_n( '%d result', '%d results', $count, 'myplugin' ),
number_format_i18n( $count )
);
Rules:
- Use
/* translators: ... */ format (block comment, not //)
- Describe each placeholder:
%s, %d, %1$s, etc.
- Keep translator comments short and descriptive
- Never concatenate translatable strings — translators need full sentence context
Loading the Text Domain
Plugins
add_action( 'init', 'myplugin_load_textdomain' );
function myplugin_load_textdomain(): void {
load_plugin_textdomain(
'myplugin',
false,
dirname( plugin_basename( __FILE__ ) ) . '/languages'
);
}
As of WordPress 6.7+, plugins hosted on WordPress.org get translations loaded automatically — load_plugin_textdomain() is still needed for custom/private plugins.
Themes
add_action( 'after_setup_theme', 'mytheme_load_textdomain' );
function mytheme_load_textdomain(): void {
load_theme_textdomain( 'mytheme', get_template_directory() . '/languages' );
}
Generating Translation Files
Using WP-CLI (Recommended)
wp i18n make-pot . languages/myplugin.pot --slug=myplugin --domain=myplugin
wp i18n make-json languages/ --no-purge
wp i18n make-mo languages/
POT File Placement
my-plugin/
├── languages/
│ ├── myplugin.pot # Source template
│ ├── myplugin-fr_FR.po # French translations
│ ├── myplugin-fr_FR.mo # Compiled French
│ ├── myplugin-de_DE.po # German translations
│ ├── myplugin-de_DE.mo # Compiled German
│ ├── myplugin-fr_FR-{md5}.json # French JS translations
│ └── myplugin-de_DE-{md5}.json # German JS translations
JavaScript Translations (WP 5.0+)
Registering JS Translations
add_action( 'wp_enqueue_scripts', 'myplugin_enqueue_scripts' );
function myplugin_enqueue_scripts(): void {
wp_enqueue_script(
'myplugin-frontend',
plugin_dir_url( __FILE__ ) . 'build/frontend.js',
[],
MYPLUGIN_VERSION,
true
);
wp_set_script_translations(
'myplugin-frontend', // Must match the script handle
'myplugin', // Text domain
plugin_dir_path( __FILE__ ) . 'languages'
);
}
Using Translations in JavaScript
import { __, _n, _x, sprintf } from '@wordpress/i18n';
const label = __( 'Settings', 'myplugin' );
const message = sprintf(
_n( '%d item', '%d items', count, 'myplugin' ),
count
);
const action = _x( 'Post', 'verb', 'myplugin' );
const greeting = sprintf(
__( 'Hello, %s!', 'myplugin' ),
userName
);
Generating JSON Translation Files
After creating PO translations, generate the JSON files WP needs for JS:
wp i18n make-json languages/ --no-purge
The --no-purge flag keeps the original strings in the PO/MO files (needed if the same string is used in both PHP and JS).
Common i18n Mistakes
Never Do This
echo __( 'Posted on ', 'myplugin' ) . $date . __( ' by ', 'myplugin' ) . $author;
printf(
esc_html__( 'Posted on %1$s by %2$s', 'myplugin' ),
esc_html( $date ),
esc_html( $author )
);
__( 'Hello', $domain );
__( 'Hello', 'myplugin' );
__( '<strong>Important:</strong> Save your work.', 'myplugin' );
sprintf(
'<strong>%s</strong> %s',
esc_html__( 'Important:', 'myplugin' ),
esc_html__( 'Save your work.', 'myplugin' )
);
__( "Hello $name", 'myplugin' );
sprintf( __( 'Hello %s', 'myplugin' ), $name );
__( esc_html( $string ), 'myplugin' );
esc_html__( 'Static string', 'myplugin' );
Number and Date Formatting
$formatted_number = number_format_i18n( 1234567.89, 2 );
$formatted_date = wp_date( 'F j, Y', $timestamp );
$formatted_date = date_i18n( 'F j, Y', $timestamp );
RTL (Right-to-Left) Support
function mytheme_enqueue_styles(): void {
wp_enqueue_style( 'mytheme-style', get_stylesheet_uri() );
wp_style_add_data( 'mytheme-style', 'rtl', 'replace' );
}
.widget {
margin-inline-start: 1rem;
padding-inline-end: 1rem;
}
if ( is_rtl() ) {
}
i18n Checklist