claude-plugin-wordpress-manager 1.4.0 → 1.7.0
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 +7 -3
- package/CHANGELOG.md +111 -0
- package/README.md +10 -3
- 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/GUIDE.md +68 -15
- 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-local-dev-tools-assessment.md +332 -0
- package/docs/plans/2026-02-27-local-env-design.md +179 -0
- package/docs/plans/2026-02-27-site-type-guides-design.md +44 -0
- package/package.json +7 -3
- package/skills/wordpress-router/SKILL.md +25 -5
- package/skills/wordpress-router/references/decision-tree.md +59 -3
- 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-deploy/SKILL.md +12 -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-local-env/SKILL.md +233 -0
- package/skills/wp-local-env/references/localwp-adapter.md +156 -0
- package/skills/wp-local-env/references/mcp-adapter-setup.md +153 -0
- package/skills/wp-local-env/references/studio-adapter.md +127 -0
- package/skills/wp-local-env/references/wpenv-adapter.md +121 -0
- package/skills/wp-local-env/scripts/detect_local_env.mjs +404 -0
- package/skills/wp-playground/SKILL.md +13 -1
- 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
- package/skills/wp-wpcli-and-ops/SKILL.md +6 -0
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
# Multilingual Setup
|
|
2
|
+
|
|
3
|
+
Use this file when configuring WordPress for multilingual content delivery.
|
|
4
|
+
|
|
5
|
+
## Plugin-based solutions
|
|
6
|
+
|
|
7
|
+
### WPML (commercial)
|
|
8
|
+
|
|
9
|
+
Most widely used. Supports posts, pages, custom post types, taxonomies, strings, menus, and widgets.
|
|
10
|
+
|
|
11
|
+
```php
|
|
12
|
+
// Check if WPML is active
|
|
13
|
+
if (defined('ICL_SITEPRESS_VERSION')) {
|
|
14
|
+
// WPML-specific code
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Get current language
|
|
18
|
+
$current_lang = apply_filters('wpml_current_language', null);
|
|
19
|
+
|
|
20
|
+
// Get element translation
|
|
21
|
+
$translated_id = apply_filters('wpml_object_id', $post_id, 'post', true, 'it');
|
|
22
|
+
|
|
23
|
+
// Switch language in a template
|
|
24
|
+
do_action('wpml_switch_lang', 'it');
|
|
25
|
+
// ... output Italian content
|
|
26
|
+
do_action('wpml_switch_lang', null); // reset
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Theme/plugin compatibility header:
|
|
30
|
+
```php
|
|
31
|
+
/**
|
|
32
|
+
* Plugin Name: My Plugin
|
|
33
|
+
* WPML Compatible: yes
|
|
34
|
+
*/
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Polylang (free + pro)
|
|
38
|
+
|
|
39
|
+
```php
|
|
40
|
+
// Check if Polylang is active
|
|
41
|
+
if (function_exists('pll_current_language')) {
|
|
42
|
+
$lang = pll_current_language();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Get translated post ID
|
|
46
|
+
$translated_id = pll_get_post($post_id, 'it');
|
|
47
|
+
|
|
48
|
+
// Get translated term ID
|
|
49
|
+
$translated_term = pll_get_term($term_id, 'it');
|
|
50
|
+
|
|
51
|
+
// Register a string for translation
|
|
52
|
+
pll_register_string('my-string-name', 'Default text', 'My Plugin');
|
|
53
|
+
|
|
54
|
+
// Get translated string
|
|
55
|
+
$text = pll__('Default text');
|
|
56
|
+
// or echo
|
|
57
|
+
pll_e('Default text');
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### TranslatePress (visual)
|
|
61
|
+
|
|
62
|
+
Frontend visual editing — translates directly on the page.
|
|
63
|
+
|
|
64
|
+
```php
|
|
65
|
+
// Check if TranslatePress is active
|
|
66
|
+
if (class_exists('TRP_Translate_Press')) {
|
|
67
|
+
// TranslatePress-specific code
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## URL structure options
|
|
72
|
+
|
|
73
|
+
| Strategy | Example | Pros | Cons |
|
|
74
|
+
|----------|---------|------|------|
|
|
75
|
+
| Subdirectories | `example.com/it/` | Simple, single domain | Shared hosting limits |
|
|
76
|
+
| Subdomains | `it.example.com` | Separate analytics | DNS setup required |
|
|
77
|
+
| Domains | `example.it` | Best for SEO by country | Multiple SSL certs |
|
|
78
|
+
| URL parameter | `example.com?lang=it` | Easiest setup | Worst for SEO |
|
|
79
|
+
|
|
80
|
+
Recommended: **subdirectories** for most projects.
|
|
81
|
+
|
|
82
|
+
## Making plugins translation-compatible
|
|
83
|
+
|
|
84
|
+
### String registration (WPML)
|
|
85
|
+
|
|
86
|
+
```php
|
|
87
|
+
// Register admin strings
|
|
88
|
+
add_action('init', function() {
|
|
89
|
+
if (function_exists('icl_register_string')) {
|
|
90
|
+
icl_register_string('my-plugin', 'CTA Button Text', get_option('my_cta_text'));
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// Retrieve translated string
|
|
95
|
+
function get_my_cta_text() {
|
|
96
|
+
$text = get_option('my_cta_text');
|
|
97
|
+
if (function_exists('icl_t')) {
|
|
98
|
+
return icl_t('my-plugin', 'CTA Button Text', $text);
|
|
99
|
+
}
|
|
100
|
+
return $text;
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### wpml-config.xml
|
|
105
|
+
|
|
106
|
+
Required for WPML to know which custom fields and options to translate:
|
|
107
|
+
|
|
108
|
+
```xml
|
|
109
|
+
<wpml-config>
|
|
110
|
+
<custom-fields>
|
|
111
|
+
<custom-field action="translate">subtitle</custom-field>
|
|
112
|
+
<custom-field action="copy">price</custom-field>
|
|
113
|
+
<custom-field action="ignore">internal_notes</custom-field>
|
|
114
|
+
</custom-fields>
|
|
115
|
+
<admin-texts>
|
|
116
|
+
<key name="my_plugin_options">
|
|
117
|
+
<key name="cta_text" />
|
|
118
|
+
<key name="footer_text" />
|
|
119
|
+
</key>
|
|
120
|
+
</admin-texts>
|
|
121
|
+
<custom-types>
|
|
122
|
+
<custom-type translate="1">product</custom-type>
|
|
123
|
+
</custom-types>
|
|
124
|
+
<taxonomies>
|
|
125
|
+
<taxonomy translate="1">product_category</taxonomy>
|
|
126
|
+
</taxonomies>
|
|
127
|
+
</wpml-config>
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Place at plugin or theme root. WPML reads it automatically.
|
|
131
|
+
|
|
132
|
+
## Multilingual REST API
|
|
133
|
+
|
|
134
|
+
### WPML
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
# Get posts in Italian
|
|
138
|
+
curl "https://site.com/wp-json/wp/v2/posts?lang=it"
|
|
139
|
+
|
|
140
|
+
# Requires WPML REST API addon or custom filter
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Polylang
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
# Polylang adds ?lang= support to REST API
|
|
147
|
+
curl "https://site.com/wp-json/wp/v2/posts?lang=it"
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Custom REST language filtering
|
|
151
|
+
|
|
152
|
+
```php
|
|
153
|
+
add_filter('rest_post_query', function($args, $request) {
|
|
154
|
+
$lang = $request->get_param('lang');
|
|
155
|
+
if ($lang && function_exists('pll_current_language')) {
|
|
156
|
+
$args['lang'] = $lang;
|
|
157
|
+
}
|
|
158
|
+
return $args;
|
|
159
|
+
}, 10, 2);
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## hreflang tags
|
|
163
|
+
|
|
164
|
+
Essential for SEO — tells search engines which language version to show:
|
|
165
|
+
|
|
166
|
+
```php
|
|
167
|
+
add_action('wp_head', function() {
|
|
168
|
+
// WPML handles this automatically
|
|
169
|
+
// For custom implementations:
|
|
170
|
+
$languages = [
|
|
171
|
+
'en' => 'https://example.com/page/',
|
|
172
|
+
'it' => 'https://example.com/it/pagina/',
|
|
173
|
+
'de' => 'https://example.com/de/seite/',
|
|
174
|
+
];
|
|
175
|
+
foreach ($languages as $lang => $url) {
|
|
176
|
+
printf('<link rel="alternate" hreflang="%s" href="%s" />' . "\n",
|
|
177
|
+
esc_attr($lang), esc_url($url));
|
|
178
|
+
}
|
|
179
|
+
// x-default for language selector/default page
|
|
180
|
+
printf('<link rel="alternate" hreflang="x-default" href="%s" />' . "\n",
|
|
181
|
+
esc_url($languages['en']));
|
|
182
|
+
});
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## WooCommerce multilingual
|
|
186
|
+
|
|
187
|
+
### WPML + WooCommerce Multilingual
|
|
188
|
+
|
|
189
|
+
- Synchronizes products, variations, attributes across languages
|
|
190
|
+
- Multi-currency support
|
|
191
|
+
- Translated emails and checkout
|
|
192
|
+
|
|
193
|
+
```php
|
|
194
|
+
// Get translated product ID
|
|
195
|
+
$translated_product_id = apply_filters('wpml_object_id', $product_id, 'product', true, 'it');
|
|
196
|
+
|
|
197
|
+
// Get price in current currency
|
|
198
|
+
// Handled automatically by WooCommerce Multilingual
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### Polylang + Hyyan WooCommerce Polylang Integration
|
|
202
|
+
|
|
203
|
+
Free alternative. Syncs stock, prices, and product data.
|
|
204
|
+
|
|
205
|
+
## Verification
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
# Check hreflang tags
|
|
209
|
+
curl -s https://site.com/ | grep -i "hreflang"
|
|
210
|
+
|
|
211
|
+
# Verify language switcher works
|
|
212
|
+
curl -s -o /dev/null -w "%{http_code}" https://site.com/it/
|
|
213
|
+
|
|
214
|
+
# Check WPML config file exists (for WPML-compatible plugins)
|
|
215
|
+
ls wpml-config.xml
|
|
216
|
+
|
|
217
|
+
# Verify translated content exists
|
|
218
|
+
wp post list --lang=it --post_type=page --fields=ID,post_title
|
|
219
|
+
```
|
|
@@ -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)
|