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 w...
Scanned 9/5/2026
Install to Claude Code
npx -y skills add Lonsdale201/wp-agent-skills --skill classic-theme-i18n-textdomain --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Classic Theme I18n Textdomain?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/lonsdale201-classic-theme-i18n-textdomain)More formats (shields.io, HTML) on the badges page.
---
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`
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!