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,321 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* headless_inspect.mjs — Detect headless WordPress configuration.
|
|
3
|
+
*
|
|
4
|
+
* Scans for WPGraphQL, CORS config, frontend framework integration,
|
|
5
|
+
* and decoupled architecture indicators.
|
|
6
|
+
* Outputs a JSON report to stdout.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* node headless_inspect.mjs [--cwd=/path/to/check]
|
|
10
|
+
*
|
|
11
|
+
* Exit codes:
|
|
12
|
+
* 0 — headless indicators detected
|
|
13
|
+
* 1 — no headless indicators 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 WPGraphQL
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
function detectWPGraphQL(cwd) {
|
|
83
|
+
const result = { detected: false, plugins: [] };
|
|
84
|
+
|
|
85
|
+
const pluginsDir = path.join(cwd, "wp-content", "plugins");
|
|
86
|
+
const graphqlPlugins = [
|
|
87
|
+
{ dir: "wp-graphql", name: "WPGraphQL" },
|
|
88
|
+
{ dir: "wpgraphql-acf", name: "WPGraphQL for ACF" },
|
|
89
|
+
{ dir: "wp-graphql-jwt-authentication", name: "WPGraphQL JWT Auth" },
|
|
90
|
+
{ dir: "wp-graphql-smart-cache", name: "WPGraphQL Smart Cache" },
|
|
91
|
+
{ dir: "wp-graphql-woocommerce", name: "WPGraphQL WooCommerce" },
|
|
92
|
+
{ dir: "wp-gatsby", name: "WP Gatsby" },
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
for (const plugin of graphqlPlugins) {
|
|
96
|
+
if (statSafe(path.join(pluginsDir, plugin.dir))?.isDirectory()) {
|
|
97
|
+
result.detected = true;
|
|
98
|
+
result.plugins.push(plugin.name);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Check composer.json for WPGraphQL
|
|
103
|
+
const composer = readJsonSafe(path.join(cwd, "composer.json"));
|
|
104
|
+
if (composer) {
|
|
105
|
+
const allDeps = { ...composer.require, ...composer["require-dev"] };
|
|
106
|
+
if (allDeps["wp-graphql/wp-graphql"]) {
|
|
107
|
+
result.detected = true;
|
|
108
|
+
if (!result.plugins.includes("WPGraphQL")) result.plugins.push("WPGraphQL (composer)");
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return result;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
// Detect CORS configuration
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
|
|
119
|
+
function detectCORS(cwd) {
|
|
120
|
+
const result = { detected: false, sources: [] };
|
|
121
|
+
|
|
122
|
+
// Check .htaccess
|
|
123
|
+
const htaccess = readFileSafe(path.join(cwd, ".htaccess"));
|
|
124
|
+
if (htaccess && /Access-Control-Allow-Origin/i.test(htaccess)) {
|
|
125
|
+
result.detected = true;
|
|
126
|
+
result.sources.push(".htaccess");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Check wp-config.php for CORS constants
|
|
130
|
+
const wpConfig = readFileSafe(path.join(cwd, "wp-config.php"));
|
|
131
|
+
if (wpConfig && /CORS|Access-Control/i.test(wpConfig)) {
|
|
132
|
+
result.detected = true;
|
|
133
|
+
result.sources.push("wp-config.php");
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Check PHP files for CORS headers
|
|
137
|
+
const corsPhp = execSafe(
|
|
138
|
+
`grep -rl --include="*.php" "Access-Control-Allow-Origin" . 2>/dev/null | head -5`,
|
|
139
|
+
cwd
|
|
140
|
+
);
|
|
141
|
+
if (corsPhp && corsPhp.length > 0) {
|
|
142
|
+
result.detected = true;
|
|
143
|
+
const files = corsPhp.split("\n").map((f) => path.relative(cwd, f));
|
|
144
|
+
result.sources.push(...files.filter((f) => !result.sources.includes(f)));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Check nginx config (common locations)
|
|
148
|
+
for (const confPath of ["/etc/nginx/sites-enabled/default", "/etc/nginx/conf.d/default.conf"]) {
|
|
149
|
+
const content = readFileSafe(confPath);
|
|
150
|
+
if (content && /Access-Control-Allow-Origin/i.test(content)) {
|
|
151
|
+
result.detected = true;
|
|
152
|
+
result.sources.push(confPath);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return result;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ---------------------------------------------------------------------------
|
|
160
|
+
// Detect frontend framework
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
|
|
163
|
+
function detectFrontend(cwd) {
|
|
164
|
+
const result = { detected: false, framework: null, location: null };
|
|
165
|
+
|
|
166
|
+
// Check for frontend directories
|
|
167
|
+
const frontendDirs = ["frontend", "client", "app", "web", "next", "nuxt"];
|
|
168
|
+
for (const dir of frontendDirs) {
|
|
169
|
+
const pkgPath = path.join(cwd, dir, "package.json");
|
|
170
|
+
const pkg = readJsonSafe(pkgPath);
|
|
171
|
+
if (pkg) {
|
|
172
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
173
|
+
if (allDeps["next"]) {
|
|
174
|
+
result.detected = true;
|
|
175
|
+
result.framework = "Next.js";
|
|
176
|
+
result.location = dir;
|
|
177
|
+
return result;
|
|
178
|
+
}
|
|
179
|
+
if (allDeps["nuxt"] || allDeps["nuxt3"]) {
|
|
180
|
+
result.detected = true;
|
|
181
|
+
result.framework = "Nuxt";
|
|
182
|
+
result.location = dir;
|
|
183
|
+
return result;
|
|
184
|
+
}
|
|
185
|
+
if (allDeps["astro"]) {
|
|
186
|
+
result.detected = true;
|
|
187
|
+
result.framework = "Astro";
|
|
188
|
+
result.location = dir;
|
|
189
|
+
return result;
|
|
190
|
+
}
|
|
191
|
+
if (allDeps["gatsby"]) {
|
|
192
|
+
result.detected = true;
|
|
193
|
+
result.framework = "Gatsby";
|
|
194
|
+
result.location = dir;
|
|
195
|
+
return result;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Check root package.json
|
|
201
|
+
const rootPkg = readJsonSafe(path.join(cwd, "package.json"));
|
|
202
|
+
if (rootPkg) {
|
|
203
|
+
const allDeps = { ...rootPkg.dependencies, ...rootPkg.devDependencies };
|
|
204
|
+
if (allDeps["next"]) { result.detected = true; result.framework = "Next.js"; result.location = "."; }
|
|
205
|
+
else if (allDeps["nuxt"] || allDeps["nuxt3"]) { result.detected = true; result.framework = "Nuxt"; result.location = "."; }
|
|
206
|
+
else if (allDeps["astro"]) { result.detected = true; result.framework = "Astro"; result.location = "."; }
|
|
207
|
+
else if (allDeps["gatsby"]) { result.detected = true; result.framework = "Gatsby"; result.location = "."; }
|
|
208
|
+
else if (allDeps["gatsby-source-wordpress"]) { result.detected = true; result.framework = "Gatsby"; result.location = "."; }
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return result;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ---------------------------------------------------------------------------
|
|
215
|
+
// Detect headless indicators in WordPress
|
|
216
|
+
// ---------------------------------------------------------------------------
|
|
217
|
+
|
|
218
|
+
function detectHeadlessIndicators(cwd) {
|
|
219
|
+
const result = { indicators: [] };
|
|
220
|
+
|
|
221
|
+
// Check for headless theme (minimal or API-only themes)
|
|
222
|
+
const themeDir = path.join(cwd, "wp-content", "themes");
|
|
223
|
+
if (statSafe(themeDir)?.isDirectory()) {
|
|
224
|
+
const themes = readdirSafe(themeDir);
|
|
225
|
+
for (const theme of themes) {
|
|
226
|
+
const functionsPhp = readFileSafe(path.join(themeDir, theme, "functions.php"));
|
|
227
|
+
if (functionsPhp && /headless|decoupled|api.only/i.test(functionsPhp)) {
|
|
228
|
+
result.indicators.push(`Headless theme detected: ${theme}`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Check for webhook configuration
|
|
234
|
+
const wpConfig = readFileSafe(path.join(cwd, "wp-config.php"));
|
|
235
|
+
if (wpConfig) {
|
|
236
|
+
if (/HEADLESS_WEBHOOK/i.test(wpConfig)) {
|
|
237
|
+
result.indicators.push("Webhook configuration found in wp-config.php");
|
|
238
|
+
}
|
|
239
|
+
if (/HEADLESS_FRONTEND|FRONTEND_URL/i.test(wpConfig)) {
|
|
240
|
+
result.indicators.push("Frontend URL constant found in wp-config.php");
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Check for REST API customizations
|
|
245
|
+
const restCustom = execSafe(
|
|
246
|
+
`grep -rl --include="*.php" "register_rest_route" . 2>/dev/null | wc -l`,
|
|
247
|
+
cwd
|
|
248
|
+
);
|
|
249
|
+
if (restCustom && parseInt(restCustom) > 3) {
|
|
250
|
+
result.indicators.push(`${restCustom} files with custom REST routes detected`);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Check for headless plugins
|
|
254
|
+
const pluginsDir = path.join(cwd, "wp-content", "plugins");
|
|
255
|
+
const headlessPlugins = [
|
|
256
|
+
{ dir: "faust-wordpress", name: "Faust.js (WP Engine)" },
|
|
257
|
+
{ dir: "atlas-content-modeler", name: "Atlas Content Modeler" },
|
|
258
|
+
{ dir: "wp-gatsby", name: "WP Gatsby" },
|
|
259
|
+
{ dir: "headless-mode", name: "Headless Mode" },
|
|
260
|
+
];
|
|
261
|
+
|
|
262
|
+
for (const plugin of headlessPlugins) {
|
|
263
|
+
if (statSafe(path.join(pluginsDir, plugin.dir))?.isDirectory()) {
|
|
264
|
+
result.indicators.push(`Plugin: ${plugin.name}`);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return result;
|
|
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 graphql = detectWPGraphQL(cwd);
|
|
284
|
+
const cors = detectCORS(cwd);
|
|
285
|
+
const frontend = detectFrontend(cwd);
|
|
286
|
+
const indicators = detectHeadlessIndicators(cwd);
|
|
287
|
+
|
|
288
|
+
const detected = graphql.detected || cors.detected || frontend.detected || indicators.indicators.length > 0;
|
|
289
|
+
|
|
290
|
+
const report = {
|
|
291
|
+
tool: "headless_inspect",
|
|
292
|
+
version: TOOL_VERSION,
|
|
293
|
+
cwd,
|
|
294
|
+
detected,
|
|
295
|
+
graphql,
|
|
296
|
+
cors,
|
|
297
|
+
frontend,
|
|
298
|
+
indicators: indicators.indicators,
|
|
299
|
+
apiLayer: graphql.detected ? "WPGraphQL" : "REST API",
|
|
300
|
+
recommendations: [],
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
// Recommendations
|
|
304
|
+
if (frontend.detected && !graphql.detected && !cors.detected) {
|
|
305
|
+
report.recommendations.push("Frontend framework detected but no CORS or GraphQL setup found. Configure CORS headers for cross-origin API access.");
|
|
306
|
+
}
|
|
307
|
+
if (graphql.detected && !cors.detected) {
|
|
308
|
+
report.recommendations.push("WPGraphQL detected but no CORS configuration found. Add CORS headers for frontend access.");
|
|
309
|
+
}
|
|
310
|
+
if (frontend.framework === "Gatsby" && !graphql.detected) {
|
|
311
|
+
report.recommendations.push("Gatsby detected. Install WPGraphQL for optimal integration with gatsby-source-wordpress.");
|
|
312
|
+
}
|
|
313
|
+
if (detected && indicators.indicators.length === 0) {
|
|
314
|
+
report.recommendations.push("Consider adding a webhook system to notify the frontend of content changes.");
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
console.log(JSON.stringify(report, null, 2));
|
|
318
|
+
process.exit(detected ? 0 : 1);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
main();
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: wp-i18n
|
|
3
|
+
description: "Use when internationalizing WordPress plugins or themes: PHP gettext functions (__/esc_html__/etc.), JavaScript i18n (@wordpress/i18n), .pot/.po/.mo translation workflow, WP-CLI i18n commands, RTL stylesheet support, and multilingual plugin setup (Polylang/WPML)."
|
|
4
|
+
compatibility: "Targets WordPress 6.9+ (PHP 7.2.24+). Filesystem-based agent with bash + node. WP-CLI required for i18n make-pot/make-json."
|
|
5
|
+
version: 1.0.0
|
|
6
|
+
source: "vinmor/wordpress-manager"
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# WP i18n
|
|
10
|
+
|
|
11
|
+
## When to use
|
|
12
|
+
|
|
13
|
+
- Adding internationalization (i18n) to a WordPress plugin or theme
|
|
14
|
+
- Preparing a plugin or theme for WordPress.org submission (i18n is required)
|
|
15
|
+
- Generating `.pot` template files from source code
|
|
16
|
+
- Adding JavaScript translations via `@wordpress/i18n`
|
|
17
|
+
- Supporting right-to-left (RTL) languages such as Arabic, Hebrew, or Persian
|
|
18
|
+
- Setting up a multilingual WordPress site with Polylang or WPML
|
|
19
|
+
|
|
20
|
+
## Inputs required
|
|
21
|
+
|
|
22
|
+
- **Repo root** — path to the plugin or theme project directory
|
|
23
|
+
- **Text domain** — the unique slug used in all gettext calls (matches the plugin/theme slug)
|
|
24
|
+
- **Project kind** — `plugin` or `theme` (determines how text domain is loaded)
|
|
25
|
+
- **Target languages** — locale codes for translations (e.g., `it_IT`, `de_DE`, `ar`)
|
|
26
|
+
- **Whether JS translation is needed** — true if the project has JavaScript files with user-facing strings
|
|
27
|
+
|
|
28
|
+
## Procedure
|
|
29
|
+
|
|
30
|
+
### 0) Detect i18n status
|
|
31
|
+
|
|
32
|
+
Run the detection script to assess the current state of internationalization:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
node skills/wp-i18n/scripts/i18n_inspect.mjs --cwd=/path/to/project
|
|
36
|
+
node skills/wp-project-triage/scripts/detect_wp_project.mjs
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
The i18n inspection script outputs JSON with:
|
|
40
|
+
- `textDomain` — the text domain declared in the plugin/theme header
|
|
41
|
+
- `phpI18n` — count and distribution of PHP gettext function usage
|
|
42
|
+
- `jsI18n` — whether `@wordpress/i18n` is installed and configured
|
|
43
|
+
- `translationFiles` — existing .pot, .po, .mo, and .json files
|
|
44
|
+
- `issues[]` — problems found with severity and suggested fixes
|
|
45
|
+
|
|
46
|
+
If the project has no i18n at all, start from step 1. If partial i18n exists, jump to the step that addresses the gaps.
|
|
47
|
+
|
|
48
|
+
### 1) PHP internationalization
|
|
49
|
+
|
|
50
|
+
Wrap all user-facing strings in PHP files with the appropriate gettext function:
|
|
51
|
+
|
|
52
|
+
| Function | Use case |
|
|
53
|
+
|----------|----------|
|
|
54
|
+
| `__('text', 'domain')` | Return translated string |
|
|
55
|
+
| `_e('text', 'domain')` | Echo translated string |
|
|
56
|
+
| `esc_html__('text', 'domain')` | Return translated + HTML-escaped |
|
|
57
|
+
| `esc_html_e('text', 'domain')` | Echo translated + HTML-escaped |
|
|
58
|
+
| `esc_attr__('text', 'domain')` | Return translated + attribute-escaped |
|
|
59
|
+
| `_n('single', 'plural', $count, 'domain')` | Pluralization |
|
|
60
|
+
| `_x('text', 'context', 'domain')` | Disambiguation context |
|
|
61
|
+
| `sprintf(__('Hello %s', 'domain'), $name)` | Variable substitution |
|
|
62
|
+
|
|
63
|
+
Key rules:
|
|
64
|
+
- The text domain **must** match the `Text Domain:` header in the main plugin file or `style.css`
|
|
65
|
+
- Never pass variables as the first argument — strings must be literal for extraction
|
|
66
|
+
- Use `sprintf()` / `printf()` for dynamic content, never string concatenation
|
|
67
|
+
- Load the text domain: `load_plugin_textdomain()` on `init` or `load_theme_textdomain()` on `after_setup_theme`
|
|
68
|
+
|
|
69
|
+
Read: `references/php-i18n.md`
|
|
70
|
+
|
|
71
|
+
### 2) JavaScript internationalization
|
|
72
|
+
|
|
73
|
+
For JavaScript files with user-facing strings, use `@wordpress/i18n`:
|
|
74
|
+
|
|
75
|
+
```js
|
|
76
|
+
import { __, _n, _x, sprintf } from '@wordpress/i18n';
|
|
77
|
+
const label = __('Save Changes', 'my-plugin');
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Setup requires:
|
|
81
|
+
1. Add `@wordpress/i18n` as a dependency (or use `wp-scripts` which includes it)
|
|
82
|
+
2. Register script translations in PHP via `wp_set_script_translations()`
|
|
83
|
+
3. Generate JSON translation files from `.po` files via `wp i18n make-json`
|
|
84
|
+
4. For blocks, set the `textdomain` field in `block.json`
|
|
85
|
+
|
|
86
|
+
Read: `references/js-i18n.md`
|
|
87
|
+
|
|
88
|
+
### 3) Translation file workflow
|
|
89
|
+
|
|
90
|
+
The WordPress translation pipeline has four file types:
|
|
91
|
+
|
|
92
|
+
1. **`.pot`** — Template with all extractable strings (generated, never edited manually)
|
|
93
|
+
2. **`.po`** — Per-locale translations (human-edited or via Poedit/GlotPress)
|
|
94
|
+
3. **`.mo`** — Compiled binary for PHP runtime (generated from `.po`)
|
|
95
|
+
4. **`.json`** — Compiled translations for JavaScript (generated from `.po`)
|
|
96
|
+
|
|
97
|
+
File naming: `{text-domain}-{locale}.{po|mo}` (e.g., `my-plugin-it_IT.po`)
|
|
98
|
+
|
|
99
|
+
Workflow: extract → `.pot` → create/update `.po` → compile `.mo` + `.json`
|
|
100
|
+
|
|
101
|
+
Read: `references/translation-workflow.md`
|
|
102
|
+
|
|
103
|
+
### 4) WP-CLI i18n commands
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
wp i18n make-pot . languages/my-plugin.pot --domain=my-plugin
|
|
107
|
+
wp i18n make-json languages/ --no-purge
|
|
108
|
+
wp i18n make-mo languages/
|
|
109
|
+
wp i18n update-po languages/my-plugin.pot languages/
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Integrate into `package.json`:
|
|
113
|
+
```json
|
|
114
|
+
{
|
|
115
|
+
"scripts": {
|
|
116
|
+
"i18n:pot": "wp i18n make-pot . languages/my-plugin.pot",
|
|
117
|
+
"i18n:json": "wp i18n make-json languages/ --no-purge",
|
|
118
|
+
"i18n:mo": "wp i18n make-mo languages/",
|
|
119
|
+
"i18n:build": "npm run i18n:pot && npm run i18n:mo && npm run i18n:json"
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Read: `references/wpcli-i18n.md`
|
|
125
|
+
|
|
126
|
+
### 5) RTL support
|
|
127
|
+
|
|
128
|
+
WordPress auto-loads `style-rtl.css` when the locale is RTL:
|
|
129
|
+
- Use CSS logical properties (`margin-inline-start` vs `margin-left`)
|
|
130
|
+
- Use `@wordpress/scripts` with `rtlcss` to auto-generate RTL stylesheets
|
|
131
|
+
- Use `is_rtl()` in PHP for conditional logic
|
|
132
|
+
- Test with an RTL locale (Arabic `ar` or Hebrew `he_IL`)
|
|
133
|
+
|
|
134
|
+
Read: `references/rtl-support.md`
|
|
135
|
+
|
|
136
|
+
### 6) Multilingual site setup
|
|
137
|
+
|
|
138
|
+
For content translation (not just UI strings):
|
|
139
|
+
- **Polylang** (free) — `pll_register_string()`, `pll__()`, `pll_e()`
|
|
140
|
+
- **WPML** (paid) — `wpml-config.xml` for custom post types and taxonomies
|
|
141
|
+
- Both require hreflang tags and a language switcher
|
|
142
|
+
|
|
143
|
+
Read: `references/multilingual-setup.md`
|
|
144
|
+
|
|
145
|
+
## Verification
|
|
146
|
+
|
|
147
|
+
- `.pot` file generates without errors: `wp i18n make-pot . languages/<domain>.pot`
|
|
148
|
+
- Translations load in the UI: switch locale and confirm translated strings
|
|
149
|
+
- RTL renders correctly: switch to Arabic and check layout
|
|
150
|
+
- Text domain matches: `Text Domain:` header matches domain in all gettext calls
|
|
151
|
+
- JS translations work: `wp.i18n.__()` returns translated strings in browser console
|
|
152
|
+
- No untranslated strings: search for hardcoded user-facing strings
|
|
153
|
+
|
|
154
|
+
Re-run: `node skills/wp-i18n/scripts/i18n_inspect.mjs --cwd=/path`
|
|
155
|
+
|
|
156
|
+
## Failure modes / debugging
|
|
157
|
+
|
|
158
|
+
- **Translations not loading** — `load_plugin_textdomain()` missing or called on wrong hook; `languages/` path incorrect
|
|
159
|
+
- **Wrong text domain** — domain in gettext calls must exactly match `Text Domain:` header
|
|
160
|
+
- **JS translations missing** — `wp_set_script_translations()` must be called after `wp_enqueue_script()`; JSON files must exist with correct handle-based naming
|
|
161
|
+
- **`.mo` not found** — file must follow `{domain}-{locale}.mo` naming convention
|
|
162
|
+
- **Plural forms broken** — verify `Plural-Forms` header in `.po` matches target language
|
|
163
|
+
- **`make-pot` misses strings** — strings with variables or concatenation cannot be extracted; refactor to literals with `sprintf()`
|
|
164
|
+
- **RTL layout broken** — hardcoded `left`/`right` in CSS; use logical properties or `rtlcss`
|
|
165
|
+
|
|
166
|
+
## Escalation
|
|
167
|
+
|
|
168
|
+
- WordPress i18n Handbook: https://developer.wordpress.org/plugins/internationalization/
|
|
169
|
+
- CLDR Plural Rules: https://www.unicode.org/cldr/charts/latest/supplemental/language_plural_rules.html
|
|
170
|
+
- Translator Handbook: https://make.wordpress.org/polyglots/handbook/
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# JavaScript Internationalization
|
|
2
|
+
|
|
3
|
+
Use this file when internationalizing JavaScript code in WordPress blocks, plugins, and themes.
|
|
4
|
+
|
|
5
|
+
## @wordpress/i18n package
|
|
6
|
+
|
|
7
|
+
### Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @wordpress/i18n
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### Core functions
|
|
14
|
+
|
|
15
|
+
```js
|
|
16
|
+
import { __, _x, _n, _nx, sprintf } from '@wordpress/i18n';
|
|
17
|
+
|
|
18
|
+
// Simple translation
|
|
19
|
+
const label = __('Save', 'my-text-domain');
|
|
20
|
+
|
|
21
|
+
// With context
|
|
22
|
+
const post = _x('Post', 'verb', 'my-text-domain');
|
|
23
|
+
|
|
24
|
+
// Plurals
|
|
25
|
+
const message = sprintf(
|
|
26
|
+
_n('%d item', '%d items', count, 'my-text-domain'),
|
|
27
|
+
count
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
// Plurals with context
|
|
31
|
+
const posts = sprintf(
|
|
32
|
+
_nx('%d post', '%d posts', count, 'blog posts', 'my-text-domain'),
|
|
33
|
+
count
|
|
34
|
+
);
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### sprintf for variable substitution
|
|
38
|
+
|
|
39
|
+
```js
|
|
40
|
+
import { __, sprintf } from '@wordpress/i18n';
|
|
41
|
+
|
|
42
|
+
// Positional placeholders
|
|
43
|
+
const text = sprintf(
|
|
44
|
+
/* translators: 1: product name, 2: price */
|
|
45
|
+
__('%1$s costs %2$s', 'my-text-domain'),
|
|
46
|
+
productName,
|
|
47
|
+
price
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
// %s placeholder
|
|
51
|
+
const greeting = sprintf(
|
|
52
|
+
/* translators: %s: user name */
|
|
53
|
+
__('Hello, %s!', 'my-text-domain'),
|
|
54
|
+
userName
|
|
55
|
+
);
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Block editor (Gutenberg) usage
|
|
59
|
+
|
|
60
|
+
### Block registration
|
|
61
|
+
|
|
62
|
+
```js
|
|
63
|
+
import { registerBlockType } from '@wordpress/blocks';
|
|
64
|
+
import { __ } from '@wordpress/i18n';
|
|
65
|
+
|
|
66
|
+
registerBlockType('my-plugin/my-block', {
|
|
67
|
+
title: __('My Block', 'my-text-domain'),
|
|
68
|
+
description: __('A custom block.', 'my-text-domain'),
|
|
69
|
+
keywords: [
|
|
70
|
+
__('example', 'my-text-domain'),
|
|
71
|
+
__('custom', 'my-text-domain'),
|
|
72
|
+
],
|
|
73
|
+
// ...
|
|
74
|
+
});
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### InspectorControls
|
|
78
|
+
|
|
79
|
+
```js
|
|
80
|
+
import { InspectorControls } from '@wordpress/block-editor';
|
|
81
|
+
import { PanelBody, ToggleControl } from '@wordpress/components';
|
|
82
|
+
import { __ } from '@wordpress/i18n';
|
|
83
|
+
|
|
84
|
+
<InspectorControls>
|
|
85
|
+
<PanelBody title={__('Settings', 'my-text-domain')}>
|
|
86
|
+
<ToggleControl
|
|
87
|
+
label={__('Show title', 'my-text-domain')}
|
|
88
|
+
help={__('Toggle the title visibility.', 'my-text-domain')}
|
|
89
|
+
checked={showTitle}
|
|
90
|
+
onChange={(val) => setAttributes({ showTitle: val })}
|
|
91
|
+
/>
|
|
92
|
+
</PanelBody>
|
|
93
|
+
</InspectorControls>
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Translation file loading
|
|
97
|
+
|
|
98
|
+
### For blocks (block.json method — recommended)
|
|
99
|
+
|
|
100
|
+
```json
|
|
101
|
+
{
|
|
102
|
+
"name": "my-plugin/my-block",
|
|
103
|
+
"title": "My Block",
|
|
104
|
+
"textdomain": "my-text-domain"
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
WordPress automatically loads translation files for blocks with `textdomain` in `block.json`.
|
|
109
|
+
|
|
110
|
+
### For non-block scripts
|
|
111
|
+
|
|
112
|
+
Register script translations in PHP:
|
|
113
|
+
|
|
114
|
+
```php
|
|
115
|
+
add_action('init', function() {
|
|
116
|
+
wp_set_script_translations(
|
|
117
|
+
'my-script-handle', // Same handle used in wp_register_script
|
|
118
|
+
'my-text-domain',
|
|
119
|
+
plugin_dir_path(__FILE__) . 'languages'
|
|
120
|
+
);
|
|
121
|
+
});
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Translation file format
|
|
125
|
+
|
|
126
|
+
JS translations use JSON (JED format). Generated by WP-CLI:
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
wp i18n make-json languages/ --no-purge
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Output files: `my-text-domain-{locale}-{md5hash}.json`
|
|
133
|
+
|
|
134
|
+
## block.json translatable fields
|
|
135
|
+
|
|
136
|
+
These fields are automatically translated when `textdomain` is set:
|
|
137
|
+
|
|
138
|
+
```json
|
|
139
|
+
{
|
|
140
|
+
"title": "My Block",
|
|
141
|
+
"description": "Block description here.",
|
|
142
|
+
"keywords": ["example", "demo"],
|
|
143
|
+
"styles": [
|
|
144
|
+
{ "name": "default", "label": "Default" },
|
|
145
|
+
{ "name": "fancy", "label": "Fancy" }
|
|
146
|
+
],
|
|
147
|
+
"variations": [
|
|
148
|
+
{
|
|
149
|
+
"name": "wide",
|
|
150
|
+
"title": "Wide",
|
|
151
|
+
"description": "Wide layout variation."
|
|
152
|
+
}
|
|
153
|
+
],
|
|
154
|
+
"textdomain": "my-text-domain"
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## Common mistakes
|
|
159
|
+
|
|
160
|
+
### Do NOT use template literals for translation keys
|
|
161
|
+
|
|
162
|
+
```js
|
|
163
|
+
// WRONG — extraction tools cannot parse
|
|
164
|
+
const text = __(`Hello ${name}`, 'my-text-domain');
|
|
165
|
+
|
|
166
|
+
// CORRECT
|
|
167
|
+
const text = sprintf(__('Hello %s', 'my-text-domain'), name);
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### Do NOT use dynamic text domains
|
|
171
|
+
|
|
172
|
+
```js
|
|
173
|
+
// WRONG
|
|
174
|
+
const text = __(message, domain);
|
|
175
|
+
|
|
176
|
+
// CORRECT — literal string only
|
|
177
|
+
const text = __('Hello', 'my-text-domain');
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### Do NOT concatenate inside translation calls
|
|
181
|
+
|
|
182
|
+
```js
|
|
183
|
+
// WRONG
|
|
184
|
+
const text = __('Total: ' + count + ' items', 'my-text-domain');
|
|
185
|
+
|
|
186
|
+
// CORRECT
|
|
187
|
+
const text = sprintf(__('Total: %d items', 'my-text-domain'), count);
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## Verification
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
# Generate POT from JS files
|
|
194
|
+
wp i18n make-pot . languages/my-text-domain.pot --include="src/**/*.js,src/**/*.jsx"
|
|
195
|
+
|
|
196
|
+
# Generate JSON translation files
|
|
197
|
+
wp i18n make-json languages/ --no-purge
|
|
198
|
+
|
|
199
|
+
# Check for untranslated strings in JS
|
|
200
|
+
grep -rn "['\"]\`" --include="*.js" --include="*.jsx" src/ | grep -v "__\|_x\|_n\|sprintf"
|
|
201
|
+
```
|