@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.
Files changed (56) hide show
  1. package/README.md +124 -6
  2. package/dist/changeset-config.d.ts +5 -0
  3. package/dist/changeset-config.js +3 -0
  4. package/dist/cli.d.ts +5 -0
  5. package/dist/cli.js +10 -0
  6. package/dist/commands/i18n-audit.d.ts +2 -0
  7. package/dist/commands/i18n-audit.js +2 -0
  8. package/dist/commands/i18n-compile.d.ts +6 -0
  9. package/dist/commands/i18n-compile.js +76 -0
  10. package/dist/commands/i18n-json.d.ts +2 -0
  11. package/dist/commands/i18n-json.js +2 -0
  12. package/dist/commands/i18n-mo.d.ts +2 -0
  13. package/dist/commands/i18n-mo.js +2 -0
  14. package/dist/commands/i18n-php.d.ts +2 -0
  15. package/dist/commands/i18n-php.js +2 -0
  16. package/dist/commands/i18n-pot.d.ts +2 -0
  17. package/dist/commands/i18n-pot.js +2 -0
  18. package/dist/commands/i18n.d.ts +12 -0
  19. package/dist/commands/i18n.js +174 -0
  20. package/dist/i18n/atomic-output.d.ts +23 -0
  21. package/dist/i18n/atomic-output.js +90 -0
  22. package/dist/i18n/audit.d.ts +7 -0
  23. package/dist/i18n/audit.js +69 -0
  24. package/dist/i18n/catalog.d.ts +54 -0
  25. package/dist/i18n/catalog.js +86 -0
  26. package/dist/i18n/compile.d.ts +27 -0
  27. package/dist/i18n/compile.js +71 -0
  28. package/dist/i18n/extraction.d.ts +45 -0
  29. package/dist/i18n/extraction.js +105 -0
  30. package/dist/i18n/files.d.ts +7 -0
  31. package/dist/i18n/files.js +45 -0
  32. package/dist/i18n/generate.d.ts +49 -0
  33. package/dist/i18n/generate.js +240 -0
  34. package/dist/i18n/javascript.d.ts +7 -0
  35. package/dist/i18n/javascript.js +143 -0
  36. package/dist/i18n/metadata.d.ts +16 -0
  37. package/dist/i18n/metadata.js +117 -0
  38. package/dist/i18n/options.d.ts +52 -0
  39. package/dist/i18n/options.js +156 -0
  40. package/dist/i18n/packages.d.ts +24 -0
  41. package/dist/i18n/packages.js +86 -0
  42. package/dist/i18n/php.d.ts +9 -0
  43. package/dist/i18n/php.js +238 -0
  44. package/dist/i18n/placeholders.d.ts +9 -0
  45. package/dist/i18n/placeholders.js +13 -0
  46. package/dist/i18n/po.d.ts +24 -0
  47. package/dist/i18n/po.js +180 -0
  48. package/dist/i18n/pot.d.ts +16 -0
  49. package/dist/i18n/pot.js +119 -0
  50. package/dist/i18n/translation-json.d.ts +14 -0
  51. package/dist/i18n/translation-json.js +96 -0
  52. package/dist/i18n/translation-php.d.ts +6 -0
  53. package/dist/i18n/translation-php.js +35 -0
  54. package/dist/index.d.ts +22 -0
  55. package/dist/index.js +14 -0
  56. package/package.json +15 -3
@@ -0,0 +1,156 @@
1
+ import { ValidationError } from "../errors.js";
2
+ export const DEFAULT_I18N_INCLUDE = [
3
+ '**/*.{php,js,jsx,ts,tsx,mjs,cjs,mts,cts}',
4
+ '**/block.json',
5
+ '**/theme.json',
6
+ 'styles/**/*.json',
7
+ ];
8
+ export const DEFAULT_I18N_EXCLUDE = [
9
+ '**/node_modules/**',
10
+ '**/vendor/**',
11
+ '**/.*/**',
12
+ '**/tests/**',
13
+ '**/test/**',
14
+ '**/__tests__/**',
15
+ '**/*.test.*',
16
+ '**/*.spec.*',
17
+ '**/*.min.*',
18
+ '**/*.{pot,po,mo}',
19
+ '**/*.l10n.php',
20
+ ];
21
+ /**
22
+ * Validate external config independently of extraction and distribution file settings.
23
+ * @param input
24
+ * @param label
25
+ */
26
+ export function validateI18nOptions(input, label = 'i18n') {
27
+ if (undefined === input) {
28
+ return;
29
+ }
30
+ if (!input || 'object' !== typeof input || Array.isArray(input)) {
31
+ throw new ValidationError(`${label} must be an object.`);
32
+ }
33
+ const options = input;
34
+ const allowed = [
35
+ 'enabled',
36
+ 'domain',
37
+ 'allDomains',
38
+ 'include',
39
+ 'exclude',
40
+ 'destination',
41
+ 'headers',
42
+ 'fileComment',
43
+ 'references',
44
+ 'strict',
45
+ 'legacyGlobals',
46
+ 'suppress',
47
+ ];
48
+ for (const key of Object.keys(options)) {
49
+ if (!allowed.includes(key)) {
50
+ throw new ValidationError(`${label}.${key} is not a supported extraction option.`);
51
+ }
52
+ }
53
+ for (const key of [
54
+ 'enabled',
55
+ 'allDomains',
56
+ 'references',
57
+ 'strict',
58
+ 'legacyGlobals',
59
+ ]) {
60
+ if (undefined !== options[key] &&
61
+ 'boolean' !== typeof options[key]) {
62
+ throw new ValidationError(`${label}.${key} must be a boolean.`);
63
+ }
64
+ }
65
+ for (const key of ['domain', 'destination']) {
66
+ if (undefined !== options[key] &&
67
+ ('string' !== typeof options[key] || !options[key]?.trim())) {
68
+ throw new ValidationError(`${label}.${key} must be a non-empty string.`);
69
+ }
70
+ }
71
+ if (undefined !== options.fileComment) {
72
+ validateI18nFileComment(options.fileComment, `${label}.fileComment`);
73
+ }
74
+ for (const key of ['include', 'exclude', 'suppress']) {
75
+ const value = options[key];
76
+ if (undefined !== value &&
77
+ (!Array.isArray(value) ||
78
+ value.some((entry) => 'string' !== typeof entry || !entry.trim()))) {
79
+ throw new ValidationError(`${label}.${key} must be a list of non-empty strings.`);
80
+ }
81
+ }
82
+ if (undefined !== options.headers) {
83
+ validateI18nHeaders(options.headers, `${label}.headers`);
84
+ }
85
+ }
86
+ /**
87
+ * Validate static text to prepend as a POT file comment.
88
+ * @param fileComment
89
+ * @param label
90
+ */
91
+ export function validateI18nFileComment(fileComment, label = 'i18n.fileComment') {
92
+ if ('string' !== typeof fileComment ||
93
+ !fileComment.trim() ||
94
+ fileComment.includes('\0')) {
95
+ throw new ValidationError(`${label} must be a non-empty string without NUL characters.`);
96
+ }
97
+ }
98
+ /**
99
+ * Validate gettext header names and values independently of extraction options.
100
+ *
101
+ * @param headers Header values, or `false` to remove a header.
102
+ * @param label Config path for error messages.
103
+ * @throws {ValidationError} For invalid names or values, or a removed Content-Type.
104
+ */
105
+ export function validateI18nHeaders(headers, label = 'i18n.headers') {
106
+ if (!headers ||
107
+ 'object' !== typeof headers ||
108
+ Array.isArray(headers) ||
109
+ Object.entries(headers).some(([key, value]) => !/^[A-Za-z][A-Za-z0-9-]*$/.test(key) ||
110
+ (false !== value &&
111
+ ('string' !== typeof value || /[\r\n\0]/.test(value))))) {
112
+ throw new ValidationError(`${label} must contain gettext header names with single-line string values, or false to remove a header.`);
113
+ }
114
+ if (Object.entries(headers).some(([key, value]) => false === value && 'content-type' === key.toLowerCase())) {
115
+ throw new ValidationError(`${label} can't remove Content-Type, which declares the UTF-8 charset of the catalog.`);
116
+ }
117
+ }
118
+ /**
119
+ * Scalars and lists replace inherited values; header dictionaries merge by key.
120
+ * @param layers Extraction settings in increasing precedence.
121
+ */
122
+ export function resolveI18nOptions(...layers) {
123
+ let options = {
124
+ enabled: true,
125
+ include: DEFAULT_I18N_INCLUDE,
126
+ exclude: DEFAULT_I18N_EXCLUDE,
127
+ references: true,
128
+ legacyGlobals: true,
129
+ };
130
+ for (const layer of layers) {
131
+ validateI18nOptions(layer);
132
+ if (layer) {
133
+ options = {
134
+ ...options,
135
+ ...layer,
136
+ headers: mergeI18nHeaders(options.headers, layer.headers),
137
+ };
138
+ }
139
+ }
140
+ return options;
141
+ }
142
+ /**
143
+ * Merge gettext header names case-insensitively, preserving deliberate empty values and removals.
144
+ *
145
+ * @param layers Header values in increasing precedence.
146
+ * @return Merged headers, with `false` for removed headers so that later layers can still set them.
147
+ */
148
+ export function mergeI18nHeaders(...layers) {
149
+ const headers = new Map();
150
+ for (const layer of layers) {
151
+ for (const [key, value] of Object.entries(layer ?? {})) {
152
+ headers.set(key.toLowerCase(), [key, value]);
153
+ }
154
+ }
155
+ return Object.fromEntries(headers.values());
156
+ }
@@ -0,0 +1,24 @@
1
+ import type { I18nOptions } from './options.ts';
2
+ export interface I18nPackageInput {
3
+ cwd: string;
4
+ configFile?: string;
5
+ package?: string;
6
+ all?: boolean;
7
+ sourceDir?: string;
8
+ /** An explicit absolute output path; only supported for one package. */
9
+ outputPath?: string;
10
+ options?: I18nOptions;
11
+ /** Audit selection need not validate output destinations. */
12
+ validateOutputs?: boolean;
13
+ }
14
+ export interface I18nPackage {
15
+ package?: string;
16
+ sourceDir: string;
17
+ outputPath: string;
18
+ options: I18nOptions;
19
+ }
20
+ /**
21
+ * The config adapter resolves package directories and options before invoking primitives.
22
+ * @param input
23
+ */
24
+ export declare function resolveI18nPackages(input: I18nPackageInput): Promise<I18nPackage[]>;
@@ -0,0 +1,86 @@
1
+ import { basename, resolve } from 'node:path';
2
+ import { loadChangesetConfig } from "../changeset-config.js";
3
+ import { discoverConfiguredPackages, findPackage, } from "../changeset-packages.js";
4
+ import { ReleaseRepository } from "../release-repository.js";
5
+ import { readPackageMeta } from "../package-meta.js";
6
+ import { ValidationError } from "../errors.js";
7
+ import { validateI18nContainment } from "./atomic-output.js";
8
+ import { resolveI18nOptions } from "./options.js";
9
+ import { resolvePotPath, validatePotDestinations } from "./generate.js";
10
+ /**
11
+ * The config adapter resolves package directories and options before invoking primitives.
12
+ * @param input
13
+ */
14
+ export async function resolveI18nPackages(input) {
15
+ if ((input.package && input.all) ||
16
+ ((input.package || input.all) && input.sourceDir) ||
17
+ (input.all && input.outputPath)) {
18
+ throw new ValidationError('Choose a source directory, --package, or --all. --output requires one package.');
19
+ }
20
+ if ('' === input.package?.trim()) {
21
+ throw new ValidationError('--package must not be empty.');
22
+ }
23
+ const packages = [];
24
+ if (input.package || input.all) {
25
+ const loaded = await loadChangesetConfig(input);
26
+ if (!loaded) {
27
+ throw new ValidationError('--package and --all require wp-release configuration.');
28
+ }
29
+ const repository = new ReleaseRepository({
30
+ cwd: input.cwd,
31
+ });
32
+ const configured = await discoverConfiguredPackages(repository, loaded.config);
33
+ let selected = configured;
34
+ if (input.package) {
35
+ const match = findPackage(configured, input.package);
36
+ if (!match) {
37
+ throw new ValidationError(`Unknown package: ${input.package}.`);
38
+ }
39
+ selected = [match];
40
+ }
41
+ for (const pkg of selected) {
42
+ const options = resolveI18nOptions(loaded.config.i18n, pkg.group.i18n, input.options);
43
+ const meta = await readPackageMeta({
44
+ dir: pkg.directory,
45
+ slug: basename(pkg.directory),
46
+ });
47
+ if (input.package && (!options.enabled || !meta)) {
48
+ throw new ValidationError(`Package ${pkg.name} is disabled for i18n or has no WordPress headers.`);
49
+ }
50
+ if (options.enabled && meta) {
51
+ packages.push({
52
+ package: pkg.name,
53
+ sourceDir: pkg.directory,
54
+ options,
55
+ outputPath: input.outputPath ??
56
+ resolvePotPath(pkg.directory, meta, options),
57
+ });
58
+ }
59
+ }
60
+ }
61
+ else {
62
+ const sourceDir = resolve(input.cwd, input.sourceDir ?? '.');
63
+ const meta = await readPackageMeta({
64
+ dir: sourceDir,
65
+ slug: basename(sourceDir),
66
+ });
67
+ if (!meta) {
68
+ throw new ValidationError(`No WordPress plugin or theme headers found in ${sourceDir}.`);
69
+ }
70
+ const options = resolveI18nOptions(input.options);
71
+ packages.push({
72
+ sourceDir,
73
+ options,
74
+ outputPath: input.outputPath ?? resolvePotPath(sourceDir, meta, options),
75
+ });
76
+ }
77
+ if (false !== input.validateOutputs) {
78
+ await validatePotDestinations(packages.map((pkg) => pkg.outputPath));
79
+ if (!input.outputPath) {
80
+ for (const pkg of packages) {
81
+ await validateI18nContainment(pkg.sourceDir, pkg.outputPath);
82
+ }
83
+ }
84
+ }
85
+ return packages;
86
+ }
@@ -0,0 +1,9 @@
1
+ import type { CallSignature, ParseSourceInput } from './extraction.ts';
2
+ import type { ExtractionResult } from './catalog.ts';
3
+ /** PHP argument names and positions follow the WordPress l10n.php signatures. */
4
+ export declare const PHP_TRANSLATION_FUNCTIONS: Record<string, CallSignature>;
5
+ /**
6
+ * Parse PHP without executing it; namespaced fallback calls follow known file-local bindings.
7
+ * @param input
8
+ */
9
+ export declare function parsePhpTranslations(input: ParseSourceInput): ExtractionResult;
@@ -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;