classic-theme-i18n-textdomain
Build or audit internationalization in classic WordPress themes on WP 7.1. Covers `style.css` `Text Domain` and `Domain Path`, slug-matching domains, `load_theme_textdomain()` and `load_child_theme_textdomain()` on `after_setup_theme`, WP 6.7+ early translation warnings, escaped
Install
npx skills add https://github.com/Lonsdale201/wp-agent-skills/tree/main/theme-development/classic-theme-i18n-textdomain
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install lonsdale201-wp-agent-skills@llmmart
git clone https://github.com/Lonsdale201/wp-agent-skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole lonsdale201/wp-agent-skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
Classic Theme Internationalization and Text Domain
Use this when adding or reviewing translation readiness in a classic PHP theme: text domain headers, PHP strings, plural strings, context, translator comments, local .mo files, and JavaScript translations.
When to Use This Skill
- Creating or reviewing a theme
style.cssheader. - Adding visible text to templates,
functions.php, Customizer controls, menus, widgets, or comments. - Loading bundled theme translations.
- Fixing WP 6.7+ "translations loaded too early" notices.
- Auditing text domains before release.
Text Domain Header
Set the text domain in style.css.
/*
Theme Name: My Theme
Text Domain: my-theme
Domain Path: /languages
*/
Rules:
- The text domain should match the theme slug.
- Use lowercase kebab-case, not underscores.
- Use the same literal domain in every translation call.
Domain Pathis relative to the theme root and starts with/.- Use
/languagesunless the project has a clear reason for another directory.
Loading Translations
For themes distributed through WordPress.org language packs, WordPress can load translations from wp-content/languages/themes/.
If the theme bundles its own translations, register the path on after_setup_theme.
add_action( 'after_setup_theme', 'mytheme_load_textdomain' );
function mytheme_load_textdomain() {
load_theme_textdomain(
'my-theme',
get_template_directory() . '/languages'
);
}
For a child theme:
add_action( 'after_setup_theme', 'mytheme_child_load_textdomain' );
function mytheme_child_load_textdomain() {
load_child_theme_textdomain(
'my-theme',
get_stylesheet_directory() . '/languages'
);
}
Rules:
- Load theme translations no earlier than
after_setup_theme. - Do not translate strings at file load time before
after_setup_theme. - WP 6.7+ warns when just-in-time translation loading is triggered too early.
- Theme-bundled
.mofiles are named by locale, for examplede_DE.mo. - Language-pack
.mofiles underwp-content/languages/themes/are namedmy-theme-de_DE.mo.
Escaped Translation Functions
Prefer translate-and-escape helpers at output time.
esc_html_e( 'Read more', 'my-theme' );
printf(
'<a href="%1$s">%2$s</a>',
esc_url( get_permalink() ),
esc_html__( 'Continue reading', 'my-theme' )
);
Use by context:
| Output context | Function |
|---|---|
| HTML text | esc_html__() / esc_html_e() |
| Attribute | esc_attr__() / esc_attr_e() |
| URL | Translate label separately; escape URL with esc_url() |
| Controlled inline HTML | wp_kses() after translation |
Rules:
- Do not echo
__()directly into HTML unless it is escaped afterward. - Do not use
esc_html__()for attribute values; useesc_attr__(). - Keep URLs out of translatable strings when possible.
Context, Plurals, and Placeholders
Use context when the same English word has different meanings.
echo esc_html_x( 'Post', 'noun: blog post', 'my-theme' );
echo esc_html_x( 'Post', 'verb: submit form', 'my-theme' );
Use plural functions for counts.
$count = get_comments_number();
printf(
esc_html(
_n(
'%s comment',
'%s comments',
$count,
'my-theme'
)
),
esc_html( number_format_i18n( $count ) )
);
Use numbered placeholders when translators may reorder words.
printf(
/* translators: 1: post title, 2: author name. */
esc_html__( '%1$s by %2$s', 'my-theme' ),
esc_html( get_the_title() ),
esc_html( get_the_author() )
);
Rules:
- Add translator comments immediately before strings with placeholders.
- Do not concatenate sentence fragments.
- Do not translate dynamic values such as post titles, usernames, or option values.
- Use
number_format_i18n()for numbers shown to users.
JavaScript Strings
For WordPress-registered scripts that use @wordpress/i18n, set script translations.
wp_enqueue_script(
'mytheme-navigation',
get_theme_file_uri( 'assets/js/navigation.js' ),
array( 'wp-i18n' ),
mytheme_asset_version( 'assets/js/navigation.js' ),
array( 'in_footer' => true )
);
wp_set_script_translations(
'mytheme-navigation',
'my-theme',
get_theme_file_path( 'languages' )
);
Rules:
- Register script translations after registering/enqueueing the script handle.
- JavaScript translations need the same text domain.
- Do not pass already-translated PHP strings into JS just to avoid JS i18n.
Text Domain Audit
Search patterns:
rg "__\\(|_e\\(|_x\\(|_n\\(|esc_html__|esc_attr__|esc_html_e|esc_attr_e" .
Check:
- Every theme string has the literal theme text domain.
- No
$text_domainvariable is used in translation calls. - No plugin text domain is used in theme-owned strings.
- No missing second argument.
- No hardcoded visible English strings remain in templates.
Review Checklist
style.csshasText Domainmatching the theme slug.Domain Pathmatches bundled translation location.- Translation loading runs on
after_setup_themewhen needed. - No translation calls run too early at file load time.
- Output uses escaped translation functions by context.
- Plural strings use
_n()or related helpers. - Ambiguous strings use
_x()/esc_html_x(). - Placeholders are numbered and documented with translator comments.
- JavaScript translation setup uses
wp_set_script_translations()when needed.
Common Mistakes
- Using underscores in the text domain.
- Using a variable text domain, which extraction tools cannot reliably parse.
- Concatenating translatable sentence fragments.
- Echoing raw
__()output. - Translating dynamic user/content values.
- Naming bundled theme files
my-theme-de_DE.moinside the theme directory instead ofde_DE.mo.
References
- Official documentation: https://developer.wordpress.org/themes/advanced-topics/internationalization/
- Official documentation: https://developer.wordpress.org/themes/classic-themes/functionality/internationalization/
- Official documentation: https://developer.wordpress.org/reference/functions/load_theme_textdomain/
- Verified source paths:
wp-includes/l10n.phpwp-includes/class-wp-theme.phpwp-includes/link-template.phpwp-content/themes/storefront/style.csswp-content/themes/storefront/inc/class-storefront.phpwp-content/themes/generatepress/style.css
Files (wp-agent-skills)
-
SKILL.md 7.1 KB
--- name: classic-theme-i18n-textdomain description: Build or audit internationalization in classic WordPress themes on WP 7.1. Covers `style.css` `Text Domain` and `Domain Path`, slug-matching domains, `load_theme_textdomain()` and `load_child_theme_textdomain()` on `after_setup_theme`, WP 6.7+ early translation warnings, escaped translation functions, `_x`, `_n`, translator comments, placeholders, JavaScript translation setup, and common mistakes such as variable text domains, string concatenation, missing domains, raw translated output, or wrongly named theme `.mo` files. metadata: wp-skills-author: "Soczó Kristóf" wp-skills-contact: "mailto:lonsdale201@hotmail.com" wp-skills-plugin: "wordpress" wp-skills-plugin-version-tested: "7.1" wp-skills-wp-version-tested: "7.1" wp-skills-php-min: "7.4" wp-skills-last-updated: "2026-08-20" --- # Classic Theme Internationalization and Text Domain Use this when adding or reviewing translation readiness in a classic PHP theme: text domain headers, PHP strings, plural strings, context, translator comments, local `.mo` files, and JavaScript translations. ## When to Use This Skill - Creating or reviewing a theme `style.css` header. - Adding visible text to templates, `functions.php`, Customizer controls, menus, widgets, or comments. - Loading bundled theme translations. - Fixing WP 6.7+ "translations loaded too early" notices. - Auditing text domains before release. ## Text Domain Header Set the text domain in `style.css`. ```css /* Theme Name: My Theme Text Domain: my-theme Domain Path: /languages */ ``` Rules: - The text domain should match the theme slug. - Use lowercase kebab-case, not underscores. - Use the same literal domain in every translation call. - `Domain Path` is relative to the theme root and starts with `/`. - Use `/languages` unless the project has a clear reason for another directory. ## Loading Translations For themes distributed through WordPress.org language packs, WordPress can load translations from `wp-content/languages/themes/`. If the theme bundles its own translations, register the path on `after_setup_theme`. ```php add_action( 'after_setup_theme', 'mytheme_load_textdomain' ); function mytheme_load_textdomain() { load_theme_textdomain( 'my-theme', get_template_directory() . '/languages' ); } ``` For a child theme: ```php add_action( 'after_setup_theme', 'mytheme_child_load_textdomain' ); function mytheme_child_load_textdomain() { load_child_theme_textdomain( 'my-theme', get_stylesheet_directory() . '/languages' ); } ``` Rules: - Load theme translations no earlier than `after_setup_theme`. - Do not translate strings at file load time before `after_setup_theme`. - WP 6.7+ warns when just-in-time translation loading is triggered too early. - Theme-bundled `.mo` files are named by locale, for example `de_DE.mo`. - Language-pack `.mo` files under `wp-content/languages/themes/` are named `my-theme-de_DE.mo`. ## Escaped Translation Functions Prefer translate-and-escape helpers at output time. ```php esc_html_e( 'Read more', 'my-theme' ); ``` ```php printf( '<a href="%1$s">%2$s</a>', esc_url( get_permalink() ), esc_html__( 'Continue reading', 'my-theme' ) ); ``` Use by context: | Output context | Function | |---|---| | HTML text | `esc_html__()` / `esc_html_e()` | | Attribute | `esc_attr__()` / `esc_attr_e()` | | URL | Translate label separately; escape URL with `esc_url()` | | Controlled inline HTML | `wp_kses()` after translation | Rules: - Do not echo `__()` directly into HTML unless it is escaped afterward. - Do not use `esc_html__()` for attribute values; use `esc_attr__()`. - Keep URLs out of translatable strings when possible. ## Context, Plurals, and Placeholders Use context when the same English word has different meanings. ```php echo esc_html_x( 'Post', 'noun: blog post', 'my-theme' ); echo esc_html_x( 'Post', 'verb: submit form', 'my-theme' ); ``` Use plural functions for counts. ```php $count = get_comments_number(); printf( esc_html( _n( '%s comment', '%s comments', $count, 'my-theme' ) ), esc_html( number_format_i18n( $count ) ) ); ``` Use numbered placeholders when translators may reorder words. ```php printf( /* translators: 1: post title, 2: author name. */ esc_html__( '%1$s by %2$s', 'my-theme' ), esc_html( get_the_title() ), esc_html( get_the_author() ) ); ``` Rules: - Add translator comments immediately before strings with placeholders. - Do not concatenate sentence fragments. - Do not translate dynamic values such as post titles, usernames, or option values. - Use `number_format_i18n()` for numbers shown to users. ## JavaScript Strings For WordPress-registered scripts that use `@wordpress/i18n`, set script translations. ```php wp_enqueue_script( 'mytheme-navigation', get_theme_file_uri( 'assets/js/navigation.js' ), array( 'wp-i18n' ), mytheme_asset_version( 'assets/js/navigation.js' ), array( 'in_footer' => true ) ); wp_set_script_translations( 'mytheme-navigation', 'my-theme', get_theme_file_path( 'languages' ) ); ``` Rules: - Register script translations after registering/enqueueing the script handle. - JavaScript translations need the same text domain. - Do not pass already-translated PHP strings into JS just to avoid JS i18n. ## Text Domain Audit Search patterns: ```bash rg "__\\(|_e\\(|_x\\(|_n\\(|esc_html__|esc_attr__|esc_html_e|esc_attr_e" . ``` Check: - Every theme string has the literal theme text domain. - No `$text_domain` variable is used in translation calls. - No plugin text domain is used in theme-owned strings. - No missing second argument. - No hardcoded visible English strings remain in templates. ## Review Checklist - `style.css` has `Text Domain` matching the theme slug. - `Domain Path` matches bundled translation location. - Translation loading runs on `after_setup_theme` when needed. - No translation calls run too early at file load time. - Output uses escaped translation functions by context. - Plural strings use `_n()` or related helpers. - Ambiguous strings use `_x()`/`esc_html_x()`. - Placeholders are numbered and documented with translator comments. - JavaScript translation setup uses `wp_set_script_translations()` when needed. ## Common Mistakes - Using underscores in the text domain. - Using a variable text domain, which extraction tools cannot reliably parse. - Concatenating translatable sentence fragments. - Echoing raw `__()` output. - Translating dynamic user/content values. - Naming bundled theme files `my-theme-de_DE.mo` inside the theme directory instead of `de_DE.mo`. ## References - Official documentation: <https://developer.wordpress.org/themes/advanced-topics/internationalization/> - Official documentation: <https://developer.wordpress.org/themes/classic-themes/functionality/internationalization/> - Official documentation: <https://developer.wordpress.org/reference/functions/load_theme_textdomain/> - Verified source paths: - `wp-includes/l10n.php` - `wp-includes/class-wp-theme.php` - `wp-includes/link-template.php` - `wp-content/themes/storefront/style.css` - `wp-content/themes/storefront/inc/class-storefront.php` - `wp-content/themes/generatepress/style.css`
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.