WordPress i18n Plural Forms: _n(), _nx(), printf with Placeholders and Translators Comments

WordPress’s internationalization functions go beyond simple string translation. The most common mistake is using string interpolation — PHP’s sprintf() or string concatenation — to build translated sentences with dynamic values, which breaks translations in languages where the word order differs from English. Every language puts numbers, nouns, and verbs in a different position in a sentence, and a PHP string with a number concatenated in the middle is untranslatable. WordPress provides sprintf()-compatible wrappers (__()`, `_e()`, `_n()`, `_nx()`) that let translators reorder placeholders for their language. Plural forms are equally important: English has two (singular/plural), but Polish has four, Arabic has six, and Chinese has one. Using a plain if ( $count === 1 ) check hardcodes the English plural logic and will fail for many languages. _n() is the correct function — it delegates plural selection to the gettext system, which knows each language’s plural rules.

Problem: Your plugin displays messages like "3 posts deleted" and "1 comment found" — built with string concatenation and a manual singular/plural check. Translators cannot reorder words, and plural forms for non-English languages are incorrect.

Solution: Use _n() for plural-aware strings and sprintf() with %d/%s placeholders to keep dynamic values injectable at any position. Use _nx() when the same word has different plural forms in different grammatical contexts.

<?php
// ── Basic translation ──────────────────────────────────────────────────
$message = __( 'Settings saved.', 'textdomain' );
_e( 'Settings saved.', 'textdomain' ); // echo immediately

// ── Strings with dynamic values — use sprintf, NEVER concatenation ─────

// WRONG: untranslatable — translator cannot change word order
// echo $count . ' ' . __( 'posts deleted', 'textdomain' );

// CORRECT: translators can move %d to any position in their language
// translators: %d = number of deleted posts
printf( _n(
    '%d post deleted.',   // singular (English: 1 post)
    '%d posts deleted.',  // plural   (English: 2+ posts)
    $count,               // the number to evaluate
    'textdomain'
), $count );

// ── _n() with sprintf for multiple placeholders ─────────────────────────
$user_name = 'Alice';
$post_count = 5;

// translators: 1: user display name, 2: number of posts
printf( _n(
    '%2$s published one post by %1$s.',
    '%2$s published %2$d posts by %1$s.',
    $post_count,
    'textdomain'
), esc_html( $user_name ), $post_count );

// ── _nx() — plural with context (disambiguates for translators) ─────────
// Same English word, different grammatical context
// translators: %d = number of comments waiting for moderation
printf( _nx(
    '%d comment',         // singular
    '%d comments',        // plural
    $comment_count,       // number
    'comment count in moderation queue',  // context for translators
    'textdomain'
), $comment_count );

// ── _x() — simple translation with context ────────────────────────────
// When the same English string has different meanings
$label = _x( 'Draft', 'post status label', 'textdomain' );
$label = _x( 'Draft', 'button label',      'textdomain' );

// ── Translators comment format (required by WordPress coding standards) ─
// Must be on the line immediately before the translated string call.
// translators: %s = site name
printf( __( 'Welcome to %s!', 'textdomain' ), esc_html( get_bloginfo( 'name' ) ) );

// ── Number formatting (locale-aware) ──────────────────────────────────
// number_format_i18n() formats numbers according to locale settings
echo number_format_i18n( 12345.67, 2 ); // '12,345.67' in en-US, '12.345,67' in de-DE

// ── Date formatting (locale-aware) ────────────────────────────────────
echo date_i18n( get_option( 'date_format' ), current_time( 'timestamp' ) );

NOTE: The translators: comment is not cosmetic — WordPress's makepot tool and tools like wp i18n make-pot parse it to give translators context about what each placeholder represents. Without these comments, translators see %1$s and %2$d with no idea what they represent. Always add a translators: comment on the line immediately above any __() or _n() call that contains a placeholder. The comment must start with the literal text translators: (case-insensitive) to be picked up by the parser. Also note that number_format_i18n() is the WordPress equivalent of number_format() — it uses the locale's decimal separator and thousands separator, so always use it instead of PHP's native function when displaying numbers to users.