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,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
|
+
```
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# WP-CLI i18n Commands
|
|
2
|
+
|
|
3
|
+
Use this file when working with WP-CLI's `i18n` command group for translation management.
|
|
4
|
+
|
|
5
|
+
## Prerequisites
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# WP-CLI i18n is built-in since WP-CLI 2.0
|
|
9
|
+
wp i18n --help
|
|
10
|
+
|
|
11
|
+
# If missing, install the i18n command package
|
|
12
|
+
wp package install wp-cli/i18n-command
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## make-pot — Generate POT template
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
# Basic usage (from plugin/theme root)
|
|
19
|
+
wp i18n make-pot . languages/my-text-domain.pot
|
|
20
|
+
|
|
21
|
+
# Full options
|
|
22
|
+
wp i18n make-pot . languages/my-text-domain.pot \
|
|
23
|
+
--slug=my-plugin \
|
|
24
|
+
--domain=my-text-domain \
|
|
25
|
+
--include="src/,includes/,templates/" \
|
|
26
|
+
--exclude="node_modules/,vendor/,tests/,build/" \
|
|
27
|
+
--skip-js \
|
|
28
|
+
--skip-php \
|
|
29
|
+
--skip-block-json \
|
|
30
|
+
--skip-theme-json \
|
|
31
|
+
--skip-audit \
|
|
32
|
+
--headers='{"Report-Msgid-Bugs-To":"support@example.com","Last-Translator":"Dev Team"}' \
|
|
33
|
+
--file-comment="Copyright (c) 2026 My Company"
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Key options
|
|
37
|
+
|
|
38
|
+
| Option | Purpose |
|
|
39
|
+
|--------|---------|
|
|
40
|
+
| `--domain=<domain>` | Only extract strings with this text domain |
|
|
41
|
+
| `--include=<paths>` | Comma-separated list of directories to scan |
|
|
42
|
+
| `--exclude=<paths>` | Comma-separated list of directories to skip |
|
|
43
|
+
| `--skip-js` | Skip JavaScript file scanning |
|
|
44
|
+
| `--skip-php` | Skip PHP file scanning |
|
|
45
|
+
| `--skip-block-json` | Skip block.json translation extraction |
|
|
46
|
+
| `--skip-theme-json` | Skip theme.json translation extraction |
|
|
47
|
+
| `--skip-audit` | Skip string auditing (faster, no warnings) |
|
|
48
|
+
| `--headers` | JSON object of PO headers |
|
|
49
|
+
|
|
50
|
+
### What it scans
|
|
51
|
+
|
|
52
|
+
- PHP: `__()`, `_e()`, `_x()`, `_ex()`, `_n()`, `_nx()`, `esc_html__()`, `esc_html_e()`, `esc_attr__()`, `esc_attr_e()`, `esc_html_x()`, `esc_attr_x()`
|
|
53
|
+
- JS: `@wordpress/i18n` functions via static analysis
|
|
54
|
+
- block.json: `title`, `description`, `keywords`, `styles[].label`, `variations[].title`
|
|
55
|
+
- theme.json: Custom template names, style variation names
|
|
56
|
+
|
|
57
|
+
## update-po — Merge new strings into PO files
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
# Update all PO files in the directory
|
|
61
|
+
wp i18n update-po languages/my-text-domain.pot languages/
|
|
62
|
+
|
|
63
|
+
# Update a specific PO file
|
|
64
|
+
wp i18n update-po languages/my-text-domain.pot languages/my-text-domain-it_IT.po
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
This is equivalent to `msgmerge --update` but integrated into WP-CLI. Preserves existing translations and marks removed strings as obsolete.
|
|
68
|
+
|
|
69
|
+
## make-mo — Compile PO to MO
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
# Compile all PO files in directory
|
|
73
|
+
wp i18n make-mo languages/
|
|
74
|
+
|
|
75
|
+
# Compile a specific file
|
|
76
|
+
wp i18n make-mo languages/my-text-domain-it_IT.po
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## make-json — Generate JS translation files
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
# Generate JSON files for all PO files
|
|
83
|
+
wp i18n make-json languages/
|
|
84
|
+
|
|
85
|
+
# Keep JS strings in PO files (don't purge)
|
|
86
|
+
wp i18n make-json languages/ --no-purge
|
|
87
|
+
|
|
88
|
+
# Pretty-print JSON output
|
|
89
|
+
wp i18n make-json languages/ --pretty-print
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Output: `{text-domain}-{locale}-{md5}.json` where `{md5}` is the hash of the relative JS file path.
|
|
93
|
+
|
|
94
|
+
### When to use `--no-purge`
|
|
95
|
+
|
|
96
|
+
- Use `--no-purge` if strings appear in both PHP and JS files
|
|
97
|
+
- Without it, JS-only strings are removed from PO, breaking PHP translations if shared
|
|
98
|
+
|
|
99
|
+
## make-php — Generate PHP translation files (WP 6.5+)
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
# Convert PO files to PHP format
|
|
103
|
+
wp i18n make-php languages/
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
PHP translation files load faster than MO files. WordPress 6.5+ supports this format natively.
|
|
107
|
+
|
|
108
|
+
## Complete workflow example
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
# 1. Generate fresh POT
|
|
112
|
+
wp i18n make-pot . languages/my-text-domain.pot \
|
|
113
|
+
--exclude="node_modules/,vendor/"
|
|
114
|
+
|
|
115
|
+
# 2. Update existing translations
|
|
116
|
+
wp i18n update-po languages/my-text-domain.pot languages/
|
|
117
|
+
|
|
118
|
+
# 3. (Translate the PO files — manual or via Poedit)
|
|
119
|
+
|
|
120
|
+
# 4. Compile MO files
|
|
121
|
+
wp i18n make-mo languages/
|
|
122
|
+
|
|
123
|
+
# 5. Generate JSON for JavaScript
|
|
124
|
+
wp i18n make-json languages/ --no-purge
|
|
125
|
+
|
|
126
|
+
# 6. (Optional) Generate PHP translations
|
|
127
|
+
wp i18n make-php languages/
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Audit strings
|
|
131
|
+
|
|
132
|
+
The `make-pot` command includes a string auditor. Common warnings:
|
|
133
|
+
|
|
134
|
+
| Warning | Meaning |
|
|
135
|
+
|---------|---------|
|
|
136
|
+
| `Mismatched placeholders` | Printf placeholders differ between singular/plural |
|
|
137
|
+
| `Multiple text domains` | File mixes text domains |
|
|
138
|
+
| `Missing translator comment` | Placeholder string without `/* translators: */` |
|
|
139
|
+
|
|
140
|
+
Run the audit explicitly:
|
|
141
|
+
```bash
|
|
142
|
+
wp i18n make-pot . /dev/null --skip-js
|
|
143
|
+
# Warnings are printed to stderr
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## Language management
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
# Install a language pack for core
|
|
150
|
+
wp language core install it_IT
|
|
151
|
+
|
|
152
|
+
# Set site language
|
|
153
|
+
wp site switch-language it_IT
|
|
154
|
+
|
|
155
|
+
# List installed languages
|
|
156
|
+
wp language core list --status=installed
|
|
157
|
+
|
|
158
|
+
# Install plugin language pack
|
|
159
|
+
wp language plugin install my-plugin it_IT
|
|
160
|
+
|
|
161
|
+
# Install theme language pack
|
|
162
|
+
wp language theme install my-theme it_IT
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## Verification
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
# Verify POT is up to date (compare counts)
|
|
169
|
+
wp i18n make-pot . /tmp/fresh.pot --quiet
|
|
170
|
+
diff <(grep -c "^msgid" languages/my-text-domain.pot) <(grep -c "^msgid" /tmp/fresh.pot)
|
|
171
|
+
|
|
172
|
+
# Check MO files are current (MO should be newer than PO)
|
|
173
|
+
find languages/ -name "*.po" -newer languages/*.mo
|
|
174
|
+
|
|
175
|
+
# Verify JSON files exist for JS translations
|
|
176
|
+
ls languages/*.json
|
|
177
|
+
```
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* i18n_inspect.mjs — Detect internationalization setup in a WordPress project.
|
|
3
|
+
*
|
|
4
|
+
* Scans for text domain, .pot/.po/.mo files, i18n function usage,
|
|
5
|
+
* and WP-CLI i18n availability.
|
|
6
|
+
* Outputs a JSON report to stdout.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* node i18n_inspect.mjs [--cwd=/path/to/check]
|
|
10
|
+
*
|
|
11
|
+
* Exit codes:
|
|
12
|
+
* 0 — i18n setup detected
|
|
13
|
+
* 1 — no i18n setup detected
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import fs from "node:fs";
|
|
17
|
+
import path from "node:path";
|
|
18
|
+
import process from "node:process";
|
|
19
|
+
import { execSync } from "node:child_process";
|
|
20
|
+
|
|
21
|
+
const TOOL_VERSION = "1.0.0";
|
|
22
|
+
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// Helpers
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
function statSafe(p) {
|
|
28
|
+
try {
|
|
29
|
+
return fs.statSync(p);
|
|
30
|
+
} catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function readFileSafe(p) {
|
|
36
|
+
try {
|
|
37
|
+
return fs.readFileSync(p, "utf8");
|
|
38
|
+
} catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function readJsonSafe(p) {
|
|
44
|
+
const raw = readFileSafe(p);
|
|
45
|
+
if (!raw) return null;
|
|
46
|
+
try {
|
|
47
|
+
return JSON.parse(raw);
|
|
48
|
+
} catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function execSafe(cmd, cwd, timeoutMs = 5000) {
|
|
54
|
+
try {
|
|
55
|
+
return execSync(cmd, { encoding: "utf8", timeout: timeoutMs, cwd, stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
56
|
+
} catch {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function readdirSafe(dir) {
|
|
62
|
+
try {
|
|
63
|
+
return fs.readdirSync(dir);
|
|
64
|
+
} catch {
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
// Parse --cwd argument
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
function parseCwd() {
|
|
74
|
+
const cwdArg = process.argv.find((a) => a.startsWith("--cwd="));
|
|
75
|
+
return cwdArg ? cwdArg.slice(6) : process.cwd();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
// Detect text domain
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
function detectTextDomain(cwd) {
|
|
83
|
+
const result = { found: false, domain: null, source: null };
|
|
84
|
+
|
|
85
|
+
// Check plugin header
|
|
86
|
+
const phpFiles = readdirSafe(cwd).filter((f) => f.endsWith(".php"));
|
|
87
|
+
for (const file of phpFiles) {
|
|
88
|
+
const content = readFileSafe(path.join(cwd, file));
|
|
89
|
+
if (!content) continue;
|
|
90
|
+
|
|
91
|
+
const domainMatch = content.match(/Text\s*Domain:\s*(\S+)/i);
|
|
92
|
+
if (domainMatch) {
|
|
93
|
+
result.found = true;
|
|
94
|
+
result.domain = domainMatch[1];
|
|
95
|
+
result.source = file;
|
|
96
|
+
return result;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Check style.css (theme)
|
|
101
|
+
const styleContent = readFileSafe(path.join(cwd, "style.css"));
|
|
102
|
+
if (styleContent) {
|
|
103
|
+
const domainMatch = styleContent.match(/Text\s*Domain:\s*(\S+)/i);
|
|
104
|
+
if (domainMatch) {
|
|
105
|
+
result.found = true;
|
|
106
|
+
result.domain = domainMatch[1];
|
|
107
|
+
result.source = "style.css";
|
|
108
|
+
return result;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Check block.json files
|
|
113
|
+
const blockJson = readJsonSafe(path.join(cwd, "block.json"));
|
|
114
|
+
if (blockJson?.textdomain) {
|
|
115
|
+
result.found = true;
|
|
116
|
+
result.domain = blockJson.textdomain;
|
|
117
|
+
result.source = "block.json";
|
|
118
|
+
return result;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const srcBlockJson = readJsonSafe(path.join(cwd, "src", "block.json"));
|
|
122
|
+
if (srcBlockJson?.textdomain) {
|
|
123
|
+
result.found = true;
|
|
124
|
+
result.domain = srcBlockJson.textdomain;
|
|
125
|
+
result.source = "src/block.json";
|
|
126
|
+
return result;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return result;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
// Detect translation files
|
|
134
|
+
// ---------------------------------------------------------------------------
|
|
135
|
+
|
|
136
|
+
function detectTranslationFiles(cwd) {
|
|
137
|
+
const result = { languagesDir: null, pot: [], po: [], mo: [], json: [] };
|
|
138
|
+
|
|
139
|
+
const langDirs = ["languages", "lang", "i18n"];
|
|
140
|
+
for (const dir of langDirs) {
|
|
141
|
+
const full = path.join(cwd, dir);
|
|
142
|
+
if (statSafe(full)?.isDirectory()) {
|
|
143
|
+
result.languagesDir = dir;
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (!result.languagesDir) return result;
|
|
149
|
+
|
|
150
|
+
const files = readdirSafe(path.join(cwd, result.languagesDir));
|
|
151
|
+
for (const file of files) {
|
|
152
|
+
if (file.endsWith(".pot")) result.pot.push(file);
|
|
153
|
+
else if (file.endsWith(".po")) result.po.push(file);
|
|
154
|
+
else if (file.endsWith(".mo")) result.mo.push(file);
|
|
155
|
+
else if (file.endsWith(".json") && !file.startsWith(".")) result.json.push(file);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return result;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
// Detect i18n function usage
|
|
163
|
+
// ---------------------------------------------------------------------------
|
|
164
|
+
|
|
165
|
+
function detectI18nUsage(cwd) {
|
|
166
|
+
const result = { php: { detected: false, functions: {} }, js: { detected: false, functions: {} } };
|
|
167
|
+
|
|
168
|
+
// PHP i18n functions
|
|
169
|
+
const phpFunctions = ["__", "_e", "_x", "_ex", "_n", "_nx", "esc_html__", "esc_html_e", "esc_attr__", "esc_attr_e"];
|
|
170
|
+
const phpPattern = phpFunctions.map((f) => f.replace(/_/g, "_")).join("|");
|
|
171
|
+
const phpCount = execSafe(
|
|
172
|
+
`grep -rl --include="*.php" -E "(${phpPattern})\\s*\\(" . 2>/dev/null | wc -l`,
|
|
173
|
+
cwd
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
if (phpCount && parseInt(phpCount) > 0) {
|
|
177
|
+
result.php.detected = true;
|
|
178
|
+
|
|
179
|
+
// Count per function
|
|
180
|
+
for (const func of phpFunctions) {
|
|
181
|
+
const count = execSafe(
|
|
182
|
+
`grep -r --include="*.php" -c "${func}(" . 2>/dev/null | awk -F: '{s+=$2} END {print s}'`,
|
|
183
|
+
cwd
|
|
184
|
+
);
|
|
185
|
+
if (count && parseInt(count) > 0) {
|
|
186
|
+
result.php.functions[func] = parseInt(count);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// JS i18n functions (@wordpress/i18n)
|
|
192
|
+
const jsCount = execSafe(
|
|
193
|
+
`grep -rl --include="*.js" --include="*.jsx" --include="*.ts" --include="*.tsx" "@wordpress/i18n" . 2>/dev/null | wc -l`,
|
|
194
|
+
cwd
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
if (jsCount && parseInt(jsCount) > 0) {
|
|
198
|
+
result.js.detected = true;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Also check for import
|
|
202
|
+
const jsImportCount = execSafe(
|
|
203
|
+
`grep -rl --include="*.js" --include="*.jsx" --include="*.ts" --include="*.tsx" "from '@wordpress/i18n'" . 2>/dev/null | wc -l`,
|
|
204
|
+
cwd
|
|
205
|
+
);
|
|
206
|
+
if (jsImportCount && parseInt(jsImportCount) > 0) {
|
|
207
|
+
result.js.detected = true;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return result;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// ---------------------------------------------------------------------------
|
|
214
|
+
// Detect Domain Path header
|
|
215
|
+
// ---------------------------------------------------------------------------
|
|
216
|
+
|
|
217
|
+
function detectDomainPath(cwd) {
|
|
218
|
+
const phpFiles = readdirSafe(cwd).filter((f) => f.endsWith(".php"));
|
|
219
|
+
for (const file of phpFiles) {
|
|
220
|
+
const content = readFileSafe(path.join(cwd, file));
|
|
221
|
+
if (!content) continue;
|
|
222
|
+
const match = content.match(/Domain\s*Path:\s*(\S+)/i);
|
|
223
|
+
if (match) return match[1];
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const styleContent = readFileSafe(path.join(cwd, "style.css"));
|
|
227
|
+
if (styleContent) {
|
|
228
|
+
const match = styleContent.match(/Domain\s*Path:\s*(\S+)/i);
|
|
229
|
+
if (match) return match[1];
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ---------------------------------------------------------------------------
|
|
236
|
+
// Detect load_textdomain calls
|
|
237
|
+
// ---------------------------------------------------------------------------
|
|
238
|
+
|
|
239
|
+
function detectTextdomainLoading(cwd) {
|
|
240
|
+
const functions = [
|
|
241
|
+
"load_plugin_textdomain",
|
|
242
|
+
"load_theme_textdomain",
|
|
243
|
+
"load_child_theme_textdomain",
|
|
244
|
+
"wp_set_script_translations",
|
|
245
|
+
];
|
|
246
|
+
|
|
247
|
+
const detected = [];
|
|
248
|
+
for (const func of functions) {
|
|
249
|
+
const found = execSafe(
|
|
250
|
+
`grep -rl --include="*.php" "${func}" . 2>/dev/null | head -1`,
|
|
251
|
+
cwd
|
|
252
|
+
);
|
|
253
|
+
if (found) detected.push(func);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return detected;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// ---------------------------------------------------------------------------
|
|
260
|
+
// Check WP-CLI i18n availability
|
|
261
|
+
// ---------------------------------------------------------------------------
|
|
262
|
+
|
|
263
|
+
function checkWpCliI18n(cwd) {
|
|
264
|
+
const wpCli = execSafe("command -v wp", cwd);
|
|
265
|
+
if (!wpCli) return { available: false };
|
|
266
|
+
|
|
267
|
+
const i18nHelp = execSafe("wp i18n --help 2>/dev/null", cwd);
|
|
268
|
+
return { available: !!i18nHelp };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// ---------------------------------------------------------------------------
|
|
272
|
+
// Main
|
|
273
|
+
// ---------------------------------------------------------------------------
|
|
274
|
+
|
|
275
|
+
function main() {
|
|
276
|
+
const cwd = parseCwd();
|
|
277
|
+
|
|
278
|
+
if (!statSafe(cwd)?.isDirectory()) {
|
|
279
|
+
console.error(`Error: directory not found: ${cwd}`);
|
|
280
|
+
process.exit(1);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const textDomain = detectTextDomain(cwd);
|
|
284
|
+
const translationFiles = detectTranslationFiles(cwd);
|
|
285
|
+
const i18nUsage = detectI18nUsage(cwd);
|
|
286
|
+
const domainPath = detectDomainPath(cwd);
|
|
287
|
+
const textdomainLoading = detectTextdomainLoading(cwd);
|
|
288
|
+
const wpCliI18n = checkWpCliI18n(cwd);
|
|
289
|
+
|
|
290
|
+
const detected = textDomain.found || i18nUsage.php.detected || i18nUsage.js.detected || translationFiles.pot.length > 0;
|
|
291
|
+
|
|
292
|
+
const report = {
|
|
293
|
+
tool: "i18n_inspect",
|
|
294
|
+
version: TOOL_VERSION,
|
|
295
|
+
cwd,
|
|
296
|
+
detected,
|
|
297
|
+
textDomain,
|
|
298
|
+
domainPath,
|
|
299
|
+
textdomainLoading,
|
|
300
|
+
translationFiles,
|
|
301
|
+
i18nUsage,
|
|
302
|
+
wpCliI18n,
|
|
303
|
+
recommendations: [],
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
// Recommendations
|
|
307
|
+
if (!textDomain.found && (i18nUsage.php.detected || i18nUsage.js.detected)) {
|
|
308
|
+
report.recommendations.push("i18n functions used but no Text Domain header found. Add 'Text Domain:' to plugin/theme header.");
|
|
309
|
+
}
|
|
310
|
+
if (textDomain.found && translationFiles.pot.length === 0) {
|
|
311
|
+
report.recommendations.push("Text domain set but no .pot file found. Run: wp i18n make-pot . languages/" + textDomain.domain + ".pot");
|
|
312
|
+
}
|
|
313
|
+
if (translationFiles.po.length > 0 && translationFiles.mo.length === 0) {
|
|
314
|
+
report.recommendations.push("PO files found but no compiled MO files. Run: wp i18n make-mo languages/");
|
|
315
|
+
}
|
|
316
|
+
if (i18nUsage.js.detected && translationFiles.json.length === 0) {
|
|
317
|
+
report.recommendations.push("JS i18n detected but no JSON translation files. Run: wp i18n make-json languages/ --no-purge");
|
|
318
|
+
}
|
|
319
|
+
if (textDomain.found && textdomainLoading.length === 0) {
|
|
320
|
+
report.recommendations.push("Text domain found but no load_plugin_textdomain/load_theme_textdomain call detected.");
|
|
321
|
+
}
|
|
322
|
+
if (!domainPath && textDomain.found) {
|
|
323
|
+
report.recommendations.push("Consider adding 'Domain Path: /languages' to the plugin/theme header.");
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
console.log(JSON.stringify(report, null, 2));
|
|
327
|
+
process.exit(detected ? 0 : 1);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
main();
|
|
@@ -179,3 +179,7 @@ See `references/debugging.md`.
|
|
|
179
179
|
- `references/server-side-rendering.md`
|
|
180
180
|
- `references/directives-quickref.md`
|
|
181
181
|
- `references/debugging.md`
|
|
182
|
+
|
|
183
|
+
## Related skills
|
|
184
|
+
|
|
185
|
+
- `wp-accessibility` — ARIA for interactive components, keyboard navigation, focus management, screen reader testing
|