@wpelevator/wp-release 0.15.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 +366 -39
- package/dist/changeset-config.d.ts +8 -0
- package/dist/changeset-config.js +3 -0
- package/dist/changeset-release.d.ts +9 -3
- package/dist/changeset-release.js +23 -14
- package/dist/cli.d.ts +6 -0
- package/dist/cli.js +12 -0
- package/dist/commands/artifact-signing.d.ts +20 -0
- package/dist/commands/artifact-signing.js +42 -0
- package/dist/commands/build.d.ts +32 -0
- package/dist/commands/build.js +91 -0
- package/dist/commands/changeset-add.d.ts +55 -6
- package/dist/commands/changeset-add.js +12 -3
- package/dist/commands/changeset-args.js +2 -2
- package/dist/commands/changeset-project-args.d.ts +63 -0
- package/dist/commands/changeset-project-args.js +114 -0
- package/dist/commands/changeset-status.d.ts +50 -1
- package/dist/commands/changeset-status.js +4 -2
- package/dist/commands/changeset-tag.d.ts +55 -6
- package/dist/commands/changeset-tag.js +7 -4
- package/dist/commands/changeset-version.d.ts +55 -6
- package/dist/commands/changeset-version.js +4 -2
- package/dist/commands/checksums.d.ts +4 -1
- package/dist/commands/checksums.js +27 -6
- 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/commands/package-source.d.ts +43 -0
- package/dist/commands/package-source.js +101 -0
- package/dist/commands/verify.d.ts +7 -1
- package/dist/commands/verify.js +53 -17
- package/dist/commands/zip.d.ts +20 -8
- package/dist/commands/zip.js +109 -124
- 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 +28 -0
- package/dist/index.js +16 -0
- package/dist/integrity.d.ts +33 -0
- package/dist/integrity.js +115 -0
- package/dist/output-files.d.ts +7 -0
- package/dist/output-files.js +25 -0
- package/dist/package-build.d.ts +33 -0
- package/dist/package-build.js +112 -0
- package/dist/package-files.d.ts +25 -0
- package/dist/package-files.js +24 -0
- package/dist/package-zip.d.ts +14 -2
- package/dist/package-zip.js +129 -38
- package/dist/release-tags.js +6 -0
- package/dist/single-project.d.ts +32 -0
- package/dist/single-project.js +144 -0
- package/dist/summary.d.ts +9 -0
- package/dist/summary.js +25 -0
- package/dist/zip.d.ts +3 -0
- package/dist/zip.js +16 -6
- package/package.json +20 -3
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { dirname, join, resolve } from 'node:path';
|
|
3
|
+
import { ValidationError } from "../errors.js";
|
|
4
|
+
import { parseTranslatedPo, compileMo } from "./po.js";
|
|
5
|
+
import { compileScriptTranslations } from "./translation-json.js";
|
|
6
|
+
import { compilePhpTranslations } from "./translation-php.js";
|
|
7
|
+
import { writeI18nOutput, validateI18nDestinations } from "./atomic-output.js";
|
|
8
|
+
/**
|
|
9
|
+
* Parse/validate every proposed output before independently replacing completed files.
|
|
10
|
+
* @param input
|
|
11
|
+
*/
|
|
12
|
+
export async function compileTranslations(input) {
|
|
13
|
+
const sourcePath = resolve(input.sourcePath);
|
|
14
|
+
if (!sourcePath.endsWith('.po')) {
|
|
15
|
+
throw new ValidationError('Translation compilation requires an input .po file.');
|
|
16
|
+
}
|
|
17
|
+
if ('json' !== input.format && input.scriptMap) {
|
|
18
|
+
throw new ValidationError('Script mapping only applies to WordPress JSON compilation.');
|
|
19
|
+
}
|
|
20
|
+
const catalog = parseTranslatedPo(await readFile(sourcePath), input.domain ?? '');
|
|
21
|
+
if ('json' === input.format && !input.domain) {
|
|
22
|
+
throw new ValidationError('WordPress JSON compilation requires an explicit text domain.');
|
|
23
|
+
}
|
|
24
|
+
const proposed = 'json' === input.format
|
|
25
|
+
? compileScriptTranslations(catalog, input.scriptMap).map((output) => ({
|
|
26
|
+
outputPath: join(input.outputPath ?? dirname(sourcePath), output.file),
|
|
27
|
+
source: output.source,
|
|
28
|
+
bytes: output.bytes,
|
|
29
|
+
}))
|
|
30
|
+
: [
|
|
31
|
+
{
|
|
32
|
+
outputPath: input.outputPath ??
|
|
33
|
+
sourcePath.replace(/\.po$/, 'mo' === input.format ? '.mo' : '.l10n.php'),
|
|
34
|
+
bytes: 'mo' === input.format
|
|
35
|
+
? compileMo(catalog)
|
|
36
|
+
: compilePhpTranslations(catalog),
|
|
37
|
+
},
|
|
38
|
+
];
|
|
39
|
+
proposed.forEach((output) => {
|
|
40
|
+
output.outputPath = resolve(output.outputPath);
|
|
41
|
+
});
|
|
42
|
+
await validateI18nDestinations(proposed.map((output) => output.outputPath), [sourcePath]);
|
|
43
|
+
const result = {
|
|
44
|
+
sourcePath,
|
|
45
|
+
locale: catalog.locale,
|
|
46
|
+
messages: catalog.messages.length,
|
|
47
|
+
outputs: [],
|
|
48
|
+
};
|
|
49
|
+
for (const output of proposed) {
|
|
50
|
+
const previous = await readFile(output.outputPath).catch((error) => {
|
|
51
|
+
if ('ENOENT' !== error.code) {
|
|
52
|
+
throw error;
|
|
53
|
+
}
|
|
54
|
+
return undefined;
|
|
55
|
+
});
|
|
56
|
+
let current = previous?.equals(output.bytes) ?? false;
|
|
57
|
+
if (!input.check) {
|
|
58
|
+
await writeI18nOutput(output.outputPath, output.bytes);
|
|
59
|
+
current = true;
|
|
60
|
+
}
|
|
61
|
+
result.outputs.push({
|
|
62
|
+
outputPath: output.outputPath,
|
|
63
|
+
...('source' in output
|
|
64
|
+
? { source: output.source }
|
|
65
|
+
: {}),
|
|
66
|
+
written: !input.check,
|
|
67
|
+
current,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
return result;
|
|
71
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { ExtractionResult, SourceReference } from './catalog.ts';
|
|
2
|
+
export interface ParseSourceInput {
|
|
3
|
+
contents: string;
|
|
4
|
+
file: string;
|
|
5
|
+
/** Domain being selected. Omitted domain arguments use WordPress's `default`. */
|
|
6
|
+
domain: string;
|
|
7
|
+
allDomains?: boolean;
|
|
8
|
+
/** Accept unbound canonical JS names in legacy source (default true). */
|
|
9
|
+
legacyGlobals?: boolean;
|
|
10
|
+
}
|
|
11
|
+
export interface CallSignature {
|
|
12
|
+
id: number;
|
|
13
|
+
plural?: number;
|
|
14
|
+
context?: number;
|
|
15
|
+
domain: number;
|
|
16
|
+
parameters: string[];
|
|
17
|
+
}
|
|
18
|
+
export interface LexicalComment {
|
|
19
|
+
start: number;
|
|
20
|
+
end: number;
|
|
21
|
+
value: string;
|
|
22
|
+
}
|
|
23
|
+
export interface TranslationCall extends SourceReference {
|
|
24
|
+
start: number;
|
|
25
|
+
/** Start of the nearest containing statement. */
|
|
26
|
+
statementStart: number;
|
|
27
|
+
/** Starts of containing expressions, up to the nearest statement. */
|
|
28
|
+
commentAnchors?: number[];
|
|
29
|
+
name: string;
|
|
30
|
+
arguments: Array<string | undefined>;
|
|
31
|
+
signature: CallSignature;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Attach each adjacent translators comment only to the first translation call in a statement, before domain filtering.
|
|
35
|
+
* @param input
|
|
36
|
+
* @param calls
|
|
37
|
+
* @param comments
|
|
38
|
+
*/
|
|
39
|
+
export declare function extractCalls(input: ParseSourceInput, calls: TranslationCall[], comments: LexicalComment[]): ExtractionResult;
|
|
40
|
+
/**
|
|
41
|
+
* Syntax failures are data, never printed by a parser.
|
|
42
|
+
* @param file
|
|
43
|
+
* @param error
|
|
44
|
+
*/
|
|
45
|
+
export declare function syntaxFailure(file: string, error: unknown): ExtractionResult;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { getPlaceholders } from "./placeholders.js";
|
|
2
|
+
/**
|
|
3
|
+
* Attach each adjacent translators comment only to the first translation call in a statement, before domain filtering.
|
|
4
|
+
* @param input
|
|
5
|
+
* @param calls
|
|
6
|
+
* @param comments
|
|
7
|
+
*/
|
|
8
|
+
export function extractCalls(input, calls, comments) {
|
|
9
|
+
const result = { messages: [], diagnostics: [] };
|
|
10
|
+
const used = new Set();
|
|
11
|
+
for (const call of calls.sort((a, b) => a.start - b.start)) {
|
|
12
|
+
const signature = call.signature;
|
|
13
|
+
const domainValue = call.arguments[signature.domain];
|
|
14
|
+
// An omitted domain defaults to WordPress's own domain; an unresolved supplied value doesn't.
|
|
15
|
+
const domain = call.arguments.length <= signature.domain ? 'default' : domainValue;
|
|
16
|
+
const reference = {
|
|
17
|
+
file: call.file,
|
|
18
|
+
line: call.line,
|
|
19
|
+
column: call.column,
|
|
20
|
+
};
|
|
21
|
+
const adjacent = comments.filter((comment) => (call.commentAnchors ?? [call.start, call.statementStart]).some((anchor) => {
|
|
22
|
+
let offset = comment.end;
|
|
23
|
+
let gap = '';
|
|
24
|
+
for (const next of comments
|
|
25
|
+
.filter((candidate) => candidate.start >= comment.end &&
|
|
26
|
+
candidate.end <= anchor)
|
|
27
|
+
.sort((a, b) => a.start - b.start)) {
|
|
28
|
+
gap += input.contents.slice(offset, next.start);
|
|
29
|
+
offset = next.end;
|
|
30
|
+
}
|
|
31
|
+
gap += input.contents.slice(offset, anchor);
|
|
32
|
+
return comment.end <= anchor && /^[\s?:]*$/.test(gap);
|
|
33
|
+
}));
|
|
34
|
+
const translatorComments = adjacent.filter((comment) => !used.has(comment.start) &&
|
|
35
|
+
/translators:/i.test(comment.value));
|
|
36
|
+
translatorComments.forEach((comment) => used.add(comment.start));
|
|
37
|
+
if (undefined === domain ||
|
|
38
|
+
input.allDomains ||
|
|
39
|
+
input.domain === domain) {
|
|
40
|
+
const required = [
|
|
41
|
+
signature.id,
|
|
42
|
+
signature.plural,
|
|
43
|
+
signature.context,
|
|
44
|
+
].filter((index) => undefined !== index);
|
|
45
|
+
if (undefined === domain ||
|
|
46
|
+
required.some((index) => undefined === call.arguments[index])) {
|
|
47
|
+
result.diagnostics.push({
|
|
48
|
+
...reference,
|
|
49
|
+
code: 'dynamic-argument',
|
|
50
|
+
severity: 'warning',
|
|
51
|
+
message: `${call.name} requires static message IDs, context and domain; use string literals or static concatenation.`,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
const message = {
|
|
56
|
+
id: call.arguments[signature.id] ?? '',
|
|
57
|
+
domain,
|
|
58
|
+
...(undefined !== signature.plural
|
|
59
|
+
? { plural: call.arguments[signature.plural] }
|
|
60
|
+
: {}),
|
|
61
|
+
...(undefined !== signature.context
|
|
62
|
+
? { context: call.arguments[signature.context] }
|
|
63
|
+
: {}),
|
|
64
|
+
translations: [],
|
|
65
|
+
flags: [],
|
|
66
|
+
references: [reference],
|
|
67
|
+
comments: translatorComments.map((comment) => comment.value
|
|
68
|
+
.replace(/^\s*(?:\/\*+|\/\/|#)/, '')
|
|
69
|
+
.replace(/\*\/\s*$/, '')
|
|
70
|
+
.replace(/^\s*\*\s?/gm, '')
|
|
71
|
+
.trim()),
|
|
72
|
+
};
|
|
73
|
+
if ([message.id, message.plural ?? ''].some((form) => getPlaceholders(form).length)) {
|
|
74
|
+
message.flags.push(input.file.endsWith('.php')
|
|
75
|
+
? 'php-format'
|
|
76
|
+
: 'js-format');
|
|
77
|
+
}
|
|
78
|
+
result.messages.push(message);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return result;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Syntax failures are data, never printed by a parser.
|
|
86
|
+
* @param file
|
|
87
|
+
* @param error
|
|
88
|
+
*/
|
|
89
|
+
export function syntaxFailure(file, error) {
|
|
90
|
+
const failure = error;
|
|
91
|
+
const column = failure.loc?.column ?? failure.columnNumber;
|
|
92
|
+
return {
|
|
93
|
+
messages: [],
|
|
94
|
+
diagnostics: [
|
|
95
|
+
{
|
|
96
|
+
code: 'invalid-syntax',
|
|
97
|
+
severity: 'error',
|
|
98
|
+
file,
|
|
99
|
+
message: failure.message ?? String(error),
|
|
100
|
+
line: failure.loc?.line ?? failure.lineNumber,
|
|
101
|
+
column: undefined !== column ? column + 1 : undefined,
|
|
102
|
+
},
|
|
103
|
+
],
|
|
104
|
+
};
|
|
105
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { I18nOptions } from './options.ts';
|
|
2
|
+
/**
|
|
3
|
+
* Select source files with package-relative minimatch globs, never distribution rules.
|
|
4
|
+
* @param root
|
|
5
|
+
* @param input
|
|
6
|
+
*/
|
|
7
|
+
export declare function discoverI18nFiles(root: string, input?: I18nOptions): Promise<string[]>;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { readdir } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { minimatch } from 'minimatch';
|
|
4
|
+
import { ValidationError } from "../errors.js";
|
|
5
|
+
import { compareText } from "./catalog.js";
|
|
6
|
+
import { resolveI18nOptions } from "./options.js";
|
|
7
|
+
/**
|
|
8
|
+
* Select source files with package-relative minimatch globs, never distribution rules.
|
|
9
|
+
* @param root
|
|
10
|
+
* @param input
|
|
11
|
+
*/
|
|
12
|
+
export async function discoverI18nFiles(root, input = {}) {
|
|
13
|
+
const options = resolveI18nOptions(input);
|
|
14
|
+
const includes = options.include ?? [];
|
|
15
|
+
const excludes = options.exclude ?? [];
|
|
16
|
+
const matches = (file, patterns) => patterns.some((pattern) => minimatch(file, pattern, {
|
|
17
|
+
dot: true,
|
|
18
|
+
nocase: false,
|
|
19
|
+
nonegate: true,
|
|
20
|
+
}));
|
|
21
|
+
const files = [];
|
|
22
|
+
async function visit(prefix) {
|
|
23
|
+
const entries = await readdir(join(root, prefix), {
|
|
24
|
+
withFileTypes: true,
|
|
25
|
+
});
|
|
26
|
+
for (const entry of entries.sort((a, b) => compareText(a.name, b.name))) {
|
|
27
|
+
const file = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
28
|
+
if (entry.isSymbolicLink() &&
|
|
29
|
+
matches(file, includes) &&
|
|
30
|
+
!matches(file, excludes)) {
|
|
31
|
+
throw new ValidationError(`Selected source symlinks are unsupported: ${file}.`);
|
|
32
|
+
}
|
|
33
|
+
if (entry.isDirectory() && !matches(`${file}/`, excludes)) {
|
|
34
|
+
await visit(file);
|
|
35
|
+
}
|
|
36
|
+
else if (entry.isFile() &&
|
|
37
|
+
matches(file, includes) &&
|
|
38
|
+
!matches(file, excludes)) {
|
|
39
|
+
files.push(file);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
await visit('');
|
|
44
|
+
return files.sort(compareText);
|
|
45
|
+
}
|
|
@@ -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;
|