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.
Files changed (81) hide show
  1. package/.claude-plugin/plugin.json +7 -3
  2. package/CHANGELOG.md +111 -0
  3. package/README.md +10 -3
  4. package/agents/wp-accessibility-auditor.md +206 -0
  5. package/agents/wp-content-strategist.md +18 -0
  6. package/agents/wp-deployment-engineer.md +34 -2
  7. package/agents/wp-performance-optimizer.md +12 -0
  8. package/agents/wp-security-auditor.md +20 -0
  9. package/agents/wp-security-hardener.md +266 -0
  10. package/agents/wp-site-manager.md +14 -0
  11. package/agents/wp-test-engineer.md +207 -0
  12. package/docs/GUIDE.md +68 -15
  13. package/docs/guides/INDEX.md +46 -0
  14. package/docs/guides/wp-blog.md +590 -0
  15. package/docs/guides/wp-design-system.md +976 -0
  16. package/docs/guides/wp-ecommerce.md +786 -0
  17. package/docs/guides/wp-landing-page.md +762 -0
  18. package/docs/guides/wp-portfolio.md +713 -0
  19. package/docs/plans/2026-02-27-design-system-guide-design.md +30 -0
  20. package/docs/plans/2026-02-27-local-dev-tools-assessment.md +332 -0
  21. package/docs/plans/2026-02-27-local-env-design.md +179 -0
  22. package/docs/plans/2026-02-27-site-type-guides-design.md +44 -0
  23. package/package.json +7 -3
  24. package/skills/wordpress-router/SKILL.md +25 -5
  25. package/skills/wordpress-router/references/decision-tree.md +59 -3
  26. package/skills/wp-accessibility/SKILL.md +170 -0
  27. package/skills/wp-accessibility/references/a11y-audit-tools.md +248 -0
  28. package/skills/wp-accessibility/references/a11y-testing.md +222 -0
  29. package/skills/wp-accessibility/references/block-a11y.md +247 -0
  30. package/skills/wp-accessibility/references/interactive-a11y.md +272 -0
  31. package/skills/wp-accessibility/references/media-a11y.md +254 -0
  32. package/skills/wp-accessibility/references/theme-a11y.md +309 -0
  33. package/skills/wp-audit/SKILL.md +4 -0
  34. package/skills/wp-block-development/SKILL.md +5 -0
  35. package/skills/wp-block-themes/SKILL.md +4 -0
  36. package/skills/wp-deploy/SKILL.md +12 -0
  37. package/skills/wp-e2e-testing/SKILL.md +186 -0
  38. package/skills/wp-e2e-testing/references/ci-integration.md +174 -0
  39. package/skills/wp-e2e-testing/references/jest-wordpress.md +114 -0
  40. package/skills/wp-e2e-testing/references/phpunit-wordpress.md +141 -0
  41. package/skills/wp-e2e-testing/references/playwright-wordpress.md +108 -0
  42. package/skills/wp-e2e-testing/references/test-data-generation.md +127 -0
  43. package/skills/wp-e2e-testing/references/visual-regression.md +107 -0
  44. package/skills/wp-e2e-testing/references/wp-env-setup.md +97 -0
  45. package/skills/wp-e2e-testing/scripts/test_inspect.mjs +375 -0
  46. package/skills/wp-headless/SKILL.md +168 -0
  47. package/skills/wp-headless/references/api-layer-choice.md +160 -0
  48. package/skills/wp-headless/references/cors-config.md +245 -0
  49. package/skills/wp-headless/references/frontend-integration.md +331 -0
  50. package/skills/wp-headless/references/headless-auth.md +286 -0
  51. package/skills/wp-headless/references/webhooks.md +277 -0
  52. package/skills/wp-headless/references/wpgraphql.md +331 -0
  53. package/skills/wp-headless/scripts/headless_inspect.mjs +321 -0
  54. package/skills/wp-i18n/SKILL.md +170 -0
  55. package/skills/wp-i18n/references/js-i18n.md +201 -0
  56. package/skills/wp-i18n/references/multilingual-setup.md +219 -0
  57. package/skills/wp-i18n/references/php-i18n.md +196 -0
  58. package/skills/wp-i18n/references/rtl-support.md +206 -0
  59. package/skills/wp-i18n/references/translation-workflow.md +178 -0
  60. package/skills/wp-i18n/references/wpcli-i18n.md +177 -0
  61. package/skills/wp-i18n/scripts/i18n_inspect.mjs +330 -0
  62. package/skills/wp-interactivity-api/SKILL.md +4 -0
  63. package/skills/wp-local-env/SKILL.md +233 -0
  64. package/skills/wp-local-env/references/localwp-adapter.md +156 -0
  65. package/skills/wp-local-env/references/mcp-adapter-setup.md +153 -0
  66. package/skills/wp-local-env/references/studio-adapter.md +127 -0
  67. package/skills/wp-local-env/references/wpenv-adapter.md +121 -0
  68. package/skills/wp-local-env/scripts/detect_local_env.mjs +404 -0
  69. package/skills/wp-playground/SKILL.md +13 -1
  70. package/skills/wp-plugin-development/SKILL.md +6 -0
  71. package/skills/wp-rest-api/SKILL.md +4 -0
  72. package/skills/wp-security/SKILL.md +179 -0
  73. package/skills/wp-security/references/api-restriction.md +147 -0
  74. package/skills/wp-security/references/authentication-hardening.md +105 -0
  75. package/skills/wp-security/references/filesystem-hardening.md +105 -0
  76. package/skills/wp-security/references/http-headers.md +105 -0
  77. package/skills/wp-security/references/incident-response.md +144 -0
  78. package/skills/wp-security/references/user-capabilities.md +115 -0
  79. package/skills/wp-security/references/wp-config-security.md +129 -0
  80. package/skills/wp-security/scripts/security_inspect.mjs +393 -0
  81. 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