@wpelevator/wp-release 0.16.0 → 0.17.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/README.md +124 -6
- package/dist/changeset-config.d.ts +5 -0
- package/dist/changeset-config.js +3 -0
- package/dist/cli.d.ts +5 -0
- package/dist/cli.js +10 -0
- package/dist/commands/i18n-audit.d.ts +2 -0
- package/dist/commands/i18n-audit.js +2 -0
- package/dist/commands/i18n-compile.d.ts +6 -0
- package/dist/commands/i18n-compile.js +76 -0
- package/dist/commands/i18n-json.d.ts +2 -0
- package/dist/commands/i18n-json.js +2 -0
- package/dist/commands/i18n-mo.d.ts +2 -0
- package/dist/commands/i18n-mo.js +2 -0
- package/dist/commands/i18n-php.d.ts +2 -0
- package/dist/commands/i18n-php.js +2 -0
- package/dist/commands/i18n-pot.d.ts +2 -0
- package/dist/commands/i18n-pot.js +2 -0
- package/dist/commands/i18n.d.ts +12 -0
- package/dist/commands/i18n.js +174 -0
- package/dist/i18n/atomic-output.d.ts +23 -0
- package/dist/i18n/atomic-output.js +90 -0
- package/dist/i18n/audit.d.ts +7 -0
- package/dist/i18n/audit.js +69 -0
- package/dist/i18n/catalog.d.ts +54 -0
- package/dist/i18n/catalog.js +86 -0
- package/dist/i18n/compile.d.ts +27 -0
- package/dist/i18n/compile.js +71 -0
- package/dist/i18n/extraction.d.ts +45 -0
- package/dist/i18n/extraction.js +105 -0
- package/dist/i18n/files.d.ts +7 -0
- package/dist/i18n/files.js +45 -0
- package/dist/i18n/generate.d.ts +49 -0
- package/dist/i18n/generate.js +240 -0
- package/dist/i18n/javascript.d.ts +7 -0
- package/dist/i18n/javascript.js +143 -0
- package/dist/i18n/metadata.d.ts +16 -0
- package/dist/i18n/metadata.js +117 -0
- package/dist/i18n/options.d.ts +52 -0
- package/dist/i18n/options.js +156 -0
- package/dist/i18n/packages.d.ts +24 -0
- package/dist/i18n/packages.js +86 -0
- package/dist/i18n/php.d.ts +9 -0
- package/dist/i18n/php.js +238 -0
- package/dist/i18n/placeholders.d.ts +9 -0
- package/dist/i18n/placeholders.js +13 -0
- package/dist/i18n/po.d.ts +24 -0
- package/dist/i18n/po.js +180 -0
- package/dist/i18n/pot.d.ts +16 -0
- package/dist/i18n/pot.js +119 -0
- package/dist/i18n/translation-json.d.ts +14 -0
- package/dist/i18n/translation-json.js +96 -0
- package/dist/i18n/translation-php.d.ts +6 -0
- package/dist/i18n/translation-php.js +35 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +14 -0
- package/package.json +15 -3
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { PackageMeta } from '../package-meta.ts';
|
|
2
|
+
import type { CatalogResult, I18nDiagnostic } from './catalog.ts';
|
|
3
|
+
import type { I18nOptions } from './options.ts';
|
|
4
|
+
export interface ExtractCatalogInput {
|
|
5
|
+
sourceDir: string;
|
|
6
|
+
options?: I18nOptions;
|
|
7
|
+
/** Exact output paths excluded even when include globs select them. */
|
|
8
|
+
outputPaths?: string[];
|
|
9
|
+
}
|
|
10
|
+
export interface ExtractCatalogResult extends CatalogResult {
|
|
11
|
+
/** Unique files consulted, including the main metadata file if excluded by source globs. */
|
|
12
|
+
files: number;
|
|
13
|
+
}
|
|
14
|
+
export interface PackagePotInput extends ExtractCatalogInput {
|
|
15
|
+
/** Absolute output path (CLI resolves explicit paths against cwd). */
|
|
16
|
+
outputPath?: string;
|
|
17
|
+
check?: boolean;
|
|
18
|
+
}
|
|
19
|
+
export interface PackagePotResult {
|
|
20
|
+
outputPath: string;
|
|
21
|
+
messages: number;
|
|
22
|
+
/** Unique files consulted, including the main metadata file if excluded by source globs. */
|
|
23
|
+
files: number;
|
|
24
|
+
diagnostics: I18nDiagnostic[];
|
|
25
|
+
written: boolean;
|
|
26
|
+
current: boolean;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Read source files and collect all failures before reporting completion.
|
|
30
|
+
* @param input
|
|
31
|
+
*/
|
|
32
|
+
export declare function extractCatalog(input: ExtractCatalogInput): Promise<ExtractCatalogResult>;
|
|
33
|
+
/**
|
|
34
|
+
* Resolve a package-relative destination, treating /languages like WordPress Domain Path.
|
|
35
|
+
* @param sourceDir
|
|
36
|
+
* @param meta
|
|
37
|
+
* @param options
|
|
38
|
+
*/
|
|
39
|
+
export declare function resolvePotPath(sourceDir: string, meta: PackageMeta | null, options?: I18nOptions): string;
|
|
40
|
+
/**
|
|
41
|
+
* Validate destinations before any writes, including collisions through existing symlinks.
|
|
42
|
+
* @param paths
|
|
43
|
+
*/
|
|
44
|
+
export declare function validatePotDestinations(paths: string[]): Promise<void>;
|
|
45
|
+
/**
|
|
46
|
+
* Each package replaces its output atomically only after successful extraction and serialization.
|
|
47
|
+
* @param input
|
|
48
|
+
*/
|
|
49
|
+
export declare function generatePackagePot(input: PackagePotInput): Promise<PackagePotResult>;
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { basename, isAbsolute, join, relative, resolve } from 'node:path';
|
|
3
|
+
import { ValidationError } from "../errors.js";
|
|
4
|
+
import { readPackageMeta } from "../package-meta.js";
|
|
5
|
+
import { aggregateCatalog, compareText, hasFatalDiagnostics, } from "./catalog.js";
|
|
6
|
+
import { discoverI18nFiles } from "./files.js";
|
|
7
|
+
import { parsePhpTranslations } from "./php.js";
|
|
8
|
+
import { parseJavascriptTranslations } from "./javascript.js";
|
|
9
|
+
import { extractHeaderTranslations, parseMetadataTranslations, } from "./metadata.js";
|
|
10
|
+
import { mergeI18nHeaders, resolveI18nOptions } from "./options.js";
|
|
11
|
+
import { serializePot } from "./pot.js";
|
|
12
|
+
import { writeI18nOutput, validateI18nDestinations, validateI18nContainment, } from "./atomic-output.js";
|
|
13
|
+
/**
|
|
14
|
+
* Group sources by type: PHP, then JavaScript, then block.json, then other WordPress JSON.
|
|
15
|
+
*
|
|
16
|
+
* @param file Package-relative source file.
|
|
17
|
+
* @return Sort rank of the file.
|
|
18
|
+
*/
|
|
19
|
+
function sourceRank(file) {
|
|
20
|
+
if (file.endsWith('.php')) {
|
|
21
|
+
return 0;
|
|
22
|
+
}
|
|
23
|
+
if (!file.endsWith('.json')) {
|
|
24
|
+
return 1;
|
|
25
|
+
}
|
|
26
|
+
return file.endsWith('/block.json') || 'block.json' === file ? 2 : 3;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Order the messages of one file by their position, keeping the parser order for equal positions.
|
|
30
|
+
*
|
|
31
|
+
* @param messages Messages extracted from one file.
|
|
32
|
+
* @return Messages in source order.
|
|
33
|
+
*/
|
|
34
|
+
function sortBySourcePosition(messages) {
|
|
35
|
+
const position = (message) => message.references[0];
|
|
36
|
+
return [...messages].sort((a, b) => (position(a)?.line ?? 0) - (position(b)?.line ?? 0) ||
|
|
37
|
+
(position(a)?.column ?? 0) - (position(b)?.column ?? 0));
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Read source files and collect all failures before reporting completion.
|
|
41
|
+
* @param input
|
|
42
|
+
*/
|
|
43
|
+
export async function extractCatalog(input) {
|
|
44
|
+
const sourceDir = resolve(input.sourceDir);
|
|
45
|
+
const options = resolveI18nOptions(input.options);
|
|
46
|
+
const diagnostics = [];
|
|
47
|
+
let meta = null;
|
|
48
|
+
try {
|
|
49
|
+
meta = await readPackageMeta({
|
|
50
|
+
dir: sourceDir,
|
|
51
|
+
slug: basename(sourceDir),
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
diagnostics.push({
|
|
56
|
+
file: '.',
|
|
57
|
+
code: 'unreadable-file',
|
|
58
|
+
severity: 'error',
|
|
59
|
+
message: String(error),
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
const domain = options.domain ??
|
|
63
|
+
meta?.headers['Text Domain'] ??
|
|
64
|
+
basename(sourceDir);
|
|
65
|
+
const excluded = new Set((input.outputPaths ?? []).map((file) => resolve(file)));
|
|
66
|
+
let files = [];
|
|
67
|
+
try {
|
|
68
|
+
files = (await discoverI18nFiles(sourceDir, options))
|
|
69
|
+
.filter((file) => !excluded.has(resolve(sourceDir, file)))
|
|
70
|
+
.sort((a, b) => sourceRank(a) - sourceRank(b) || compareText(a, b));
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
diagnostics.push({
|
|
74
|
+
file: '.',
|
|
75
|
+
code: 'source-discovery-failed',
|
|
76
|
+
severity: 'error',
|
|
77
|
+
message: String(error),
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
const messages = [];
|
|
81
|
+
if (meta) {
|
|
82
|
+
try {
|
|
83
|
+
messages.push(...extractHeaderTranslations(meta, await readFile(join(sourceDir, meta.mainFile), 'utf8'), domain));
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
diagnostics.push({
|
|
87
|
+
file: meta.mainFile,
|
|
88
|
+
code: 'unreadable-file',
|
|
89
|
+
severity: 'error',
|
|
90
|
+
message: String(error),
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
for (const file of files) {
|
|
95
|
+
try {
|
|
96
|
+
const contents = await readFile(join(sourceDir, file), 'utf8');
|
|
97
|
+
const parseInput = {
|
|
98
|
+
contents,
|
|
99
|
+
file,
|
|
100
|
+
domain,
|
|
101
|
+
allDomains: options.allDomains,
|
|
102
|
+
legacyGlobals: options.legacyGlobals,
|
|
103
|
+
};
|
|
104
|
+
let parsed;
|
|
105
|
+
if (file.endsWith('.php')) {
|
|
106
|
+
parsed = parsePhpTranslations(parseInput);
|
|
107
|
+
}
|
|
108
|
+
else if (file.endsWith('.json')) {
|
|
109
|
+
parsed = parseMetadataTranslations(parseInput, file.endsWith('/block.json') || 'block.json' === file
|
|
110
|
+
? 'block'
|
|
111
|
+
: 'theme');
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
parsed = parseJavascriptTranslations(parseInput);
|
|
115
|
+
}
|
|
116
|
+
messages.push(...sortBySourcePosition(parsed.messages));
|
|
117
|
+
diagnostics.push(...parsed.diagnostics);
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
diagnostics.push({
|
|
121
|
+
file,
|
|
122
|
+
code: 'unreadable-file',
|
|
123
|
+
severity: 'error',
|
|
124
|
+
message: error instanceof Error ? error.message : String(error),
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
const result = aggregateCatalog(messages, domain, mergeI18nHeaders({
|
|
129
|
+
'Project-Id-Version': `${meta?.headers['Plugin Name'] ??
|
|
130
|
+
meta?.headers['Theme Name'] ??
|
|
131
|
+
domain}${meta?.version ? ` ${meta.version}` : ''}`,
|
|
132
|
+
}, options.headers), options.allDomains);
|
|
133
|
+
return {
|
|
134
|
+
...result,
|
|
135
|
+
diagnostics: [...diagnostics, ...result.diagnostics],
|
|
136
|
+
files: files.length +
|
|
137
|
+
(meta && !files.includes(meta.mainFile) ? 1 : 0),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Resolve a package-relative destination, treating /languages like WordPress Domain Path.
|
|
142
|
+
* @param sourceDir
|
|
143
|
+
* @param meta
|
|
144
|
+
* @param options
|
|
145
|
+
*/
|
|
146
|
+
export function resolvePotPath(sourceDir, meta, options = {}) {
|
|
147
|
+
const domain = options.domain ??
|
|
148
|
+
meta?.headers['Text Domain'] ??
|
|
149
|
+
basename(sourceDir);
|
|
150
|
+
if (!domain ||
|
|
151
|
+
/[\\/\0:*?"<>|]/.test(domain) ||
|
|
152
|
+
['.', '..'].includes(domain)) {
|
|
153
|
+
throw new ValidationError('The text domain must be a safe filename component.');
|
|
154
|
+
}
|
|
155
|
+
const destination = (options.destination ??
|
|
156
|
+
meta?.headers['Domain Path'] ??
|
|
157
|
+
'languages').replace(/^\/+/, '');
|
|
158
|
+
const directory = resolve(sourceDir, destination);
|
|
159
|
+
const path = relative(resolve(sourceDir), directory);
|
|
160
|
+
if (isAbsolute(path) ||
|
|
161
|
+
'..' === path ||
|
|
162
|
+
path.startsWith('../') ||
|
|
163
|
+
/[\\\0:]/.test(destination)) {
|
|
164
|
+
throw new ValidationError('The i18n destination must stay inside the package directory.');
|
|
165
|
+
}
|
|
166
|
+
return join(directory, `${domain}.pot`);
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Validate destinations before any writes, including collisions through existing symlinks.
|
|
170
|
+
* @param paths
|
|
171
|
+
*/
|
|
172
|
+
export async function validatePotDestinations(paths) {
|
|
173
|
+
if (paths.some((file) => !file.endsWith('.pot'))) {
|
|
174
|
+
throw new ValidationError('POT outputs must use the .pot extension.');
|
|
175
|
+
}
|
|
176
|
+
await validateI18nDestinations(paths);
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Each package replaces its output atomically only after successful extraction and serialization.
|
|
180
|
+
* @param input
|
|
181
|
+
*/
|
|
182
|
+
export async function generatePackagePot(input) {
|
|
183
|
+
const sourceDir = resolve(input.sourceDir);
|
|
184
|
+
const options = resolveI18nOptions(input.options);
|
|
185
|
+
const meta = await readPackageMeta({
|
|
186
|
+
dir: sourceDir,
|
|
187
|
+
slug: basename(sourceDir),
|
|
188
|
+
});
|
|
189
|
+
const outputPath = resolve(input.outputPath ?? resolvePotPath(sourceDir, meta, options));
|
|
190
|
+
await validatePotDestinations([outputPath]);
|
|
191
|
+
if (!input.outputPath) {
|
|
192
|
+
await validateI18nContainment(sourceDir, outputPath);
|
|
193
|
+
}
|
|
194
|
+
const extracted = await extractCatalog({
|
|
195
|
+
...input,
|
|
196
|
+
outputPaths: [outputPath, ...(input.outputPaths ?? [])],
|
|
197
|
+
});
|
|
198
|
+
const result = {
|
|
199
|
+
outputPath,
|
|
200
|
+
messages: extracted.catalog.messages.length,
|
|
201
|
+
files: extracted.files,
|
|
202
|
+
diagnostics: extracted.diagnostics,
|
|
203
|
+
written: false,
|
|
204
|
+
current: false,
|
|
205
|
+
};
|
|
206
|
+
if (!hasFatalDiagnostics(result.diagnostics, options.strict)) {
|
|
207
|
+
try {
|
|
208
|
+
const bytes = serializePot(extracted.catalog, options);
|
|
209
|
+
const previous = await readFile(outputPath).catch((error) => {
|
|
210
|
+
if ('ENOENT' !== error.code) {
|
|
211
|
+
throw error;
|
|
212
|
+
}
|
|
213
|
+
return undefined;
|
|
214
|
+
});
|
|
215
|
+
result.current = previous?.equals(bytes) ?? false;
|
|
216
|
+
if (!input.check) {
|
|
217
|
+
await writeI18nOutput(outputPath, bytes);
|
|
218
|
+
result.written = true;
|
|
219
|
+
result.current = true;
|
|
220
|
+
}
|
|
221
|
+
if (input.check && !result.current) {
|
|
222
|
+
result.diagnostics.push({
|
|
223
|
+
file: outputPath,
|
|
224
|
+
code: 'stale-pot',
|
|
225
|
+
severity: 'error',
|
|
226
|
+
message: 'POT is missing or stale; regenerate it with i18n-pot.',
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
catch (error) {
|
|
231
|
+
result.diagnostics.push({
|
|
232
|
+
file: outputPath,
|
|
233
|
+
code: 'output-failed',
|
|
234
|
+
severity: 'error',
|
|
235
|
+
message: error instanceof Error ? error.message : String(error),
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return result;
|
|
240
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ParseSourceInput } from './extraction.ts';
|
|
2
|
+
import type { ExtractionResult } from './catalog.ts';
|
|
3
|
+
/**
|
|
4
|
+
* Parse JS/JSX/TS/TSX with lexical scope resolution and untouched source positions.
|
|
5
|
+
* @param input
|
|
6
|
+
*/
|
|
7
|
+
export declare function parseJavascriptTranslations(input: ParseSourceInput): ExtractionResult;
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { parse } from '@babel/parser';
|
|
2
|
+
import traverseModule from '@babel/traverse';
|
|
3
|
+
import { extractCalls, syntaxFailure } from "./extraction.js";
|
|
4
|
+
// Babel traverse publishes CommonJS; Node's ESM default is its module object.
|
|
5
|
+
const traverse = traverseModule.default;
|
|
6
|
+
const signatures = {
|
|
7
|
+
__: { id: 0, domain: 1, parameters: [] },
|
|
8
|
+
_x: { id: 0, context: 1, domain: 2, parameters: [] },
|
|
9
|
+
_n: { id: 0, plural: 1, domain: 3, parameters: [] },
|
|
10
|
+
_nx: { id: 0, plural: 1, context: 3, domain: 4, parameters: [] },
|
|
11
|
+
};
|
|
12
|
+
function staticString(node) {
|
|
13
|
+
if ('StringLiteral' === node?.type) {
|
|
14
|
+
return node.value;
|
|
15
|
+
}
|
|
16
|
+
if ('TemplateLiteral' === node?.type && !node.expressions.length) {
|
|
17
|
+
return node.quasis
|
|
18
|
+
.map((part) => part.value.cooked ?? part.value.raw)
|
|
19
|
+
.join('');
|
|
20
|
+
}
|
|
21
|
+
if ('BinaryExpression' === node?.type && '+' === node.operator) {
|
|
22
|
+
const left = staticString(node.left);
|
|
23
|
+
const right = staticString(node.right);
|
|
24
|
+
return undefined !== left && undefined !== right
|
|
25
|
+
? left + right
|
|
26
|
+
: undefined;
|
|
27
|
+
}
|
|
28
|
+
if (node &&
|
|
29
|
+
[
|
|
30
|
+
'TSAsExpression',
|
|
31
|
+
'TSSatisfiesExpression',
|
|
32
|
+
'TSNonNullExpression',
|
|
33
|
+
'ParenthesizedExpression',
|
|
34
|
+
].includes(node.type)) {
|
|
35
|
+
return staticString(node.expression);
|
|
36
|
+
}
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
function importedName(path, name) {
|
|
40
|
+
const binding = path.scope.getBinding(name);
|
|
41
|
+
const declaration = binding?.path.parentPath;
|
|
42
|
+
if (binding?.constant &&
|
|
43
|
+
declaration?.isImportDeclaration() &&
|
|
44
|
+
'type' !== declaration.node.importKind &&
|
|
45
|
+
'@wordpress/i18n' === declaration.node.source.value &&
|
|
46
|
+
binding.path.isImportSpecifier() &&
|
|
47
|
+
'type' !== binding.path.node.importKind) {
|
|
48
|
+
const imported = binding.path.node.imported;
|
|
49
|
+
return 'Identifier' === imported.type ? imported.name : imported.value;
|
|
50
|
+
}
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
function memberName(node) {
|
|
54
|
+
return !node.computed && 'Identifier' === node.property.type
|
|
55
|
+
? node.property.name
|
|
56
|
+
: undefined;
|
|
57
|
+
}
|
|
58
|
+
function functionName(path, legacyGlobals) {
|
|
59
|
+
const callee = path.node.callee;
|
|
60
|
+
if ('Identifier' === callee.type) {
|
|
61
|
+
return (importedName(path, callee.name) ??
|
|
62
|
+
(legacyGlobals && !path.scope.getBinding(callee.name)
|
|
63
|
+
? callee.name
|
|
64
|
+
: undefined));
|
|
65
|
+
}
|
|
66
|
+
if ('MemberExpression' === callee.type) {
|
|
67
|
+
const object = callee.object;
|
|
68
|
+
if ('Identifier' === object.type) {
|
|
69
|
+
const binding = path.scope.getBinding(object.name);
|
|
70
|
+
const declaration = binding?.path.parentPath;
|
|
71
|
+
if (binding?.constant &&
|
|
72
|
+
binding.path.isImportNamespaceSpecifier() &&
|
|
73
|
+
declaration?.isImportDeclaration() &&
|
|
74
|
+
'type' !== declaration.node.importKind &&
|
|
75
|
+
'@wordpress/i18n' === declaration.node.source.value) {
|
|
76
|
+
return memberName(callee);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if ('MemberExpression' === object.type &&
|
|
80
|
+
'i18n' === memberName(object) &&
|
|
81
|
+
'Identifier' === object.object.type &&
|
|
82
|
+
'wp' === object.object.name &&
|
|
83
|
+
!path.scope.getBinding('wp')) {
|
|
84
|
+
return memberName(callee);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Parse JS/JSX/TS/TSX with lexical scope resolution and untouched source positions.
|
|
91
|
+
* @param input
|
|
92
|
+
*/
|
|
93
|
+
export function parseJavascriptTranslations(input) {
|
|
94
|
+
try {
|
|
95
|
+
const typescript = /\.[cm]?tsx?$/.test(input.file);
|
|
96
|
+
const jsx = /\.(jsx|tsx|js|mjs|cjs)$/.test(input.file);
|
|
97
|
+
const ast = parse(input.contents, {
|
|
98
|
+
sourceType: 'unambiguous',
|
|
99
|
+
plugins: [
|
|
100
|
+
...(typescript ? ['typescript'] : []),
|
|
101
|
+
...(jsx ? ['jsx'] : []),
|
|
102
|
+
],
|
|
103
|
+
});
|
|
104
|
+
const calls = [];
|
|
105
|
+
traverse(ast, {
|
|
106
|
+
CallExpression(path) {
|
|
107
|
+
const name = functionName(path, input.legacyGlobals ?? true);
|
|
108
|
+
const signature = name && Object.hasOwn(signatures, name)
|
|
109
|
+
? signatures[name]
|
|
110
|
+
: undefined;
|
|
111
|
+
if (name && signature) {
|
|
112
|
+
const statement = path.getStatementParent();
|
|
113
|
+
const commentAnchors = [];
|
|
114
|
+
let ancestor = path;
|
|
115
|
+
while (ancestor && ancestor !== statement) {
|
|
116
|
+
commentAnchors.push(ancestor.node.start ?? 0);
|
|
117
|
+
ancestor = ancestor.parentPath;
|
|
118
|
+
}
|
|
119
|
+
commentAnchors.push(statement?.node.start ?? 0);
|
|
120
|
+
calls.push({
|
|
121
|
+
name,
|
|
122
|
+
signature,
|
|
123
|
+
commentAnchors,
|
|
124
|
+
arguments: path.node.arguments.map(staticString),
|
|
125
|
+
file: input.file,
|
|
126
|
+
line: path.node.loc?.start.line,
|
|
127
|
+
column: (path.node.loc?.start.column ?? 0) + 1,
|
|
128
|
+
start: path.node.start ?? 0,
|
|
129
|
+
statementStart: statement?.node.start ?? path.node.start ?? 0,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
return extractCalls(input, calls, (ast.comments ?? []).map((comment) => ({
|
|
135
|
+
start: comment.start ?? 0,
|
|
136
|
+
end: comment.end ?? 0,
|
|
137
|
+
value: comment.value,
|
|
138
|
+
})));
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
return syntaxFailure(input.file, error);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { PackageMeta } from '../package-meta.ts';
|
|
2
|
+
import type { ExtractedMessage, ExtractionResult } from './catalog.ts';
|
|
3
|
+
import type { ParseSourceInput } from './extraction.ts';
|
|
4
|
+
/**
|
|
5
|
+
* Extract only the actual WordPress main file's translatable header values.
|
|
6
|
+
* @param meta
|
|
7
|
+
* @param contents
|
|
8
|
+
* @param domain
|
|
9
|
+
*/
|
|
10
|
+
export declare function extractHeaderTranslations(meta: PackageMeta, contents: string, domain: string): ExtractedMessage[];
|
|
11
|
+
/**
|
|
12
|
+
* Walk versioned WordPress metadata tables, including array and wildcard fields.
|
|
13
|
+
* @param input
|
|
14
|
+
* @param kind
|
|
15
|
+
*/
|
|
16
|
+
export declare function parseMetadataTranslations(input: ParseSourceInput, kind: 'block' | 'theme'): ExtractionResult;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { findFileHeaderLine } from "../package-meta.js";
|
|
2
|
+
import { syntaxFailure } from "./extraction.js";
|
|
3
|
+
const blockFields = {
|
|
4
|
+
title: 'block title',
|
|
5
|
+
description: 'block description',
|
|
6
|
+
keywords: ['block keyword'],
|
|
7
|
+
styles: [{ label: 'block style label' }],
|
|
8
|
+
variations: [
|
|
9
|
+
{
|
|
10
|
+
title: 'block variation title',
|
|
11
|
+
description: 'block variation description',
|
|
12
|
+
keywords: ['block variation keyword'],
|
|
13
|
+
},
|
|
14
|
+
],
|
|
15
|
+
};
|
|
16
|
+
const presetFields = {
|
|
17
|
+
typography: {
|
|
18
|
+
fontSizes: [{ name: 'Font size name' }],
|
|
19
|
+
fontFamilies: [{ name: 'Font family name' }],
|
|
20
|
+
},
|
|
21
|
+
color: {
|
|
22
|
+
palette: [{ name: 'Color name' }],
|
|
23
|
+
gradients: [{ name: 'Gradient name' }],
|
|
24
|
+
duotone: [{ name: 'Duotone name' }],
|
|
25
|
+
},
|
|
26
|
+
spacing: { spacingSizes: [{ name: 'Space size name' }] },
|
|
27
|
+
dimensions: { aspectRatios: [{ name: 'Aspect ratio name' }] },
|
|
28
|
+
};
|
|
29
|
+
const themeFields = {
|
|
30
|
+
title: 'Style variation name',
|
|
31
|
+
description: 'Style variation description',
|
|
32
|
+
settings: {
|
|
33
|
+
...presetFields,
|
|
34
|
+
shadow: { presets: [{ name: 'Shadow name' }] },
|
|
35
|
+
blocks: { '*': presetFields },
|
|
36
|
+
},
|
|
37
|
+
customTemplates: [{ title: 'Custom template name' }],
|
|
38
|
+
templateParts: [{ title: 'Template part name' }],
|
|
39
|
+
};
|
|
40
|
+
function message(id, domain, file, context, label, line) {
|
|
41
|
+
return {
|
|
42
|
+
id,
|
|
43
|
+
domain,
|
|
44
|
+
...(context ? { context } : {}),
|
|
45
|
+
translations: [],
|
|
46
|
+
flags: [],
|
|
47
|
+
comments: label ? [label] : [],
|
|
48
|
+
references: [{ file, ...(line ? { line } : {}) }],
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Extract only the actual WordPress main file's translatable header values.
|
|
53
|
+
* @param meta
|
|
54
|
+
* @param contents
|
|
55
|
+
* @param domain
|
|
56
|
+
*/
|
|
57
|
+
export function extractHeaderTranslations(meta, contents, domain) {
|
|
58
|
+
const fields = [
|
|
59
|
+
'plugin' === meta.type ? 'Plugin Name' : 'Theme Name',
|
|
60
|
+
'plugin' === meta.type ? 'Plugin URI' : 'Theme URI',
|
|
61
|
+
'Description',
|
|
62
|
+
'Author',
|
|
63
|
+
'Author URI',
|
|
64
|
+
];
|
|
65
|
+
return fields
|
|
66
|
+
.filter((field) => meta.headers[field])
|
|
67
|
+
.map((field) => message(meta.headers[field] ?? '', domain, meta.mainFile, undefined, `${field} of the ${meta.type}`, findFileHeaderLine(contents, field) ?? undefined));
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Walk versioned WordPress metadata tables, including array and wildcard fields.
|
|
71
|
+
* @param input
|
|
72
|
+
* @param kind
|
|
73
|
+
*/
|
|
74
|
+
export function parseMetadataTranslations(input, kind) {
|
|
75
|
+
try {
|
|
76
|
+
const data = JSON.parse(input.contents);
|
|
77
|
+
if (!data || 'object' !== typeof data || Array.isArray(data)) {
|
|
78
|
+
throw new Error('WordPress metadata must be a JSON object.');
|
|
79
|
+
}
|
|
80
|
+
const domain = 'block' === kind
|
|
81
|
+
? data.textdomain ??
|
|
82
|
+
input.domain
|
|
83
|
+
: input.domain;
|
|
84
|
+
if ('string' !== typeof domain) {
|
|
85
|
+
throw new Error('Block textdomain must be a string.');
|
|
86
|
+
}
|
|
87
|
+
const messages = [];
|
|
88
|
+
function visit(value, table) {
|
|
89
|
+
if ('string' === typeof table && 'string' === typeof value) {
|
|
90
|
+
messages.push(message(value, domain, input.file, table));
|
|
91
|
+
}
|
|
92
|
+
else if (Array.isArray(table) &&
|
|
93
|
+
Array.isArray(value) &&
|
|
94
|
+
table[0]) {
|
|
95
|
+
value.forEach((item) => visit(item, table[0]));
|
|
96
|
+
}
|
|
97
|
+
else if (table &&
|
|
98
|
+
'object' === typeof table &&
|
|
99
|
+
!Array.isArray(table) &&
|
|
100
|
+
value &&
|
|
101
|
+
'object' === typeof value &&
|
|
102
|
+
!Array.isArray(value)) {
|
|
103
|
+
for (const [key, child] of Object.entries(value)) {
|
|
104
|
+
const schema = table[key] ?? table['*'];
|
|
105
|
+
if (schema) {
|
|
106
|
+
visit(child, schema);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
visit(data, 'block' === kind ? blockFields : themeFields);
|
|
112
|
+
return { messages, diagnostics: [] };
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
return syntaxFailure(input.file, error);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/** Gettext header values by name; `false` removes a header set by the defaults or an earlier layer. */
|
|
2
|
+
export type I18nHeaders = Record<string, string | false>;
|
|
3
|
+
export interface I18nOptions {
|
|
4
|
+
enabled?: boolean;
|
|
5
|
+
domain?: string;
|
|
6
|
+
allDomains?: boolean;
|
|
7
|
+
include?: string[];
|
|
8
|
+
exclude?: string[];
|
|
9
|
+
/** Package-relative directory; Domain Path or languages by default. */
|
|
10
|
+
destination?: string;
|
|
11
|
+
headers?: I18nHeaders;
|
|
12
|
+
/** Static comment lines to prepend to the POT; no year is generated. */
|
|
13
|
+
fileComment?: string;
|
|
14
|
+
references?: boolean;
|
|
15
|
+
strict?: boolean;
|
|
16
|
+
legacyGlobals?: boolean;
|
|
17
|
+
suppress?: string[];
|
|
18
|
+
}
|
|
19
|
+
export declare const DEFAULT_I18N_INCLUDE: string[];
|
|
20
|
+
export declare const DEFAULT_I18N_EXCLUDE: string[];
|
|
21
|
+
/**
|
|
22
|
+
* Validate external config independently of extraction and distribution file settings.
|
|
23
|
+
* @param input
|
|
24
|
+
* @param label
|
|
25
|
+
*/
|
|
26
|
+
export declare function validateI18nOptions(input: unknown, label?: string): asserts input is I18nOptions | undefined;
|
|
27
|
+
/**
|
|
28
|
+
* Validate static text to prepend as a POT file comment.
|
|
29
|
+
* @param fileComment
|
|
30
|
+
* @param label
|
|
31
|
+
*/
|
|
32
|
+
export declare function validateI18nFileComment(fileComment: unknown, label?: string): asserts fileComment is string;
|
|
33
|
+
/**
|
|
34
|
+
* Validate gettext header names and values independently of extraction options.
|
|
35
|
+
*
|
|
36
|
+
* @param headers Header values, or `false` to remove a header.
|
|
37
|
+
* @param label Config path for error messages.
|
|
38
|
+
* @throws {ValidationError} For invalid names or values, or a removed Content-Type.
|
|
39
|
+
*/
|
|
40
|
+
export declare function validateI18nHeaders(headers: unknown, label?: string): asserts headers is I18nHeaders;
|
|
41
|
+
/**
|
|
42
|
+
* Scalars and lists replace inherited values; header dictionaries merge by key.
|
|
43
|
+
* @param layers Extraction settings in increasing precedence.
|
|
44
|
+
*/
|
|
45
|
+
export declare function resolveI18nOptions(...layers: Array<I18nOptions | undefined>): I18nOptions;
|
|
46
|
+
/**
|
|
47
|
+
* Merge gettext header names case-insensitively, preserving deliberate empty values and removals.
|
|
48
|
+
*
|
|
49
|
+
* @param layers Header values in increasing precedence.
|
|
50
|
+
* @return Merged headers, with `false` for removed headers so that later layers can still set them.
|
|
51
|
+
*/
|
|
52
|
+
export declare function mergeI18nHeaders(...layers: Array<I18nHeaders | undefined>): I18nHeaders;
|