Files
playbook/antigravity-awesome-skills/skills/wp-guard/references/i18n.md
T
2026-07-18 00:02:59 +00:00

3.9 KiB

WP Guard — Internationalization Reference

Contents

  • Wrapper selection
  • Text domain rules
  • Placeholders and translator comments
  • Plurals
  • Sentence assembly
  • JavaScript i18n
  • Dates, numbers, RTL
  • Multilingual-plugin gotchas

Wrapper selection

Situation Use
Return a translated string __( 'Text', 'my-plugin' )
Echo a translated string _e( 'Text', 'my-plugin' ) — or better, esc_html_e()
Translate + escape for HTML esc_html__() / esc_html_e()
Translate + escape for attribute esc_attr__() / esc_attr_e()
Ambiguous word needing context _x( 'Post', 'verb', 'my-plugin' )
Plural _n( '%s item', '%s items', $count, 'my-plugin' )
Plural + context _nx()

When a string is both translated and output, the combined wrappers (esc_html__) are required — translations are untrusted input like any other (a compromised translation file is an XSS vector).

Text domain rules

  • Literal string, always: __( 'Text', 'my-plugin' ). Never __( 'Text', PLUGIN_DOMAIN ), never a variable — static analysis tools and translate.wordpress.org both fail on non-literal domains.
  • Must match the plugin slug exactly (the WordPress.org directory enforces this).
  • One domain per plugin/theme. AI agents copying snippets from other projects routinely import foreign text domains — grep for domains that don't match the project's.

Placeholders and translator comments

/* translators: 1: customer name, 2: order number. */
$message = sprintf(
	__( 'Hi %1$s, your order #%2$d is on its way.', 'my-plugin' ),
	$customer_name,
	$order_id
);
  • Numbered placeholders (%1$s) whenever there is more than one — translators reorder words.
  • A /* translators: … */ comment on every string with placeholders, immediately above the line.
  • Never put variables or HTML soup inside the translatable string when it can sit outside.

Plurals

_n() exists because languages have between one and six plural forms. Never:

// Wrong — English-only logic.
$label = $count === 1 ? __( 'item', 'my-plugin' ) : __( 'items', 'my-plugin' );

Always _n( '%s item', '%s items', $count, 'my-plugin' ), then sprintf() with number_format_i18n( $count ).

Sentence assembly

Never build sentences by concatenation — word order differs across languages:

// Wrong: translators get fragments they cannot reorder.
echo __( 'Imported', 'my-plugin' ) . ' ' . $count . ' ' . __( 'products', 'my-plugin' );

// Right: one string, one placeholder, full context.
/* translators: %s: number of imported products. */
printf( esc_html__( 'Imported %s products.', 'my-plugin' ), number_format_i18n( $count ) );

JavaScript i18n

  • wp_set_script_translations( 'my-handle', 'my-plugin' ) after enqueueing; use __() from @wordpress/i18n in the JS.
  • Legacy pattern (wp_localize_script with pre-translated strings) is acceptable in legacy codebases — match the project.

Dates, numbers, RTL

  • Dates: wp_date() / date_i18n() with the site's format options — never raw date() for display.
  • Numbers: number_format_i18n().
  • CSS: logical properties (margin-inline-start, not margin-left) for new styles; don't hand-write directional CSS — build -rtl.css files with RTLCSS. Auto-loading them applies only to core styles and block.json-registered styles; classic plugin/theme handles must opt in with wp_style_add_data( $handle, 'rtl', 'replace' ).

Multilingual-plugin gotchas (WPML / Polylang)

  • Strings stored in options/meta are NOT translated by __() — they need string registration (WPML String Translation / pll_register_string). Flag stored user-facing strings during review.
  • IDs are language-specific: a hardcoded page_id points at one language's page. Resolve through the multilingual plugin's API or filters when the project uses one.
  • Queries on multilingual sites are language-filtered by default — explicitly note when a query intentionally crosses languages.