claude-plugin-wordpress-manager 1.5.0 → 1.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +2 -2
- package/CHANGELOG.md +97 -0
- package/README.md +27 -13
- package/agents/wp-accessibility-auditor.md +206 -0
- package/agents/wp-content-strategist.md +18 -0
- package/agents/wp-deployment-engineer.md +34 -2
- package/agents/wp-performance-optimizer.md +12 -0
- package/agents/wp-security-auditor.md +20 -0
- package/agents/wp-security-hardener.md +266 -0
- package/agents/wp-site-manager.md +14 -0
- package/agents/wp-test-engineer.md +207 -0
- package/docs/guides/INDEX.md +46 -0
- package/docs/guides/wp-blog.md +590 -0
- package/docs/guides/wp-design-system.md +976 -0
- package/docs/guides/wp-ecommerce.md +786 -0
- package/docs/guides/wp-landing-page.md +762 -0
- package/docs/guides/wp-portfolio.md +713 -0
- package/docs/plans/2026-02-27-design-system-guide-design.md +30 -0
- package/docs/plans/2026-02-27-site-type-guides-design.md +44 -0
- package/package.json +2 -2
- package/skills/wordpress-router/references/decision-tree.md +12 -2
- package/skills/wp-accessibility/SKILL.md +170 -0
- package/skills/wp-accessibility/references/a11y-audit-tools.md +248 -0
- package/skills/wp-accessibility/references/a11y-testing.md +222 -0
- package/skills/wp-accessibility/references/block-a11y.md +247 -0
- package/skills/wp-accessibility/references/interactive-a11y.md +272 -0
- package/skills/wp-accessibility/references/media-a11y.md +254 -0
- package/skills/wp-accessibility/references/theme-a11y.md +309 -0
- package/skills/wp-audit/SKILL.md +4 -0
- package/skills/wp-block-development/SKILL.md +5 -0
- package/skills/wp-block-themes/SKILL.md +4 -0
- package/skills/wp-e2e-testing/SKILL.md +186 -0
- package/skills/wp-e2e-testing/references/ci-integration.md +174 -0
- package/skills/wp-e2e-testing/references/jest-wordpress.md +114 -0
- package/skills/wp-e2e-testing/references/phpunit-wordpress.md +141 -0
- package/skills/wp-e2e-testing/references/playwright-wordpress.md +108 -0
- package/skills/wp-e2e-testing/references/test-data-generation.md +127 -0
- package/skills/wp-e2e-testing/references/visual-regression.md +107 -0
- package/skills/wp-e2e-testing/references/wp-env-setup.md +97 -0
- package/skills/wp-e2e-testing/scripts/test_inspect.mjs +375 -0
- package/skills/wp-headless/SKILL.md +168 -0
- package/skills/wp-headless/references/api-layer-choice.md +160 -0
- package/skills/wp-headless/references/cors-config.md +245 -0
- package/skills/wp-headless/references/frontend-integration.md +331 -0
- package/skills/wp-headless/references/headless-auth.md +286 -0
- package/skills/wp-headless/references/webhooks.md +277 -0
- package/skills/wp-headless/references/wpgraphql.md +331 -0
- package/skills/wp-headless/scripts/headless_inspect.mjs +321 -0
- package/skills/wp-i18n/SKILL.md +170 -0
- package/skills/wp-i18n/references/js-i18n.md +201 -0
- package/skills/wp-i18n/references/multilingual-setup.md +219 -0
- package/skills/wp-i18n/references/php-i18n.md +196 -0
- package/skills/wp-i18n/references/rtl-support.md +206 -0
- package/skills/wp-i18n/references/translation-workflow.md +178 -0
- package/skills/wp-i18n/references/wpcli-i18n.md +177 -0
- package/skills/wp-i18n/scripts/i18n_inspect.mjs +330 -0
- package/skills/wp-interactivity-api/SKILL.md +4 -0
- package/skills/wp-plugin-development/SKILL.md +6 -0
- package/skills/wp-rest-api/SKILL.md +4 -0
- package/skills/wp-security/SKILL.md +179 -0
- package/skills/wp-security/references/api-restriction.md +147 -0
- package/skills/wp-security/references/authentication-hardening.md +105 -0
- package/skills/wp-security/references/filesystem-hardening.md +105 -0
- package/skills/wp-security/references/http-headers.md +105 -0
- package/skills/wp-security/references/incident-response.md +144 -0
- package/skills/wp-security/references/user-capabilities.md +115 -0
- package/skills/wp-security/references/wp-config-security.md +129 -0
- package/skills/wp-security/scripts/security_inspect.mjs +393 -0
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# PHP Internationalization
|
|
2
|
+
|
|
3
|
+
Use this file when internationalizing PHP code in WordPress plugins and themes.
|
|
4
|
+
|
|
5
|
+
## Core translation functions
|
|
6
|
+
|
|
7
|
+
### Simple strings
|
|
8
|
+
|
|
9
|
+
```php
|
|
10
|
+
// Return translated string
|
|
11
|
+
$text = __('Hello World', 'my-text-domain');
|
|
12
|
+
|
|
13
|
+
// Echo translated string
|
|
14
|
+
_e('Hello World', 'my-text-domain');
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
### Strings with context
|
|
18
|
+
|
|
19
|
+
```php
|
|
20
|
+
// "Post" as a verb vs noun
|
|
21
|
+
$verb = _x('Post', 'verb', 'my-text-domain');
|
|
22
|
+
$noun = _x('Post', 'noun', 'my-text-domain');
|
|
23
|
+
|
|
24
|
+
// Echo version
|
|
25
|
+
_ex('Post', 'verb', 'my-text-domain');
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### Strings with variables
|
|
29
|
+
|
|
30
|
+
```php
|
|
31
|
+
// Single placeholder
|
|
32
|
+
$text = sprintf(
|
|
33
|
+
/* translators: %s: user display name */
|
|
34
|
+
__('Welcome, %s!', 'my-text-domain'),
|
|
35
|
+
$user->display_name
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
// Multiple placeholders (use numbered)
|
|
39
|
+
$text = sprintf(
|
|
40
|
+
/* translators: 1: product name, 2: price */
|
|
41
|
+
__('%1$s costs %2$s', 'my-text-domain'),
|
|
42
|
+
$product_name,
|
|
43
|
+
$price
|
|
44
|
+
);
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Plurals
|
|
48
|
+
|
|
49
|
+
```php
|
|
50
|
+
$text = sprintf(
|
|
51
|
+
/* translators: %d: number of items */
|
|
52
|
+
_n('%d item', '%d items', $count, 'my-text-domain'),
|
|
53
|
+
$count
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
// With context
|
|
57
|
+
$text = sprintf(
|
|
58
|
+
_nx('%d post', '%d posts', $count, 'blog posts', 'my-text-domain'),
|
|
59
|
+
$count
|
|
60
|
+
);
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Escaping functions
|
|
64
|
+
|
|
65
|
+
Always use escaping variants when outputting to HTML:
|
|
66
|
+
|
|
67
|
+
```php
|
|
68
|
+
// Escape for HTML content
|
|
69
|
+
echo esc_html__('Safe text', 'my-text-domain');
|
|
70
|
+
esc_html_e('Safe text', 'my-text-domain');
|
|
71
|
+
|
|
72
|
+
// Escape for HTML attributes
|
|
73
|
+
echo esc_attr__('attribute value', 'my-text-domain');
|
|
74
|
+
esc_attr_e('attribute value', 'my-text-domain');
|
|
75
|
+
|
|
76
|
+
// With context
|
|
77
|
+
echo esc_html_x('Post', 'verb', 'my-text-domain');
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### When to use which
|
|
81
|
+
|
|
82
|
+
| Context | Function |
|
|
83
|
+
|---------|----------|
|
|
84
|
+
| HTML content | `esc_html__()` / `esc_html_e()` |
|
|
85
|
+
| HTML attributes | `esc_attr__()` / `esc_attr_e()` |
|
|
86
|
+
| URLs | `esc_url()` around `__()` |
|
|
87
|
+
| JavaScript strings | `esc_js()` around `__()` |
|
|
88
|
+
| Already-safe internal use | `__()` / `_e()` |
|
|
89
|
+
|
|
90
|
+
## Text domain loading
|
|
91
|
+
|
|
92
|
+
### Plugins
|
|
93
|
+
|
|
94
|
+
```php
|
|
95
|
+
add_action('init', function() {
|
|
96
|
+
load_plugin_textdomain(
|
|
97
|
+
'my-text-domain',
|
|
98
|
+
false,
|
|
99
|
+
dirname(plugin_basename(__FILE__)) . '/languages'
|
|
100
|
+
);
|
|
101
|
+
});
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Themes
|
|
105
|
+
|
|
106
|
+
```php
|
|
107
|
+
add_action('after_setup_theme', function() {
|
|
108
|
+
load_theme_textdomain(
|
|
109
|
+
'my-text-domain',
|
|
110
|
+
get_template_directory() . '/languages'
|
|
111
|
+
);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// Child theme
|
|
115
|
+
add_action('after_setup_theme', function() {
|
|
116
|
+
load_child_theme_textdomain(
|
|
117
|
+
'my-child-domain',
|
|
118
|
+
get_stylesheet_directory() . '/languages'
|
|
119
|
+
);
|
|
120
|
+
});
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### WordPress 6.7+ (automatic loading)
|
|
124
|
+
|
|
125
|
+
Since WordPress 6.7, translation files placed in `wp-content/languages/plugins/` or `wp-content/languages/themes/` are loaded automatically. Manual `load_plugin_textdomain()` is still recommended as a fallback.
|
|
126
|
+
|
|
127
|
+
## Common mistakes
|
|
128
|
+
|
|
129
|
+
### Do NOT concatenate translatable strings
|
|
130
|
+
|
|
131
|
+
```php
|
|
132
|
+
// WRONG — translators cannot reorder
|
|
133
|
+
__('There are ' . $count . ' items', 'my-text-domain');
|
|
134
|
+
|
|
135
|
+
// CORRECT
|
|
136
|
+
sprintf(__('There are %d items', 'my-text-domain'), $count);
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Do NOT use variables as text domain
|
|
140
|
+
|
|
141
|
+
```php
|
|
142
|
+
// WRONG — tools cannot extract
|
|
143
|
+
__('Hello', $domain);
|
|
144
|
+
|
|
145
|
+
// CORRECT — literal string only
|
|
146
|
+
__('Hello', 'my-text-domain');
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Do NOT translate HTML
|
|
150
|
+
|
|
151
|
+
```php
|
|
152
|
+
// WRONG — HTML in translatable string
|
|
153
|
+
__('<strong>Warning:</strong> This is dangerous', 'my-text-domain');
|
|
154
|
+
|
|
155
|
+
// CORRECT — separate HTML from text
|
|
156
|
+
'<strong>' . esc_html__('Warning:', 'my-text-domain') . '</strong> '
|
|
157
|
+
. esc_html__('This is dangerous', 'my-text-domain');
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### Do NOT split sentences
|
|
161
|
+
|
|
162
|
+
```php
|
|
163
|
+
// WRONG — sentence split across calls
|
|
164
|
+
__('Click ', 'my-text-domain') . '<a>' . __('here', 'my-text-domain') . '</a>';
|
|
165
|
+
|
|
166
|
+
// CORRECT — full sentence with placeholder
|
|
167
|
+
sprintf(
|
|
168
|
+
/* translators: %s: link HTML */
|
|
169
|
+
__('Click %s for details', 'my-text-domain'),
|
|
170
|
+
'<a href="...">' . esc_html__('here', 'my-text-domain') . '</a>'
|
|
171
|
+
);
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## Translator comments
|
|
175
|
+
|
|
176
|
+
Add context for translators with `/* translators: */` comments:
|
|
177
|
+
|
|
178
|
+
```php
|
|
179
|
+
/* translators: %s: date in ISO 8601 format */
|
|
180
|
+
sprintf(__('Published on %s', 'my-text-domain'), $date);
|
|
181
|
+
|
|
182
|
+
/* translators: 1: opening link tag, 2: closing link tag */
|
|
183
|
+
sprintf(__('Read the %1$sfull article%2$s', 'my-text-domain'), '<a href="...">', '</a>');
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
These comments must appear on the line immediately before the translation function call.
|
|
187
|
+
|
|
188
|
+
## Verification
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
# Check for missing text domains
|
|
192
|
+
wp i18n make-pot . languages/my-text-domain.pot --slug=my-text-domain
|
|
193
|
+
|
|
194
|
+
# Audit for untranslated strings
|
|
195
|
+
grep -rn "echo \"\|echo '" --include="*.php" | grep -v "__\|_e\|esc_"
|
|
196
|
+
```
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# RTL (Right-to-Left) Support
|
|
2
|
+
|
|
3
|
+
Use this file when adding right-to-left language support to WordPress themes and plugins.
|
|
4
|
+
|
|
5
|
+
## RTL languages
|
|
6
|
+
|
|
7
|
+
Arabic (`ar`), Hebrew (`he`), Persian/Farsi (`fa`), Urdu (`ur`), Pashto (`ps`), Sindhi (`sd`), Kurdish (Sorani) (`ckb`), Uyghur (`ug`), Divehi (`dv`).
|
|
8
|
+
|
|
9
|
+
## How WordPress handles RTL
|
|
10
|
+
|
|
11
|
+
WordPress automatically:
|
|
12
|
+
1. Sets `dir="rtl"` on `<html>` when the locale is RTL
|
|
13
|
+
2. Adds `class="rtl"` to `<body>`
|
|
14
|
+
3. Loads `*-rtl.css` stylesheets (if they exist) instead of `*.css`
|
|
15
|
+
|
|
16
|
+
Check direction in PHP:
|
|
17
|
+
```php
|
|
18
|
+
if (is_rtl()) {
|
|
19
|
+
// RTL-specific logic
|
|
20
|
+
}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Check in JavaScript:
|
|
24
|
+
```js
|
|
25
|
+
const isRtl = document.documentElement.dir === 'rtl';
|
|
26
|
+
// or
|
|
27
|
+
const isRtl = document.body.classList.contains('rtl');
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## CSS approach: Logical properties (recommended)
|
|
31
|
+
|
|
32
|
+
Modern CSS logical properties handle both LTR and RTL automatically:
|
|
33
|
+
|
|
34
|
+
```css
|
|
35
|
+
/* AVOID physical properties */
|
|
36
|
+
.card {
|
|
37
|
+
margin-left: 20px; /* LTR only */
|
|
38
|
+
padding-right: 10px; /* LTR only */
|
|
39
|
+
text-align: left; /* LTR only */
|
|
40
|
+
float: left; /* LTR only */
|
|
41
|
+
border-left: 1px solid; /* LTR only */
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/* USE logical properties */
|
|
45
|
+
.card {
|
|
46
|
+
margin-inline-start: 20px; /* left in LTR, right in RTL */
|
|
47
|
+
padding-inline-end: 10px; /* right in LTR, left in RTL */
|
|
48
|
+
text-align: start; /* left in LTR, right in RTL */
|
|
49
|
+
float: inline-start; /* left in LTR, right in RTL */
|
|
50
|
+
border-inline-start: 1px solid; /* left in LTR, right in RTL */
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Logical property mapping
|
|
55
|
+
|
|
56
|
+
| Physical | Logical | LTR | RTL |
|
|
57
|
+
|----------|---------|-----|-----|
|
|
58
|
+
| `left` | `inline-start` | left | right |
|
|
59
|
+
| `right` | `inline-end` | right | left |
|
|
60
|
+
| `margin-left` | `margin-inline-start` | margin-left | margin-right |
|
|
61
|
+
| `margin-right` | `margin-inline-end` | margin-right | margin-left |
|
|
62
|
+
| `padding-left` | `padding-inline-start` | padding-left | padding-right |
|
|
63
|
+
| `padding-right` | `padding-inline-end` | padding-right | padding-left |
|
|
64
|
+
| `border-left` | `border-inline-start` | border-left | border-right |
|
|
65
|
+
| `text-align: left` | `text-align: start` | left | right |
|
|
66
|
+
| `float: left` | `float: inline-start` | left | right |
|
|
67
|
+
|
|
68
|
+
### Shorthand
|
|
69
|
+
|
|
70
|
+
```css
|
|
71
|
+
/* Physical: top right bottom left */
|
|
72
|
+
margin: 10px 20px 10px 0;
|
|
73
|
+
|
|
74
|
+
/* Logical */
|
|
75
|
+
margin-block: 10px; /* top and bottom */
|
|
76
|
+
margin-inline: 0 20px; /* start and end */
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## CSS approach: RTL stylesheets (legacy)
|
|
80
|
+
|
|
81
|
+
### Automatic RTL generation
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
# Generate RTL stylesheets with rtlcss
|
|
85
|
+
npx rtlcss style.css style-rtl.css
|
|
86
|
+
|
|
87
|
+
# Watch mode
|
|
88
|
+
npx rtlcss -w src/style.css dist/style-rtl.css
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### rtlcss directives
|
|
92
|
+
|
|
93
|
+
Control flipping with comments:
|
|
94
|
+
|
|
95
|
+
```css
|
|
96
|
+
/* rtl:ignore — skip this rule */
|
|
97
|
+
.icon-arrow {
|
|
98
|
+
/* rtl:ignore */
|
|
99
|
+
transform: rotate(45deg);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/* rtl:remove — remove this rule in RTL */
|
|
103
|
+
.ltr-only {
|
|
104
|
+
/* rtl:remove */
|
|
105
|
+
float: left;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/* rtl:raw — insert raw CSS in RTL */
|
|
109
|
+
.custom {
|
|
110
|
+
/* rtl:raw:
|
|
111
|
+
float: right;
|
|
112
|
+
*/
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Enqueuing RTL stylesheets
|
|
117
|
+
|
|
118
|
+
WordPress handles this automatically when you enqueue properly:
|
|
119
|
+
|
|
120
|
+
```php
|
|
121
|
+
wp_enqueue_style('my-style', plugins_url('css/style.css', __FILE__));
|
|
122
|
+
// WordPress will automatically load css/style-rtl.css in RTL contexts
|
|
123
|
+
// IF the RTL file exists in the same directory
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
For custom paths:
|
|
127
|
+
```php
|
|
128
|
+
wp_style_add_data('my-style', 'rtl', 'replace');
|
|
129
|
+
// WordPress will load style-rtl.css instead of style.css
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Or add an independent RTL stylesheet:
|
|
133
|
+
```php
|
|
134
|
+
wp_style_add_data('my-style', 'rtl', plugin_dir_url(__FILE__) . 'css/custom-rtl.css');
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Block editor RTL support
|
|
138
|
+
|
|
139
|
+
### Block styles
|
|
140
|
+
|
|
141
|
+
```css
|
|
142
|
+
/* Use logical properties in block stylesheets */
|
|
143
|
+
.wp-block-my-plugin-card {
|
|
144
|
+
padding-inline-start: 1rem;
|
|
145
|
+
border-inline-start: 3px solid var(--wp--preset--color--primary);
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### useBlockProps and direction
|
|
150
|
+
|
|
151
|
+
```js
|
|
152
|
+
import { useBlockProps } from '@wordpress/block-editor';
|
|
153
|
+
|
|
154
|
+
export default function Edit() {
|
|
155
|
+
const blockProps = useBlockProps();
|
|
156
|
+
// blockProps automatically inherits the document direction
|
|
157
|
+
return <div {...blockProps}>Content</div>;
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Icons and directional elements
|
|
162
|
+
|
|
163
|
+
Flip directional icons in RTL:
|
|
164
|
+
|
|
165
|
+
```css
|
|
166
|
+
/* Arrows, chevrons, navigation icons */
|
|
167
|
+
.rtl .icon-arrow-right {
|
|
168
|
+
transform: scaleX(-1);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/* Or with logical approach */
|
|
172
|
+
[dir="rtl"] .icon-next {
|
|
173
|
+
transform: scaleX(-1);
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Do NOT flip:
|
|
178
|
+
- Clocks (always clockwise)
|
|
179
|
+
- Media playback controls (play/pause universal)
|
|
180
|
+
- Checkmarks
|
|
181
|
+
- Phone icons
|
|
182
|
+
- Logos and brand marks
|
|
183
|
+
|
|
184
|
+
## Testing RTL
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
# Switch site to Arabic
|
|
188
|
+
wp site switch-language ar
|
|
189
|
+
|
|
190
|
+
# Switch back to English
|
|
191
|
+
wp site switch-language en_US
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Quick browser test: add `dir="rtl"` to `<html>` in DevTools.
|
|
195
|
+
|
|
196
|
+
WordPress admin: Settings → General → Site Language → Arabic (or any RTL language).
|
|
197
|
+
|
|
198
|
+
## Verification checklist
|
|
199
|
+
|
|
200
|
+
- [ ] All CSS uses logical properties (or RTL stylesheets exist)
|
|
201
|
+
- [ ] No hardcoded `left`/`right` in positioning
|
|
202
|
+
- [ ] Directional icons flip correctly
|
|
203
|
+
- [ ] Text alignment uses `start`/`end` not `left`/`right`
|
|
204
|
+
- [ ] Flexbox/Grid layout respects direction
|
|
205
|
+
- [ ] RTL stylesheets are properly enqueued with `wp_style_add_data`
|
|
206
|
+
- [ ] Tested with at least one RTL locale (Arabic recommended)
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# Translation Workflow
|
|
2
|
+
|
|
3
|
+
Use this file when managing the .pot/.po/.mo translation file lifecycle.
|
|
4
|
+
|
|
5
|
+
## File types
|
|
6
|
+
|
|
7
|
+
| File | Purpose | Format |
|
|
8
|
+
|------|---------|--------|
|
|
9
|
+
| `.pot` | Template — master list of all translatable strings | Portable Object Template |
|
|
10
|
+
| `.po` | Translation — human-editable translations for a locale | Portable Object |
|
|
11
|
+
| `.mo` | Compiled — binary version loaded by PHP at runtime | Machine Object |
|
|
12
|
+
| `.json` | JS translations — JED format for `@wordpress/i18n` | JSON |
|
|
13
|
+
|
|
14
|
+
## Directory structure
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
my-plugin/
|
|
18
|
+
├── languages/
|
|
19
|
+
│ ├── my-text-domain.pot # Template
|
|
20
|
+
│ ├── my-text-domain-it_IT.po # Italian translations
|
|
21
|
+
│ ├── my-text-domain-it_IT.mo # Compiled Italian
|
|
22
|
+
│ ├── my-text-domain-it_IT-{hash}.json # JS Italian translations
|
|
23
|
+
│ └── my-text-domain-de_DE.po # German translations
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Naming convention: `{text-domain}-{locale}.{ext}`
|
|
27
|
+
|
|
28
|
+
Common locales: `it_IT`, `de_DE`, `fr_FR`, `es_ES`, `pt_BR`, `ja`, `zh_CN`, `ar`
|
|
29
|
+
|
|
30
|
+
## Step 1: Generate POT file
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
# From plugin root
|
|
34
|
+
wp i18n make-pot . languages/my-text-domain.pot
|
|
35
|
+
|
|
36
|
+
# With options
|
|
37
|
+
wp i18n make-pot . languages/my-text-domain.pot \
|
|
38
|
+
--slug=my-plugin \
|
|
39
|
+
--domain=my-text-domain \
|
|
40
|
+
--include="src/,includes/" \
|
|
41
|
+
--exclude="node_modules/,vendor/,tests/" \
|
|
42
|
+
--headers='{"Report-Msgid-Bugs-To":"https://github.com/user/repo/issues"}'
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
The `make-pot` command scans:
|
|
46
|
+
- PHP files for `__()`, `_e()`, `esc_html__()`, etc.
|
|
47
|
+
- JS/JSX files for `@wordpress/i18n` calls
|
|
48
|
+
- `block.json` files for translatable fields (title, description, keywords)
|
|
49
|
+
|
|
50
|
+
## Step 2: Create/update PO files
|
|
51
|
+
|
|
52
|
+
### From scratch
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
# Create a new PO file for Italian
|
|
56
|
+
msginit --input=languages/my-text-domain.pot \
|
|
57
|
+
--output-file=languages/my-text-domain-it_IT.po \
|
|
58
|
+
--locale=it_IT
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Update existing PO with new strings
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
# Merge new POT into existing PO (preserves existing translations)
|
|
65
|
+
wp i18n update-po languages/my-text-domain.pot languages/
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Or with `msgmerge`:
|
|
69
|
+
```bash
|
|
70
|
+
msgmerge --update languages/my-text-domain-it_IT.po languages/my-text-domain.pot
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Step 3: Translate
|
|
74
|
+
|
|
75
|
+
### Using a PO editor
|
|
76
|
+
|
|
77
|
+
Recommended tools:
|
|
78
|
+
- **Poedit** (desktop, free + pro) — the standard tool
|
|
79
|
+
- **GlotPress** (web-based, used by WordPress.org)
|
|
80
|
+
- **Loco Translate** (WordPress plugin, in-dashboard editing)
|
|
81
|
+
|
|
82
|
+
### PO file format
|
|
83
|
+
|
|
84
|
+
```po
|
|
85
|
+
#: src/includes/class-main.php:42
|
|
86
|
+
#. translators: %s: site name
|
|
87
|
+
msgid "Welcome to %s"
|
|
88
|
+
msgstr "Benvenuto su %s"
|
|
89
|
+
|
|
90
|
+
#: src/includes/class-main.php:55
|
|
91
|
+
msgid "Save Changes"
|
|
92
|
+
msgstr "Salva Modifiche"
|
|
93
|
+
|
|
94
|
+
#: src/includes/class-main.php:60
|
|
95
|
+
msgctxt "verb"
|
|
96
|
+
msgid "Post"
|
|
97
|
+
msgstr "Pubblica"
|
|
98
|
+
|
|
99
|
+
#: src/includes/class-main.php:70
|
|
100
|
+
msgid "%d item"
|
|
101
|
+
msgid_plural "%d items"
|
|
102
|
+
msgstr[0] "%d elemento"
|
|
103
|
+
msgstr[1] "%d elementi"
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Step 4: Compile MO files
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
# Compile all PO files to MO
|
|
110
|
+
wp i18n make-mo languages/
|
|
111
|
+
|
|
112
|
+
# Or with msgfmt
|
|
113
|
+
msgfmt languages/my-text-domain-it_IT.po -o languages/my-text-domain-it_IT.mo
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Step 5: Generate JSON for JavaScript
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
# Generate JSON from PO files (for wp_set_script_translations)
|
|
120
|
+
wp i18n make-json languages/ --no-purge
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
`--no-purge` keeps the JS strings in the PO file (useful if you also use them server-side).
|
|
124
|
+
|
|
125
|
+
## Automation: Build script integration
|
|
126
|
+
|
|
127
|
+
### package.json
|
|
128
|
+
|
|
129
|
+
```json
|
|
130
|
+
{
|
|
131
|
+
"scripts": {
|
|
132
|
+
"i18n:pot": "wp i18n make-pot . languages/my-text-domain.pot --exclude=node_modules/,vendor/",
|
|
133
|
+
"i18n:update": "wp i18n update-po languages/my-text-domain.pot languages/",
|
|
134
|
+
"i18n:mo": "wp i18n make-mo languages/",
|
|
135
|
+
"i18n:json": "wp i18n make-json languages/ --no-purge",
|
|
136
|
+
"i18n:build": "npm run i18n:pot && npm run i18n:update && npm run i18n:mo && npm run i18n:json"
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Pre-release checklist
|
|
142
|
+
|
|
143
|
+
1. Run `wp i18n make-pot` to capture all new strings
|
|
144
|
+
2. Run `wp i18n update-po` to merge into existing translations
|
|
145
|
+
3. Send updated PO files to translators
|
|
146
|
+
4. Compile MO files after translations are complete
|
|
147
|
+
5. Generate JSON files for JS strings
|
|
148
|
+
6. Test each locale by switching `WPLANG` in `wp-config.php`
|
|
149
|
+
|
|
150
|
+
## WordPress.org translation (GlotPress)
|
|
151
|
+
|
|
152
|
+
For plugins/themes hosted on WordPress.org:
|
|
153
|
+
1. Set `Text Domain` and `Domain Path` in the plugin header
|
|
154
|
+
2. Upload the POT file to SVN `trunk/languages/`
|
|
155
|
+
3. Translations are crowdsourced via translate.wordpress.org
|
|
156
|
+
4. Users receive translations automatically via WordPress updates
|
|
157
|
+
|
|
158
|
+
Plugin header:
|
|
159
|
+
```php
|
|
160
|
+
/**
|
|
161
|
+
* Plugin Name: My Plugin
|
|
162
|
+
* Text Domain: my-text-domain
|
|
163
|
+
* Domain Path: /languages
|
|
164
|
+
*/
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## Verification
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
# Check PO file for errors
|
|
171
|
+
msgfmt --check languages/my-text-domain-it_IT.po
|
|
172
|
+
|
|
173
|
+
# Count translated/untranslated strings
|
|
174
|
+
msgfmt --statistics languages/my-text-domain-it_IT.po
|
|
175
|
+
|
|
176
|
+
# Verify text domain consistency
|
|
177
|
+
grep -rn "__('" --include="*.php" src/ | grep -v "'my-text-domain'"
|
|
178
|
+
```
|