@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.
Files changed (98) hide show
  1. package/README.md +366 -39
  2. package/dist/changeset-config.d.ts +8 -0
  3. package/dist/changeset-config.js +3 -0
  4. package/dist/changeset-release.d.ts +9 -3
  5. package/dist/changeset-release.js +23 -14
  6. package/dist/cli.d.ts +6 -0
  7. package/dist/cli.js +12 -0
  8. package/dist/commands/artifact-signing.d.ts +20 -0
  9. package/dist/commands/artifact-signing.js +42 -0
  10. package/dist/commands/build.d.ts +32 -0
  11. package/dist/commands/build.js +91 -0
  12. package/dist/commands/changeset-add.d.ts +55 -6
  13. package/dist/commands/changeset-add.js +12 -3
  14. package/dist/commands/changeset-args.js +2 -2
  15. package/dist/commands/changeset-project-args.d.ts +63 -0
  16. package/dist/commands/changeset-project-args.js +114 -0
  17. package/dist/commands/changeset-status.d.ts +50 -1
  18. package/dist/commands/changeset-status.js +4 -2
  19. package/dist/commands/changeset-tag.d.ts +55 -6
  20. package/dist/commands/changeset-tag.js +7 -4
  21. package/dist/commands/changeset-version.d.ts +55 -6
  22. package/dist/commands/changeset-version.js +4 -2
  23. package/dist/commands/checksums.d.ts +4 -1
  24. package/dist/commands/checksums.js +27 -6
  25. package/dist/commands/i18n-audit.d.ts +2 -0
  26. package/dist/commands/i18n-audit.js +2 -0
  27. package/dist/commands/i18n-compile.d.ts +6 -0
  28. package/dist/commands/i18n-compile.js +76 -0
  29. package/dist/commands/i18n-json.d.ts +2 -0
  30. package/dist/commands/i18n-json.js +2 -0
  31. package/dist/commands/i18n-mo.d.ts +2 -0
  32. package/dist/commands/i18n-mo.js +2 -0
  33. package/dist/commands/i18n-php.d.ts +2 -0
  34. package/dist/commands/i18n-php.js +2 -0
  35. package/dist/commands/i18n-pot.d.ts +2 -0
  36. package/dist/commands/i18n-pot.js +2 -0
  37. package/dist/commands/i18n.d.ts +12 -0
  38. package/dist/commands/i18n.js +174 -0
  39. package/dist/commands/package-source.d.ts +43 -0
  40. package/dist/commands/package-source.js +101 -0
  41. package/dist/commands/verify.d.ts +7 -1
  42. package/dist/commands/verify.js +53 -17
  43. package/dist/commands/zip.d.ts +20 -8
  44. package/dist/commands/zip.js +109 -124
  45. package/dist/i18n/atomic-output.d.ts +23 -0
  46. package/dist/i18n/atomic-output.js +90 -0
  47. package/dist/i18n/audit.d.ts +7 -0
  48. package/dist/i18n/audit.js +69 -0
  49. package/dist/i18n/catalog.d.ts +54 -0
  50. package/dist/i18n/catalog.js +86 -0
  51. package/dist/i18n/compile.d.ts +27 -0
  52. package/dist/i18n/compile.js +71 -0
  53. package/dist/i18n/extraction.d.ts +45 -0
  54. package/dist/i18n/extraction.js +105 -0
  55. package/dist/i18n/files.d.ts +7 -0
  56. package/dist/i18n/files.js +45 -0
  57. package/dist/i18n/generate.d.ts +49 -0
  58. package/dist/i18n/generate.js +240 -0
  59. package/dist/i18n/javascript.d.ts +7 -0
  60. package/dist/i18n/javascript.js +143 -0
  61. package/dist/i18n/metadata.d.ts +16 -0
  62. package/dist/i18n/metadata.js +117 -0
  63. package/dist/i18n/options.d.ts +52 -0
  64. package/dist/i18n/options.js +156 -0
  65. package/dist/i18n/packages.d.ts +24 -0
  66. package/dist/i18n/packages.js +86 -0
  67. package/dist/i18n/php.d.ts +9 -0
  68. package/dist/i18n/php.js +238 -0
  69. package/dist/i18n/placeholders.d.ts +9 -0
  70. package/dist/i18n/placeholders.js +13 -0
  71. package/dist/i18n/po.d.ts +24 -0
  72. package/dist/i18n/po.js +180 -0
  73. package/dist/i18n/pot.d.ts +16 -0
  74. package/dist/i18n/pot.js +119 -0
  75. package/dist/i18n/translation-json.d.ts +14 -0
  76. package/dist/i18n/translation-json.js +96 -0
  77. package/dist/i18n/translation-php.d.ts +6 -0
  78. package/dist/i18n/translation-php.js +35 -0
  79. package/dist/index.d.ts +28 -0
  80. package/dist/index.js +16 -0
  81. package/dist/integrity.d.ts +33 -0
  82. package/dist/integrity.js +115 -0
  83. package/dist/output-files.d.ts +7 -0
  84. package/dist/output-files.js +25 -0
  85. package/dist/package-build.d.ts +33 -0
  86. package/dist/package-build.js +112 -0
  87. package/dist/package-files.d.ts +25 -0
  88. package/dist/package-files.js +24 -0
  89. package/dist/package-zip.d.ts +14 -2
  90. package/dist/package-zip.js +129 -38
  91. package/dist/release-tags.js +6 -0
  92. package/dist/single-project.d.ts +32 -0
  93. package/dist/single-project.js +144 -0
  94. package/dist/summary.d.ts +9 -0
  95. package/dist/summary.js +25 -0
  96. package/dist/zip.d.ts +3 -0
  97. package/dist/zip.js +16 -6
  98. package/package.json +20 -3
@@ -0,0 +1,238 @@
1
+ import { Engine } from 'php-parser';
2
+ import { extractCalls, syntaxFailure } from "./extraction.js";
3
+ const simple = {
4
+ id: 0,
5
+ domain: 1,
6
+ parameters: ['text', 'domain'],
7
+ };
8
+ const contextual = {
9
+ id: 0,
10
+ context: 1,
11
+ domain: 2,
12
+ parameters: ['text', 'context', 'domain'],
13
+ };
14
+ const plural = {
15
+ id: 0,
16
+ plural: 1,
17
+ domain: 3,
18
+ parameters: ['single', 'plural', 'number', 'domain'],
19
+ };
20
+ const contextualPlural = {
21
+ id: 0,
22
+ plural: 1,
23
+ context: 3,
24
+ domain: 4,
25
+ parameters: ['single', 'plural', 'number', 'context', 'domain'],
26
+ };
27
+ /** PHP argument names and positions follow the WordPress l10n.php signatures. */
28
+ export const PHP_TRANSLATION_FUNCTIONS = {
29
+ __: simple,
30
+ _e: simple,
31
+ esc_attr__: simple,
32
+ esc_attr_e: simple,
33
+ esc_html__: simple,
34
+ esc_html_e: simple,
35
+ _x: contextual,
36
+ _ex: contextual,
37
+ esc_attr_x: contextual,
38
+ esc_html_x: contextual,
39
+ _n: plural,
40
+ _nx: contextualPlural,
41
+ _n_noop: {
42
+ id: 0,
43
+ plural: 1,
44
+ domain: 2,
45
+ parameters: ['singular', 'plural', 'domain'],
46
+ },
47
+ _nx_noop: {
48
+ id: 0,
49
+ plural: 1,
50
+ context: 2,
51
+ domain: 3,
52
+ parameters: ['singular', 'plural', 'context', 'domain'],
53
+ },
54
+ };
55
+ function nodes(node) {
56
+ return Object.entries(node)
57
+ .filter(([key]) => ![
58
+ 'loc',
59
+ 'comments',
60
+ 'leadingComments',
61
+ 'trailingComments',
62
+ ].includes(key))
63
+ .flatMap(([, value]) => {
64
+ const values = Array.isArray(value) ? value : [value];
65
+ return values.filter((child) => !!child && 'object' === typeof child && 'kind' in child);
66
+ });
67
+ }
68
+ function nameOf(node) {
69
+ if ('string' === typeof node?.name) {
70
+ return node.name;
71
+ }
72
+ return 'string' === typeof node?.name?.name ? node.name.name : '';
73
+ }
74
+ function staticString(node) {
75
+ if (node &&
76
+ ['string', 'nowdoc'].includes(node.kind) &&
77
+ 'string' === typeof node.value) {
78
+ return node.value;
79
+ }
80
+ if ('bin' === node?.kind && '.' === node.type) {
81
+ const left = staticString(node.left);
82
+ const right = staticString(node.right);
83
+ return undefined !== left && undefined !== right
84
+ ? left + right
85
+ : undefined;
86
+ }
87
+ if ('encapsed' === node?.kind &&
88
+ 'heredoc' === node.type &&
89
+ Array.isArray(node.value)) {
90
+ const parts = node.value.map((part) => staticString(part.expression));
91
+ return parts.every((part) => undefined !== part)
92
+ ? parts.join('')
93
+ : undefined;
94
+ }
95
+ return undefined;
96
+ }
97
+ function bindingsFor(root) {
98
+ const imports = new Map();
99
+ const functions = new Set();
100
+ function collect(node) {
101
+ if ('function' === node.kind) {
102
+ functions.add(nameOf(node).toLowerCase());
103
+ }
104
+ if ('usegroup' === node.kind) {
105
+ for (const item of node.items ?? []) {
106
+ if ('function' === (item.type ?? node.type)) {
107
+ const imported = [nameOf(node), nameOf(item)]
108
+ .filter(Boolean)
109
+ .join('\\')
110
+ .replace(/^\\/, '')
111
+ .toLowerCase();
112
+ imports.set((nameOf(item.alias) ||
113
+ imported.split('\\').at(-1) ||
114
+ '').toLowerCase(), imported);
115
+ }
116
+ }
117
+ }
118
+ if ('namespace' !== node.kind || node === root) {
119
+ nodes(node).forEach(collect);
120
+ }
121
+ }
122
+ collect(root);
123
+ return { imports, functions };
124
+ }
125
+ function resolveFunction(node, bindings, globalFunctions) {
126
+ if ('name' !== node?.kind) {
127
+ return undefined;
128
+ }
129
+ const name = nameOf(node).toLowerCase();
130
+ if (name.startsWith('\\')) {
131
+ return globalFunctions.has(name.slice(1))
132
+ ? undefined
133
+ : name.slice(1);
134
+ }
135
+ if (name.includes('\\') || 'rn' === node.resolution) {
136
+ return undefined;
137
+ }
138
+ if (bindings.imports.has(name)) {
139
+ const imported = bindings.imports.get(name);
140
+ return imported && !globalFunctions.has(imported)
141
+ ? imported
142
+ : undefined;
143
+ }
144
+ return bindings.functions.has(name) || globalFunctions.has(name)
145
+ ? undefined
146
+ : name;
147
+ }
148
+ /**
149
+ * Parse PHP without executing it; namespaced fallback calls follow known file-local bindings.
150
+ * @param input
151
+ */
152
+ export function parsePhpTranslations(input) {
153
+ try {
154
+ const parser = new Engine({
155
+ parser: { extractDoc: true, version: '8.3' },
156
+ ast: { withPositions: true },
157
+ });
158
+ const ast = parser.parseCode(input.contents, input.file);
159
+ const declarations = new Map();
160
+ function collectDeclarations(node, namespace) {
161
+ const currentNamespace = 'namespace' === node.kind
162
+ ? nameOf(node).toLowerCase()
163
+ : namespace;
164
+ if ('function' === node.kind) {
165
+ const names = declarations.get(currentNamespace) ?? new Set();
166
+ names.add(nameOf(node).toLowerCase());
167
+ declarations.set(currentNamespace, names);
168
+ }
169
+ nodes(node).forEach((child) => collectDeclarations(child, currentNamespace));
170
+ }
171
+ collectDeclarations(ast, '');
172
+ const globalFunctions = declarations.get('') ?? new Set();
173
+ const calls = [];
174
+ function visit(node, bindings, statementStart, anchors) {
175
+ const current = 'namespace' === node.kind
176
+ ? {
177
+ ...bindingsFor(node),
178
+ functions: declarations.get(nameOf(node).toLowerCase()) ?? new Set(),
179
+ }
180
+ : bindings;
181
+ const start = node.loc?.start.offset ?? statementStart;
182
+ const statement = [
183
+ 'expressionstatement',
184
+ 'echo',
185
+ 'return',
186
+ 'throw',
187
+ 'if',
188
+ 'while',
189
+ 'for',
190
+ 'foreach',
191
+ ].includes(node.kind)
192
+ ? start
193
+ : statementStart;
194
+ const currentAnchors = statement === start ? [start] : [start, ...anchors];
195
+ if ('call' === node.kind) {
196
+ const name = resolveFunction(node.what, current, globalFunctions);
197
+ const signature = name && Object.hasOwn(PHP_TRANSLATION_FUNCTIONS, name)
198
+ ? PHP_TRANSLATION_FUNCTIONS[name]
199
+ : undefined;
200
+ if (name && signature) {
201
+ const args = [];
202
+ for (const [index, argument] of (node.arguments ?? []).entries()) {
203
+ const position = 'namedargument' === argument.kind
204
+ ? signature.parameters.indexOf(nameOf(argument))
205
+ : index;
206
+ if (position >= 0) {
207
+ args[position] = staticString('namedargument' === argument.kind
208
+ ? argument.value
209
+ : argument);
210
+ }
211
+ }
212
+ calls.push({
213
+ name,
214
+ signature,
215
+ arguments: args,
216
+ file: input.file,
217
+ line: node.loc?.start.line,
218
+ column: (node.loc?.start.column ?? 0) + 1,
219
+ start,
220
+ statementStart: statement,
221
+ commentAnchors: currentAnchors,
222
+ });
223
+ }
224
+ }
225
+ nodes(node).forEach((child) => visit(child, current, statement, currentAnchors));
226
+ }
227
+ visit(ast, bindingsFor(ast), 0, []);
228
+ const comments = (ast.comments ?? []).map((comment) => ({
229
+ start: comment.loc?.start.offset ?? 0,
230
+ end: comment.loc?.end.offset ?? 0,
231
+ value: String(comment.value ?? ''),
232
+ }));
233
+ return extractCalls(input, calls, comments);
234
+ }
235
+ catch (error) {
236
+ return syntaxFailure(input.file, error);
237
+ }
238
+ }
@@ -0,0 +1,9 @@
1
+ export interface Placeholder {
2
+ position?: number;
3
+ type: string;
4
+ }
5
+ /**
6
+ * PHP sprintf and JS sprintf-compatible forms; literal %% consumes no argument.
7
+ * @param text
8
+ */
9
+ export declare function getPlaceholders(text: string): Placeholder[];
@@ -0,0 +1,13 @@
1
+ /**
2
+ * PHP sprintf and JS sprintf-compatible forms; literal %% consumes no argument.
3
+ * @param text
4
+ */
5
+ export function getPlaceholders(text) {
6
+ const matches = text.matchAll(/%%|%(?:(\d+)\$)?[-+ 0#]*(?:'[^\r\n])?\d*(?:\.\d+)?([bcdeEfFgGosuxX])/g);
7
+ return [...matches]
8
+ .filter((match) => '%%' !== match[0])
9
+ .map((match) => ({
10
+ ...(match[1] ? { position: Number(match[1]) } : {}),
11
+ type: match[2] ?? '',
12
+ }));
13
+ }
@@ -0,0 +1,24 @@
1
+ import type { Catalog, CatalogMessage } from './catalog.ts';
2
+ export interface TranslatedCatalog extends Catalog {
3
+ /** Headers parsed from the PO, always with string values. */
4
+ headers: Record<string, string>;
5
+ locale: string;
6
+ pluralForms: string;
7
+ nplurals: number;
8
+ }
9
+ /**
10
+ * Parse translated PO as data, filtering fuzzy, obsolete and untranslated entries.
11
+ * @param contents
12
+ * @param domain
13
+ */
14
+ export declare function parseTranslatedPo(contents: Buffer | string, domain: string): TranslatedCatalog;
15
+ /**
16
+ * WordPress/Jed lookup key uses context + EOT + singular, even for plural messages.
17
+ * @param message
18
+ */
19
+ export declare function translationKey(message: CatalogMessage): string;
20
+ /**
21
+ * Compile MO through gettext-parser; never write or mutate the input PO.
22
+ * @param catalog
23
+ */
24
+ export declare function compileMo(catalog: TranslatedCatalog): Buffer;
@@ -0,0 +1,180 @@
1
+ import { po, mo } from 'gettext-parser';
2
+ import postfixModule from '@tannin/postfix';
3
+ import { ValidationError } from "../errors.js";
4
+ import { compareText } from "./catalog.js";
5
+ const postfix = postfixModule;
6
+ /**
7
+ * Validate gettext plural expressions with Tannin's parser, without eval or Function.
8
+ * @param header
9
+ */
10
+ function validatePluralForms(header) {
11
+ const match = /^\s*nplurals\s*=\s*([1-9]\d*)\s*;\s*plural\s*=\s*([^;]+)\s*;?\s*$/.exec(header);
12
+ if (!match || Number(match[1]) > 100) {
13
+ throw new ValidationError('PO requires a valid Plural-Forms header with nplurals and a plural expression.');
14
+ }
15
+ const expression = match[2] ?? '';
16
+ if (/[^\dn\s!%*/+<>=&|?:()\-]/.test(expression)) {
17
+ throw new ValidationError('Plural-Forms contains an unsupported expression token.');
18
+ }
19
+ try {
20
+ let depth = 0;
21
+ for (const term of postfix(expression)) {
22
+ if ('n' === term || /^\d+$/.test(String(term))) {
23
+ depth++;
24
+ }
25
+ else {
26
+ if (![
27
+ '!',
28
+ '*',
29
+ '/',
30
+ '%',
31
+ '+',
32
+ '-',
33
+ '<',
34
+ '<=',
35
+ '>',
36
+ '>=',
37
+ '==',
38
+ '!=',
39
+ '&&',
40
+ '||',
41
+ '?:',
42
+ ].includes(term)) {
43
+ throw new Error('Unsupported plural operator.');
44
+ }
45
+ let arity = 2;
46
+ if ('!' === term) {
47
+ arity = 1;
48
+ }
49
+ if ('?:' === term) {
50
+ arity = 3;
51
+ }
52
+ if (depth < arity) {
53
+ throw new Error('Invalid operand count.');
54
+ }
55
+ depth -= arity - 1;
56
+ }
57
+ }
58
+ if (1 !== depth) {
59
+ throw new Error('Incomplete expression.');
60
+ }
61
+ }
62
+ catch (error) {
63
+ throw new ValidationError(`Malformed Plural-Forms expression: ${String(error)}`);
64
+ }
65
+ return Number(match[1]);
66
+ }
67
+ /**
68
+ * Parse translated PO as data, filtering fuzzy, obsolete and untranslated entries.
69
+ * @param contents
70
+ * @param domain
71
+ */
72
+ export function parseTranslatedPo(contents, domain) {
73
+ let parsed;
74
+ try {
75
+ parsed = po.parse(contents, {
76
+ validation: true,
77
+ defaultCharset: 'UTF-8',
78
+ });
79
+ }
80
+ catch (error) {
81
+ throw new ValidationError(`Malformed PO catalog: ${String(error)}`);
82
+ }
83
+ if (!parsed.headers || !parsed.translations) {
84
+ throw new ValidationError('Malformed PO catalog: missing gettext headers or translations.');
85
+ }
86
+ const headers = Object.fromEntries(Object.entries(parsed.headers).map(([key, value]) => [
87
+ key.toLowerCase(),
88
+ value,
89
+ ]));
90
+ const locale = headers.language;
91
+ if (!locale || !/^[A-Za-z][A-Za-z0-9_@.-]*$/.test(locale)) {
92
+ throw new ValidationError('PO requires a non-empty valid Language header.');
93
+ }
94
+ const pluralForms = headers['plural-forms'] ?? '';
95
+ const nplurals = validatePluralForms(pluralForms);
96
+ const messages = [];
97
+ for (const entry of Object.values(parsed.translations).flatMap((context) => Object.values(context))) {
98
+ const flags = (entry.comments?.flag ?? '')
99
+ .split(',')
100
+ .map((flag) => flag.trim())
101
+ .filter(Boolean);
102
+ if (entry.msgid &&
103
+ !entry.obsolete &&
104
+ !flags.includes('fuzzy') &&
105
+ entry.msgstr.every((translation) => !!translation)) {
106
+ if (entry.msgstr.length !==
107
+ (undefined !== entry.msgid_plural ? nplurals : 1) ||
108
+ /[\0\u0004]/.test(entry.msgid) ||
109
+ /[\0\u0004]/.test(entry.msgctxt ?? '') ||
110
+ entry.msgstr.some((value) => value.includes('\0'))) {
111
+ throw new ValidationError(`Invalid translation forms or reserved separators for ${JSON.stringify(entry.msgid)}.`);
112
+ }
113
+ const references = (entry.comments?.reference ?? '')
114
+ .split(/\s+/)
115
+ .filter(Boolean)
116
+ .map((reference) => {
117
+ const location = /^(.*?)(?::(\d+))?$/.exec(reference);
118
+ return {
119
+ file: location?.[1] ?? reference,
120
+ ...(location?.[2]
121
+ ? { line: Number(location[2]) }
122
+ : {}),
123
+ };
124
+ });
125
+ messages.push({
126
+ id: entry.msgid,
127
+ ...(undefined !== entry.msgid_plural
128
+ ? { plural: entry.msgid_plural }
129
+ : {}),
130
+ ...(undefined !== entry.msgctxt
131
+ ? { context: entry.msgctxt }
132
+ : {}),
133
+ translations: [...entry.msgstr],
134
+ flags,
135
+ references,
136
+ comments: (entry.comments?.extracted ?? '')
137
+ .split('\n')
138
+ .filter(Boolean),
139
+ });
140
+ }
141
+ }
142
+ messages.sort((a, b) => compareText(translationKey(a), translationKey(b)));
143
+ return { domain, headers, messages, locale, pluralForms, nplurals };
144
+ }
145
+ /**
146
+ * WordPress/Jed lookup key uses context + EOT + singular, even for plural messages.
147
+ * @param message
148
+ */
149
+ export function translationKey(message) {
150
+ return (message.context ? `${message.context}\u0004` : '') + message.id;
151
+ }
152
+ /**
153
+ * Compile MO through gettext-parser; never write or mutate the input PO.
154
+ * @param catalog
155
+ */
156
+ export function compileMo(catalog) {
157
+ const table = {
158
+ charset: 'UTF-8',
159
+ headers: {
160
+ ...catalog.headers,
161
+ 'content-type': 'text/plain; charset=UTF-8',
162
+ },
163
+ translations: Object.create(null),
164
+ };
165
+ for (const message of catalog.messages) {
166
+ const context = message.context ?? '';
167
+ table.translations[context] ??= Object.create(null);
168
+ table.translations[context][message.id] = {
169
+ msgid: message.id,
170
+ ...(undefined !== message.context
171
+ ? { msgctxt: message.context }
172
+ : {}),
173
+ ...(undefined !== message.plural
174
+ ? { msgid_plural: message.plural }
175
+ : {}),
176
+ msgstr: [...message.translations],
177
+ };
178
+ }
179
+ return mo.compile(table);
180
+ }
@@ -0,0 +1,16 @@
1
+ import type { Catalog } from './catalog.ts';
2
+ /**
3
+ * Serialize deterministic UTF-8 POT bytes in the gettext layout, with no wall-clock data.
4
+ *
5
+ * Messages keep the catalog order, and each entry lists extracted comments, references and flags before its strings.
6
+ *
7
+ * @param catalog Catalog to serialize.
8
+ * @param options Serialization options.
9
+ * @param options.references Whether to include source references (default true).
10
+ * @param options.fileComment Static text to prepend as gettext comments.
11
+ * @return POT file contents.
12
+ */
13
+ export declare function serializePot(catalog: Catalog, options?: {
14
+ references?: boolean;
15
+ fileComment?: string;
16
+ }): Buffer;
@@ -0,0 +1,119 @@
1
+ import { mergeI18nHeaders, validateI18nFileComment, validateI18nHeaders, } from "./options.js";
2
+ const ESCAPES = {
3
+ '\\': '\\\\',
4
+ '"': '\\"',
5
+ '\x07': '\\a',
6
+ '\b': '\\b',
7
+ '\f': '\\f',
8
+ '\n': '\\n',
9
+ '\r': '\\r',
10
+ '\t': '\\t',
11
+ '\v': '\\v',
12
+ };
13
+ /**
14
+ * Serialize deterministic UTF-8 POT bytes in the gettext layout, with no wall-clock data.
15
+ *
16
+ * Messages keep the catalog order, and each entry lists extracted comments, references and flags before its strings.
17
+ *
18
+ * @param catalog Catalog to serialize.
19
+ * @param options Serialization options.
20
+ * @param options.references Whether to include source references (default true).
21
+ * @param options.fileComment Static text to prepend as gettext comments.
22
+ * @return POT file contents.
23
+ */
24
+ export function serializePot(catalog, options = {}) {
25
+ validateI18nHeaders(catalog.headers);
26
+ if (undefined !== options.fileComment) {
27
+ validateI18nFileComment(options.fileComment);
28
+ }
29
+ const fileComment = undefined === options.fileComment
30
+ ? ''
31
+ : options.fileComment
32
+ .split(/\r\n|\r|\n/)
33
+ .map((line) => `# ${line}\n`)
34
+ .join('');
35
+ const entries = catalog.messages.map((message) => serializeMessage(message, false !== options.references));
36
+ return Buffer.from(fileComment + [serializeHeaders(catalog), ...entries].join('\n'));
37
+ }
38
+ /**
39
+ * Serialize the header entry: the default headers in a fixed order, overridden in place or removed with `false`, then custom headers in the given order.
40
+ *
41
+ * @param catalog Catalog with the domain and header overrides.
42
+ * @return Header entry.
43
+ */
44
+ function serializeHeaders(catalog) {
45
+ const defaults = {
46
+ 'Project-Id-Version': catalog.domain,
47
+ 'Report-Msgid-Bugs-To': '',
48
+ 'Last-Translator': '',
49
+ 'Language-Team': '',
50
+ 'MIME-Version': '1.0',
51
+ 'Content-Type': 'text/plain; charset=UTF-8',
52
+ 'Content-Transfer-Encoding': '8bit',
53
+ 'POT-Creation-Date': '',
54
+ 'PO-Revision-Date': '',
55
+ 'X-Domain': catalog.domain,
56
+ };
57
+ const headers = Object.entries(mergeI18nHeaders(defaults, catalog.headers))
58
+ .filter(([, value]) => false !== value)
59
+ .map(([name, value]) => `"${escape(`${name}: ${value}\n`)}"\n`);
60
+ return `msgid ""\nmsgstr ""\n${headers.join('')}`;
61
+ }
62
+ /**
63
+ * Serialize one message entry.
64
+ *
65
+ * @param message Catalog message.
66
+ * @param references Whether to include source references.
67
+ * @return Message entry.
68
+ */
69
+ function serializeMessage(message, references) {
70
+ const lines = [
71
+ ...message.comments.flatMap((comment) => comment.split(/\r\n|\r|\n/).map((line) => `#. ${line}`)),
72
+ ...(references
73
+ ? [
74
+ ...new Set(message.references.map((reference) => `#: ${reference.file}${reference.line ? `:${reference.line}` : ''}`)),
75
+ ]
76
+ : []),
77
+ ...(message.flags.length
78
+ ? [`#, ${message.flags.join(', ')}`]
79
+ : []),
80
+ ...(undefined !== message.context
81
+ ? [poString('msgctxt', message.context)]
82
+ : []),
83
+ poString('msgid', message.id),
84
+ ];
85
+ if (undefined === message.plural) {
86
+ lines.push('msgstr ""');
87
+ }
88
+ else {
89
+ lines.push(poString('msgid_plural', message.plural), 'msgstr[0] ""', 'msgstr[1] ""');
90
+ }
91
+ return lines.join('\n') + '\n';
92
+ }
93
+ /**
94
+ * Format a gettext keyword and string, splitting strings with newlines into one line per segment.
95
+ *
96
+ * @param keyword Gettext keyword, like msgid.
97
+ * @param value String value.
98
+ * @return Serialized keyword lines.
99
+ */
100
+ function poString(keyword, value) {
101
+ if (!value.includes('\n')) {
102
+ return `${keyword} "${escape(value)}"`;
103
+ }
104
+ return [
105
+ `${keyword} ""`,
106
+ ...value
107
+ .split(/(?<=\n)/)
108
+ .map((segment) => `"${escape(segment)}"`),
109
+ ].join('\n');
110
+ }
111
+ /**
112
+ * Escape a string for a quoted gettext string.
113
+ *
114
+ * @param value Raw string.
115
+ * @return Escaped string.
116
+ */
117
+ function escape(value) {
118
+ return value.replace(/[\\"\x07\b\f\n\r\t\v]/g, (character) => ESCAPES[character] ?? character);
119
+ }
@@ -0,0 +1,14 @@
1
+ import type { TranslatedCatalog } from './po.ts';
2
+ /** Many-to-many source references to runtime scripts, both relative to the package. */
3
+ export type ScriptTranslationMap = Record<string, string[]>;
4
+ export interface ScriptTranslationOutput {
5
+ file: string;
6
+ source: string;
7
+ bytes: Buffer;
8
+ }
9
+ /**
10
+ * Compile deterministic Jed catalogs using explicit source/build mapping or unbundled identity paths.
11
+ * @param catalog
12
+ * @param mapping
13
+ */
14
+ export declare function compileScriptTranslations(catalog: TranslatedCatalog, mapping?: ScriptTranslationMap): ScriptTranslationOutput[];