WordPress Internationalization Example: Build a Translation-Ready Plugin
Make a WordPress plugin translation-ready with text domains, escaping translation functions, placeholders, plural forms, and JavaScript i18n.
Published
April 20, 2026
Reading Time
3 min read
Updated
April 20, 2026

Implementation Notes
Extension points, code paths, and implementation choices that should survive contact with production.
Best For
WordPress developers, agencies, and technical teams building custom plugin or theme functionality with cleaner operational defaults.
Primary Topics
Editorial Focus
Build Pattern: Extension points, code paths, and implementation choices that should survive contact with production. Updated on April 20, 2026.
Full Report
Last reviewed: April 20, 2026
Internationalization is not only for large multilingual sites. A plugin that hardcodes English strings becomes harder to sell, harder to maintain, and harder to use for teams outside one language. The fix is not complicated, but it must be done consistently.
This guide shows how to make a WordPress plugin translation-ready with text domains, escaping translation functions, translator comments, JavaScript string handling, and practical review rules.
Who this guide is for
This article is for WordPress plugin developers, agencies building reusable client plugins, and teams that want code to be ready for localization before it becomes expensive to retrofit.
Use a stable text domain
The text domain identifies the plugin’s translation strings. Use the plugin slug as the text domain and keep it stable. Changing it later breaks translation files and creates unnecessary work.
<?php
/**
* Plugin Name: VulnWP Notices
* Text Domain: vulnwp-notices
* Domain Path: /languages
*/
Translate and escape output
Use translation functions that match the output context. If the translated string is printed into HTML text, use esc_html__() or esc_html_e(). If it is printed into an attribute, use esc_attr__().
printf(
'<button type="button" aria-label="%1$s">%2$s</button>',
esc_attr__( 'Close notice', 'vulnwp-notices' ),
esc_html__( 'Dismiss', 'vulnwp-notices' )
);
Do not translate after escaping manually in a separate step. Choose the translation and escaping function that matches the final output context.
Use placeholders for dynamic strings
Do not concatenate translated strings around variables. Word order changes between languages. Use placeholders with sprintf() and escape dynamic values.
printf(
esc_html__( 'Updated %s posts.', 'vulnwp-notices' ),
number_format_i18n( $updated_count )
);
Add translator comments
When placeholders are not obvious, add a translator comment immediately before the string. This helps translators understand what the placeholder means.
/* translators: %s: post title. */
$message = sprintf(
__( 'Review completed for "%s".', 'vulnwp-notices' ),
get_the_title( $post_id )
);
Translator comments are small, but they prevent mistranslations in strings that include counts, dates, product names, or technical terms.
Handle plural forms
Pluralization is language-specific. Use _n() for singular and plural forms instead of manual if/else strings.
$message = sprintf(
_n(
'%s issue found.',
'%s issues found.',
$count,
'vulnwp-notices'
),
number_format_i18n( $count )
);
JavaScript strings
If the plugin has editor or admin JavaScript, use the WordPress i18n package in build workflows. Keep JavaScript strings in the same text domain and make sure script translations are loaded as part of the plugin build and enqueue process.
import { __ } from '@wordpress/i18n';
const label = __( 'Open review panel', 'vulnwp-notices' );
Load plugin translations
Modern WordPress.org plugins can often rely on language packs, but custom plugins should still be structured predictably. Keep translation files under a consistent directory such as languages, and make sure the plugin header uses the same text domain.
add_action(
'plugins_loaded',
function () {
load_plugin_textdomain(
'vulnwp-notices',
false,
dirname( plugin_basename( __FILE__ ) ) . '/languages'
);
}
);
For JavaScript-heavy plugins, also verify that script translation files are generated and loaded by the build process. PHP and JavaScript strings are extracted differently.
Review workflow
- Search the plugin for hardcoded user-facing strings.
- Check every translation function for the correct text domain.
- Confirm dynamic strings use placeholders instead of concatenation.
- Check that output is escaped for the final context.
- Run a build or extraction step and confirm strings appear in the translation template.
Production checklist
- Use one stable text domain for the plugin.
- Escape translated output for the final context.
- Use placeholders instead of string concatenation.
- Add translator comments for placeholders and ambiguous strings.
- Use
_n()for plural forms. - Use
number_format_i18n()and date formatting helpers where appropriate. - Review JavaScript strings during build setup.
Common mistakes
- Hardcoded user-facing strings. Every visible string should be translation-ready.
- Concatenating sentences. Word order differs between languages.
- Using the wrong escaping function. HTML text and attributes need different escaping.
- No translator comments. Placeholders without context lead to weak translations.
- Changing text domains. Translation files depend on a stable domain.
Related reading
This pairs with our Settings API example, because admin labels, field descriptions, and notices should be translation-ready from the first version.


