@lingui/cli 6.7.0 → 6.8.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/dist/api/catalog/extractFromFiles.d.ts +3 -3
- package/dist/api/catalog/extractFromFiles.js +15 -7
- package/dist/api/catalog/getTranslationsForCatalog.d.ts +11 -2
- package/dist/api/catalog/getTranslationsForCatalog.js +61 -28
- package/dist/api/catalog/mergeCatalog.js +3 -1
- package/dist/api/catalog/translations.d.ts +16 -0
- package/dist/api/catalog/translations.js +35 -0
- package/dist/api/catalog.d.ts +0 -1
- package/dist/api/catalog.js +4 -58
- package/dist/api/check/index.d.ts +8 -0
- package/dist/api/check/index.js +39 -0
- package/dist/api/check/missing.d.ts +2 -0
- package/dist/api/check/missing.js +56 -0
- package/dist/api/check/sync.d.ts +2 -0
- package/dist/api/check/sync.js +96 -0
- package/dist/api/check/types.d.ts +62 -0
- package/dist/api/check/types.js +13 -0
- package/dist/api/compile/compileLocale.js +6 -7
- package/dist/api/extractors/index.d.ts +1 -4
- package/dist/api/extractors/index.js +3 -10
- package/dist/api/formats/formatterWrapper.d.ts +1 -0
- package/dist/api/formats/formatterWrapper.js +12 -5
- package/dist/api/index.d.ts +3 -1
- package/dist/api/index.js +1 -1
- package/dist/api/messages.d.ts +8 -2
- package/dist/api/messages.js +29 -2
- package/dist/api/resolveWorkersOptions.js +17 -3
- package/dist/api/runBounded.d.ts +1 -0
- package/dist/api/runBounded.js +13 -0
- package/dist/api/utils.d.ts +5 -0
- package/dist/api/utils.js +7 -0
- package/dist/api/workerPools.d.ts +2 -0
- package/dist/api/workerPools.js +2 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -0
- package/dist/lingui-check.d.ts +6 -0
- package/dist/lingui-check.js +137 -0
- package/dist/lingui-compile.js +6 -6
- package/dist/lingui-extract-experimental.js +1 -1
- package/dist/lingui-extract-template.js +1 -1
- package/dist/lingui-extract.js +1 -1
- package/dist/lingui.js +1 -0
- package/dist/workers/missingWorker.d.ts +4 -0
- package/dist/workers/missingWorker.js +23 -0
- package/dist/workers/missingWorkerWrapper.prod.d.ts +3 -0
- package/dist/workers/missingWorkerWrapper.prod.js +2 -0
- package/package.json +8 -8
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ExtractedMessage, LinguiConfigNormalized } from "@lingui/conf";
|
|
2
2
|
import { ExtractedCatalogType } from "../types.js";
|
|
3
|
-
import
|
|
4
|
-
export declare function extractFromFiles(paths: string[],
|
|
3
|
+
import { ExtractWorkerPool } from "../workerPools.js";
|
|
4
|
+
export declare function extractFromFiles(paths: string[], config: LinguiConfigNormalized): Promise<ExtractedCatalogType | undefined>;
|
|
5
5
|
export declare function mergeExtractedMessage(next: ExtractedMessage, messages: ExtractedCatalogType, config: LinguiConfigNormalized): void;
|
|
6
|
-
export declare function extractFromFilesWithWorkerPool(workerPool: ExtractWorkerPool, paths: string[],
|
|
6
|
+
export declare function extractFromFilesWithWorkerPool(workerPool: ExtractWorkerPool, paths: string[], config: LinguiConfigNormalized): Promise<ExtractedCatalogType | undefined>;
|
|
@@ -33,13 +33,18 @@ function mergePlaceholders(prev, next) {
|
|
|
33
33
|
});
|
|
34
34
|
return res;
|
|
35
35
|
}
|
|
36
|
-
export async function extractFromFiles(paths,
|
|
36
|
+
export async function extractFromFiles(paths, config) {
|
|
37
|
+
const messages = {};
|
|
37
38
|
let catalogSuccess = true;
|
|
38
39
|
for (const filename of paths) {
|
|
39
|
-
const fileSuccess = await extract(filename,
|
|
40
|
+
const fileSuccess = await extract(filename, (next) => {
|
|
41
|
+
mergeExtractedMessage(next, messages, config);
|
|
42
|
+
}, config);
|
|
40
43
|
catalogSuccess &&= fileSuccess;
|
|
41
44
|
}
|
|
42
|
-
|
|
45
|
+
if (!catalogSuccess)
|
|
46
|
+
return undefined;
|
|
47
|
+
return messages;
|
|
43
48
|
}
|
|
44
49
|
export function mergeExtractedMessage(next, messages, config) {
|
|
45
50
|
if (!messages[next.id]) {
|
|
@@ -74,12 +79,13 @@ export function mergeExtractedMessage(next, messages, config) {
|
|
|
74
79
|
placeholders: mergePlaceholders(prev.placeholders, next.placeholders),
|
|
75
80
|
};
|
|
76
81
|
}
|
|
77
|
-
export async function extractFromFilesWithWorkerPool(workerPool, paths,
|
|
82
|
+
export async function extractFromFilesWithWorkerPool(workerPool, paths, config) {
|
|
83
|
+
const messages = {};
|
|
84
|
+
let catalogSuccess = true;
|
|
78
85
|
const resolvedConfigPath = config.resolvedConfigPath;
|
|
79
86
|
if (!resolvedConfigPath) {
|
|
80
87
|
throw new Error("Multithreading is only supported when lingui config loaded from file system, not passed by API");
|
|
81
88
|
}
|
|
82
|
-
let catalogSuccess = true;
|
|
83
89
|
const results = await Promise.all(paths.map((filename) => workerPool.run(filename, resolvedConfigPath)));
|
|
84
90
|
results.forEach((result) => {
|
|
85
91
|
if (!result.success) {
|
|
@@ -87,9 +93,11 @@ export async function extractFromFilesWithWorkerPool(workerPool, paths, onMessag
|
|
|
87
93
|
}
|
|
88
94
|
else {
|
|
89
95
|
result.messages.forEach((message) => {
|
|
90
|
-
|
|
96
|
+
mergeExtractedMessage(message, messages, config);
|
|
91
97
|
});
|
|
92
98
|
}
|
|
93
99
|
});
|
|
94
|
-
|
|
100
|
+
if (!catalogSuccess)
|
|
101
|
+
return undefined;
|
|
102
|
+
return messages;
|
|
95
103
|
}
|
|
@@ -1,16 +1,25 @@
|
|
|
1
|
-
import { Catalog } from "../catalog.js";
|
|
2
1
|
import { FallbackLocales } from "@lingui/conf";
|
|
2
|
+
import type { AllCatalogsType, CatalogType } from "../types.js";
|
|
3
3
|
export type TranslationMissingEvent = {
|
|
4
4
|
source: string;
|
|
5
5
|
id: string;
|
|
6
6
|
};
|
|
7
|
+
export type MissingBehavior = "resolved" | "catalog";
|
|
8
|
+
export declare function isMissingBehavior(value: string): value is MissingBehavior;
|
|
7
9
|
export type GetTranslationsOptions = {
|
|
8
10
|
sourceLocale: string;
|
|
9
11
|
fallbackLocales: FallbackLocales;
|
|
12
|
+
missingBehavior?: MissingBehavior;
|
|
13
|
+
ignoreObsolete?: boolean;
|
|
10
14
|
};
|
|
11
|
-
|
|
15
|
+
type CatalogTranslationsReader = {
|
|
16
|
+
readAll(locales: string[]): Promise<AllCatalogsType>;
|
|
17
|
+
readTemplate(): Promise<CatalogType | undefined>;
|
|
18
|
+
};
|
|
19
|
+
export declare function getTranslationsForCatalog(catalog: CatalogTranslationsReader, locale: string, options: GetTranslationsOptions): Promise<{
|
|
12
20
|
missing: TranslationMissingEvent[];
|
|
13
21
|
messages: {
|
|
14
22
|
[id: string]: string;
|
|
15
23
|
};
|
|
16
24
|
}>;
|
|
25
|
+
export {};
|
|
@@ -1,21 +1,25 @@
|
|
|
1
1
|
import { getFallbackListForLocale } from "./getFallbackListForLocale.js";
|
|
2
|
+
export function isMissingBehavior(value) {
|
|
3
|
+
return value === "resolved" || value === "catalog";
|
|
4
|
+
}
|
|
2
5
|
export async function getTranslationsForCatalog(catalog, locale, options) {
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
...getFallbackListForLocale(options.fallbackLocales, locale),
|
|
7
|
-
]);
|
|
8
|
-
const [catalogs, template] = await Promise.all([
|
|
6
|
+
const fallbackList = getFallbackListForLocale(options.fallbackLocales, locale);
|
|
7
|
+
const locales = new Set([locale, options.sourceLocale, ...fallbackList]);
|
|
8
|
+
const [rawCatalogs, rawTemplate] = await Promise.all([
|
|
9
9
|
catalog.readAll(Array.from(locales)),
|
|
10
10
|
catalog.readTemplate(),
|
|
11
11
|
]);
|
|
12
|
+
const ignoreObsolete = options.ignoreObsolete ?? false;
|
|
13
|
+
const catalogs = withoutObsolete(rawCatalogs, ignoreObsolete);
|
|
14
|
+
const template = withoutObsoleteCatalog(rawTemplate, ignoreObsolete);
|
|
12
15
|
const sourceLocaleCatalog = catalogs[options.sourceLocale] || {};
|
|
13
16
|
const input = { ...template, ...sourceLocaleCatalog, ...catalogs[locale] };
|
|
14
17
|
const missing = [];
|
|
15
|
-
const
|
|
16
|
-
|
|
18
|
+
const missingBehavior = options.missingBehavior ?? "resolved";
|
|
19
|
+
const messages = Object.entries(input).reduce((acc, [key, msg]) => {
|
|
20
|
+
acc[key] = getTranslation(catalogs, msg, locale, key, options.sourceLocale, fallbackList, ignoreObsolete, missingBehavior, (event) => {
|
|
17
21
|
missing.push(event);
|
|
18
|
-
}
|
|
22
|
+
});
|
|
19
23
|
return acc;
|
|
20
24
|
}, {});
|
|
21
25
|
return {
|
|
@@ -23,25 +27,47 @@ export async function getTranslationsForCatalog(catalog, locale, options) {
|
|
|
23
27
|
messages,
|
|
24
28
|
};
|
|
25
29
|
}
|
|
26
|
-
function
|
|
27
|
-
|
|
30
|
+
function isActiveMessage(message, ignoreObsolete) {
|
|
31
|
+
return Boolean(message && (!ignoreObsolete || !message.obsolete));
|
|
32
|
+
}
|
|
33
|
+
function withoutObsolete(catalogs, ignoreObsolete) {
|
|
34
|
+
return Object.fromEntries(Object.entries(catalogs).map(([locale, catalog]) => [
|
|
35
|
+
locale,
|
|
36
|
+
withoutObsoleteCatalog(catalog, ignoreObsolete),
|
|
37
|
+
]));
|
|
38
|
+
}
|
|
39
|
+
function withoutObsoleteCatalog(catalog, ignoreObsolete) {
|
|
40
|
+
const activeCatalog = {};
|
|
41
|
+
Object.entries(catalog ?? {}).forEach(([id, message]) => {
|
|
42
|
+
if (isActiveMessage(message, ignoreObsolete)) {
|
|
43
|
+
activeCatalog[id] = message;
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
return activeCatalog;
|
|
47
|
+
}
|
|
48
|
+
function sourceLocaleFallback(catalog, key, ignoreObsolete) {
|
|
49
|
+
const message = catalog?.[key];
|
|
50
|
+
if (!isActiveMessage(message, ignoreObsolete)) {
|
|
28
51
|
return undefined;
|
|
29
52
|
}
|
|
30
|
-
return
|
|
53
|
+
return message.translation || message.message;
|
|
31
54
|
}
|
|
32
|
-
function getTranslation(catalogs, msg, locale, key,
|
|
33
|
-
const
|
|
34
|
-
const getTranslation = (_locale) => {
|
|
55
|
+
function getTranslation(catalogs, msg, locale, key, sourceLocale, fallbackList, ignoreObsolete, missingBehavior, onMissing) {
|
|
56
|
+
const getCatalogTranslation = (_locale) => {
|
|
35
57
|
const localeCatalog = catalogs[_locale];
|
|
36
|
-
|
|
58
|
+
const message = localeCatalog?.[key];
|
|
59
|
+
if (!isActiveMessage(message, ignoreObsolete)) {
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
return message.translation;
|
|
37
63
|
};
|
|
38
|
-
const getMultipleFallbacks = (
|
|
39
|
-
|
|
40
|
-
if (!fL.length)
|
|
64
|
+
const getMultipleFallbacks = () => {
|
|
65
|
+
if (!fallbackList.length)
|
|
41
66
|
return null;
|
|
42
|
-
for (const fallbackLocale of
|
|
43
|
-
|
|
44
|
-
|
|
67
|
+
for (const fallbackLocale of fallbackList) {
|
|
68
|
+
const fallbackTranslation = getCatalogTranslation(fallbackLocale);
|
|
69
|
+
if (catalogs[fallbackLocale] && fallbackTranslation) {
|
|
70
|
+
return fallbackTranslation;
|
|
45
71
|
}
|
|
46
72
|
}
|
|
47
73
|
};
|
|
@@ -51,22 +77,29 @@ function getTranslation(catalogs, msg, locale, key, onMissing, options) {
|
|
|
51
77
|
// -> template message
|
|
52
78
|
// ** last resort **
|
|
53
79
|
// -> id
|
|
80
|
+
const catalogTranslation = getCatalogTranslation(locale);
|
|
54
81
|
const translation =
|
|
55
82
|
// Get translation in target locale
|
|
56
|
-
|
|
83
|
+
catalogTranslation ||
|
|
57
84
|
// We search in fallbackLocales as dependent of each locale
|
|
58
|
-
getMultipleFallbacks(
|
|
85
|
+
getMultipleFallbacks() ||
|
|
59
86
|
(sourceLocale &&
|
|
60
87
|
sourceLocale === locale &&
|
|
61
|
-
sourceLocaleFallback(catalogs[sourceLocale], key));
|
|
62
|
-
|
|
88
|
+
sourceLocaleFallback(catalogs[sourceLocale], key, ignoreObsolete));
|
|
89
|
+
const isMissingTranslation = missingBehavior === "catalog"
|
|
90
|
+
? locale !== sourceLocale && !catalogTranslation
|
|
91
|
+
: !translation;
|
|
92
|
+
if (isMissingTranslation) {
|
|
63
93
|
onMissing({
|
|
64
94
|
id: key,
|
|
65
|
-
source: msg.message ||
|
|
95
|
+
source: msg.message ||
|
|
96
|
+
sourceLocaleFallback(catalogs[sourceLocale], key, ignoreObsolete) ||
|
|
97
|
+
"",
|
|
66
98
|
});
|
|
67
99
|
}
|
|
68
100
|
return (translation ||
|
|
69
|
-
(sourceLocale &&
|
|
101
|
+
(sourceLocale &&
|
|
102
|
+
sourceLocaleFallback(catalogs[sourceLocale], key, ignoreObsolete)) ||
|
|
70
103
|
// take from template
|
|
71
104
|
msg.message ||
|
|
72
105
|
key);
|
|
@@ -20,11 +20,13 @@ export function mergeCatalog(prevCatalog, nextCatalog, forSourceLocale, options)
|
|
|
20
20
|
obsoleteKeys = prevKeys;
|
|
21
21
|
}
|
|
22
22
|
// Initialize new catalog with new keys
|
|
23
|
+
// `translation` is added last, so a message keeps the same key order once it
|
|
24
|
+
// is merged from the previous catalog on the next extract
|
|
23
25
|
const newMessages = Object.fromEntries(newKeys.map((key) => [
|
|
24
26
|
key,
|
|
25
27
|
{
|
|
26
|
-
translation: forSourceLocale ? nextCatalog[key].message || key : "",
|
|
27
28
|
...nextCatalog[key],
|
|
29
|
+
translation: forSourceLocale ? nextCatalog[key].message || key : "",
|
|
28
30
|
},
|
|
29
31
|
]));
|
|
30
32
|
// Merge translations from previous catalog
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Catalog } from "../catalog.js";
|
|
2
|
+
import type { CheckFindingBase } from "../check/types.js";
|
|
3
|
+
import { TranslationMissingEvent } from "./getTranslationsForCatalog.js";
|
|
4
|
+
import type { MissingBehavior } from "./getTranslationsForCatalog.js";
|
|
5
|
+
export type MissingTranslationFinding = CheckFindingBase & {
|
|
6
|
+
code: "missing_translation";
|
|
7
|
+
locale: string;
|
|
8
|
+
};
|
|
9
|
+
export declare function getMissingTranslationFindings(catalog: Catalog, locale: string, missingBehavior?: MissingBehavior): Promise<MissingTranslationFinding[]>;
|
|
10
|
+
export declare function getCatalogTranslationsWithMissing(catalog: Catalog, locale: string, missingBehavior?: MissingBehavior): Promise<{
|
|
11
|
+
messages: {
|
|
12
|
+
[id: string]: string;
|
|
13
|
+
};
|
|
14
|
+
missing: TranslationMissingEvent[];
|
|
15
|
+
}>;
|
|
16
|
+
export declare function createMissingTranslationFinding(catalog: Catalog, locale: string, missing: TranslationMissingEvent): MissingTranslationFinding;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { toRootRelativePath } from "../utils.js";
|
|
2
|
+
import { getTranslationsForCatalog, } from "./getTranslationsForCatalog.js";
|
|
3
|
+
export async function getMissingTranslationFindings(catalog, locale, missingBehavior = "resolved") {
|
|
4
|
+
if (catalog.config.pseudoLocale.some((item) => item.locale === locale)) {
|
|
5
|
+
return [];
|
|
6
|
+
}
|
|
7
|
+
const { missing } = await getCatalogTranslationsWithMissing(catalog, locale, missingBehavior);
|
|
8
|
+
return missing.map((entry) => createMissingTranslationFinding(catalog, locale, entry));
|
|
9
|
+
}
|
|
10
|
+
function createMissingTranslationMessage(messageId, source) {
|
|
11
|
+
return source || source === messageId
|
|
12
|
+
? `${messageId}: (${source})`
|
|
13
|
+
: messageId;
|
|
14
|
+
}
|
|
15
|
+
export async function getCatalogTranslationsWithMissing(catalog, locale, missingBehavior = "resolved") {
|
|
16
|
+
const { messages, missing } = await getTranslationsForCatalog(catalog, locale, {
|
|
17
|
+
fallbackLocales: catalog.config.fallbackLocales,
|
|
18
|
+
sourceLocale: catalog.config.sourceLocale,
|
|
19
|
+
missingBehavior,
|
|
20
|
+
ignoreObsolete: true,
|
|
21
|
+
});
|
|
22
|
+
return {
|
|
23
|
+
messages,
|
|
24
|
+
missing,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export function createMissingTranslationFinding(catalog, locale, missing) {
|
|
28
|
+
const catalogPath = toRootRelativePath(catalog.config.rootDir, catalog.getFilename(locale));
|
|
29
|
+
return {
|
|
30
|
+
code: "missing_translation",
|
|
31
|
+
locale,
|
|
32
|
+
catalogPath,
|
|
33
|
+
message: createMissingTranslationMessage(missing.id, missing.source),
|
|
34
|
+
};
|
|
35
|
+
}
|
package/dist/api/catalog.d.ts
CHANGED
|
@@ -54,7 +54,6 @@ export declare class Catalog {
|
|
|
54
54
|
files?: string[];
|
|
55
55
|
workerPool?: ExtractWorkerPool;
|
|
56
56
|
}): Promise<ExtractedCatalogType | undefined>;
|
|
57
|
-
private collectWithExtractors;
|
|
58
57
|
merge(prevCatalogs: AllCatalogsType, nextCatalog: ExtractedCatalogType, options: MergeOptions): {
|
|
59
58
|
[k: string]: CatalogType;
|
|
60
59
|
};
|
package/dist/api/catalog.js
CHANGED
|
@@ -4,9 +4,8 @@ import { globSync } from "node:fs";
|
|
|
4
4
|
import normalize from "normalize-path";
|
|
5
5
|
import { getTranslationsForCatalog, } from "./catalog/getTranslationsForCatalog.js";
|
|
6
6
|
import { mergeCatalog } from "./catalog/mergeCatalog.js";
|
|
7
|
-
import { extractFromFiles, extractFromFilesWithWorkerPool,
|
|
7
|
+
import { extractFromFiles, extractFromFilesWithWorkerPool, } from "./catalog/extractFromFiles.js";
|
|
8
8
|
import { isDirectory, makePathRegexSafe, normalizeRelativePath, replacePlaceholders, writeFile, } from "./utils.js";
|
|
9
|
-
import { getConfiguredExtractors, isBatchExtractor, } from "./extractors/index.js";
|
|
10
9
|
const LOCALE = "{locale}";
|
|
11
10
|
const LOCALE_SUFFIX_RE = /\{locale\}.*$/;
|
|
12
11
|
export class Catalog {
|
|
@@ -83,63 +82,10 @@ export class Catalog {
|
|
|
83
82
|
const regex = new RegExp(options.files.join("|"), "i");
|
|
84
83
|
paths = paths.filter((path) => regex.test(normalize(path)));
|
|
85
84
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
mergeExtractedMessage(next, messages, this.config);
|
|
89
|
-
};
|
|
90
|
-
const extractors = getConfiguredExtractors(this.config);
|
|
91
|
-
// Optimized hot path: if there are no batch extractors defined, skip the more complex logic.
|
|
92
|
-
if (!extractors.some(isBatchExtractor)) {
|
|
93
|
-
const success = options.workerPool
|
|
94
|
-
? await extractFromFilesWithWorkerPool(options.workerPool, paths, onMessageExtracted, this.config)
|
|
95
|
-
: await extractFromFiles(paths, onMessageExtracted, this.config);
|
|
96
|
-
return success ? messages : undefined;
|
|
97
|
-
}
|
|
98
|
-
return await this.collectWithExtractors(extractors, paths, onMessageExtracted, messages, options);
|
|
99
|
-
}
|
|
100
|
-
async collectWithExtractors(extractors, paths, onMessageExtracted, messages, options) {
|
|
101
|
-
let remaining = paths;
|
|
102
|
-
let catalogSuccess = true;
|
|
103
|
-
for (const extractor of extractors) {
|
|
104
|
-
if (remaining.length === 0)
|
|
105
|
-
break;
|
|
106
|
-
const matched = [];
|
|
107
|
-
const unmatched = [];
|
|
108
|
-
for (const f of remaining) {
|
|
109
|
-
if (extractor.match(f)) {
|
|
110
|
-
matched.push(f);
|
|
111
|
-
}
|
|
112
|
-
else {
|
|
113
|
-
unmatched.push(f);
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
if (matched.length === 0)
|
|
117
|
-
continue;
|
|
118
|
-
remaining = unmatched;
|
|
119
|
-
if (isBatchExtractor(extractor)) {
|
|
120
|
-
try {
|
|
121
|
-
await extractor.extractFromFiles(matched, onMessageExtracted, {
|
|
122
|
-
linguiConfig: this.config,
|
|
123
|
-
});
|
|
124
|
-
}
|
|
125
|
-
catch (e) {
|
|
126
|
-
console.error(`Extractor failed: ${e.message}`);
|
|
127
|
-
console.error(e.stack);
|
|
128
|
-
catalogSuccess = false;
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
else {
|
|
132
|
-
const success = options.workerPool
|
|
133
|
-
? await extractFromFilesWithWorkerPool(options.workerPool, matched, onMessageExtracted, this.config)
|
|
134
|
-
: await extractFromFiles(matched, onMessageExtracted, this.config);
|
|
135
|
-
if (!success) {
|
|
136
|
-
catalogSuccess = false;
|
|
137
|
-
}
|
|
138
|
-
}
|
|
85
|
+
if (options.workerPool) {
|
|
86
|
+
return await extractFromFilesWithWorkerPool(options.workerPool, paths, this.config);
|
|
139
87
|
}
|
|
140
|
-
|
|
141
|
-
return undefined;
|
|
142
|
-
return messages;
|
|
88
|
+
return await extractFromFiles(paths, this.config);
|
|
143
89
|
}
|
|
144
90
|
/*
|
|
145
91
|
*
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { CheckDefinition, CheckRunOptions } from "./types.js";
|
|
2
|
+
export declare const checkDefinitionsByName: {
|
|
3
|
+
sync: CheckDefinition;
|
|
4
|
+
missing: CheckDefinition;
|
|
5
|
+
};
|
|
6
|
+
export declare function getRegisteredChecks(): readonly CheckDefinition[];
|
|
7
|
+
export declare function validateSupportedOptions(check: CheckDefinition, options: CheckRunOptions): void;
|
|
8
|
+
export declare function getCheck(inputCheck: string): CheckDefinition;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { missingCheck } from "./missing.js";
|
|
2
|
+
import { syncCheck } from "./sync.js";
|
|
3
|
+
import { checkSpecificOptions, } from "./types.js";
|
|
4
|
+
export const checkDefinitionsByName = {
|
|
5
|
+
sync: syncCheck,
|
|
6
|
+
missing: missingCheck,
|
|
7
|
+
};
|
|
8
|
+
const registeredChecks = Object.values(checkDefinitionsByName);
|
|
9
|
+
export function getRegisteredChecks() {
|
|
10
|
+
return registeredChecks;
|
|
11
|
+
}
|
|
12
|
+
function getSupportedOptions(check) {
|
|
13
|
+
return check.cli.options.map((option) => option.runOption);
|
|
14
|
+
}
|
|
15
|
+
const optionOwnerByOption = new Map(registeredChecks.flatMap((check) => check.cli.options.map((option) => [
|
|
16
|
+
option.runOption,
|
|
17
|
+
{ checkName: check.name, cliOptionName: option.name },
|
|
18
|
+
])));
|
|
19
|
+
export function validateSupportedOptions(check, options) {
|
|
20
|
+
checkSpecificOptions.forEach((option) => {
|
|
21
|
+
if (!options[option] || getSupportedOptions(check).includes(option)) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const owner = optionOwnerByOption.get(option);
|
|
25
|
+
if (!owner) {
|
|
26
|
+
throw new Error(`Unsupported check option \`${option}\`.`);
|
|
27
|
+
}
|
|
28
|
+
throw new Error(`Option \`--${owner.cliOptionName}\` can only be used with the \`${owner.checkName}\` check.`);
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
function isCheckName(inputCheck) {
|
|
32
|
+
return Object.prototype.hasOwnProperty.call(checkDefinitionsByName, inputCheck);
|
|
33
|
+
}
|
|
34
|
+
export function getCheck(inputCheck) {
|
|
35
|
+
if (!isCheckName(inputCheck)) {
|
|
36
|
+
throw new Error(`Unknown check ${inputCheck}.`);
|
|
37
|
+
}
|
|
38
|
+
return checkDefinitionsByName[inputCheck];
|
|
39
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { getMissingTranslationFindings } from "../catalog/translations.js";
|
|
2
|
+
import { runBounded } from "../runBounded.js";
|
|
3
|
+
import { createMissingWorkerPool } from "../workerPools.js";
|
|
4
|
+
import { finalizeCheckResult } from "./types.js";
|
|
5
|
+
import { getMissingBehaviorDescription } from "../messages.js";
|
|
6
|
+
export const missingCheck = {
|
|
7
|
+
name: "missing",
|
|
8
|
+
description: "Verify that message catalogs have no missing translations after fallbackLocales are applied.",
|
|
9
|
+
cli: {
|
|
10
|
+
options: [
|
|
11
|
+
{
|
|
12
|
+
name: "mode",
|
|
13
|
+
runOption: "missingBehavior",
|
|
14
|
+
description: "Missing translation behavior: resolved (after fallbackLocales) or catalog (before fallbackLocales)",
|
|
15
|
+
},
|
|
16
|
+
],
|
|
17
|
+
examples: [
|
|
18
|
+
{
|
|
19
|
+
description: "Check for missing translations after fallbackLocales",
|
|
20
|
+
command: "check missing",
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
description: "Check target catalogs before fallbackLocales",
|
|
24
|
+
command: "check missing --mode catalog",
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
description: "Check missing translations verbosely for a locale",
|
|
28
|
+
command: "check missing --locale pl --verbose",
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
},
|
|
32
|
+
async run(ctx) {
|
|
33
|
+
const tasks = ctx.locales.flatMap((locale) => ctx.catalogs.map((catalog) => ({
|
|
34
|
+
locale,
|
|
35
|
+
catalog,
|
|
36
|
+
})));
|
|
37
|
+
const resolvedConfigPath = ctx.config.resolvedConfigPath;
|
|
38
|
+
let workerPool;
|
|
39
|
+
if (ctx.workersOptions.poolSize > 0 && resolvedConfigPath) {
|
|
40
|
+
workerPool = createMissingWorkerPool(ctx.workersOptions);
|
|
41
|
+
}
|
|
42
|
+
let findings;
|
|
43
|
+
try {
|
|
44
|
+
findings = (await runBounded(tasks, ctx.workersOptions.poolSize, async ({ locale, catalog }) => workerPool
|
|
45
|
+
? workerPool.run(catalog.path, locale, ctx.missingBehavior, resolvedConfigPath)
|
|
46
|
+
: getMissingTranslationFindings(catalog, locale, ctx.missingBehavior))).flat();
|
|
47
|
+
}
|
|
48
|
+
finally {
|
|
49
|
+
if (workerPool) {
|
|
50
|
+
await workerPool.destroy();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const missingBehaviorDescription = getMissingBehaviorDescription(ctx.missingBehavior);
|
|
54
|
+
return finalizeCheckResult("missing", findings, `No missing translations found ${missingBehaviorDescription}.`, (count) => `Found ${count} missing translation(s) ${missingBehaviorDescription}.`);
|
|
55
|
+
},
|
|
56
|
+
};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { cleanObsolete, order } from "../catalog.js";
|
|
2
|
+
import { runBounded } from "../runBounded.js";
|
|
3
|
+
import { createExtractWorkerPool } from "../workerPools.js";
|
|
4
|
+
import { readFile, toRootRelativePath } from "../utils.js";
|
|
5
|
+
import { finalizeCheckResult, } from "./types.js";
|
|
6
|
+
function getSyncFailureSummary(findings) {
|
|
7
|
+
const outOfSyncCount = findings.filter((finding) => finding.code === "catalog_out_of_sync").length;
|
|
8
|
+
const extractionFailureCount = findings.filter((finding) => finding.code === "extract_failed").length;
|
|
9
|
+
if (extractionFailureCount === 0) {
|
|
10
|
+
return `Found ${outOfSyncCount} out-of-sync catalog file(s).`;
|
|
11
|
+
}
|
|
12
|
+
if (outOfSyncCount === 0) {
|
|
13
|
+
return `Found ${extractionFailureCount} extraction failure(s).`;
|
|
14
|
+
}
|
|
15
|
+
return `Found ${outOfSyncCount} out-of-sync catalog file(s) and ${extractionFailureCount} extraction failure(s).`;
|
|
16
|
+
}
|
|
17
|
+
async function getCatalogSyncFindings(catalog, ctx, workerPool) {
|
|
18
|
+
const [nextCatalog, prevCatalogs] = await Promise.all([
|
|
19
|
+
catalog.collect({ workerPool }),
|
|
20
|
+
catalog.readAll(ctx.locales),
|
|
21
|
+
]);
|
|
22
|
+
if (!nextCatalog) {
|
|
23
|
+
return [
|
|
24
|
+
{
|
|
25
|
+
code: "extract_failed",
|
|
26
|
+
message: "Failed to extract messages",
|
|
27
|
+
catalogPath: toRootRelativePath(ctx.config.rootDir, catalog.path),
|
|
28
|
+
},
|
|
29
|
+
];
|
|
30
|
+
}
|
|
31
|
+
const mergedCatalogs = catalog.merge(prevCatalogs, nextCatalog, {
|
|
32
|
+
overwrite: ctx.overwrite,
|
|
33
|
+
});
|
|
34
|
+
const findings = await runBounded(ctx.locales, ctx.workersOptions.poolSize, async (locale) => {
|
|
35
|
+
const filename = catalog.getFilename(locale);
|
|
36
|
+
const catalogPath = toRootRelativePath(ctx.config.rootDir, filename);
|
|
37
|
+
let nextLocaleCatalog = mergedCatalogs[locale];
|
|
38
|
+
if (ctx.clean) {
|
|
39
|
+
nextLocaleCatalog = cleanObsolete(nextLocaleCatalog);
|
|
40
|
+
}
|
|
41
|
+
const existing = await readFile(filename);
|
|
42
|
+
const expected = await catalog.format.serialize(filename, order(ctx.config.orderBy, nextLocaleCatalog), locale, existing);
|
|
43
|
+
if (existing === expected) {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
code: "catalog_out_of_sync",
|
|
48
|
+
message: existing !== undefined
|
|
49
|
+
? "Catalog is out of sync with extract output"
|
|
50
|
+
: "Catalog is missing and would be created by extract",
|
|
51
|
+
locale,
|
|
52
|
+
catalogPath,
|
|
53
|
+
};
|
|
54
|
+
});
|
|
55
|
+
return findings.filter((finding) => finding !== undefined);
|
|
56
|
+
}
|
|
57
|
+
export const syncCheck = {
|
|
58
|
+
name: "sync",
|
|
59
|
+
description: "Verify that locale catalogs are already synchronized with what lingui extract would write.",
|
|
60
|
+
cli: {
|
|
61
|
+
options: [
|
|
62
|
+
{
|
|
63
|
+
name: "clean",
|
|
64
|
+
runOption: "clean",
|
|
65
|
+
description: "Remove obsolete messages from the expected catalog before comparing it with the existing catalog. Existing obsolete messages are reported as out of sync.",
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
name: "overwrite",
|
|
69
|
+
runOption: "overwrite",
|
|
70
|
+
description: "Mirror extract --overwrite behavior when running sync check",
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
examples: [
|
|
74
|
+
{
|
|
75
|
+
description: "Check that catalogs are in sync with extract output",
|
|
76
|
+
command: "check sync",
|
|
77
|
+
},
|
|
78
|
+
],
|
|
79
|
+
},
|
|
80
|
+
async run(ctx) {
|
|
81
|
+
let workerPool;
|
|
82
|
+
if (ctx.workersOptions.poolSize > 0 && ctx.config.resolvedConfigPath) {
|
|
83
|
+
workerPool = createExtractWorkerPool(ctx.workersOptions);
|
|
84
|
+
}
|
|
85
|
+
let findings;
|
|
86
|
+
try {
|
|
87
|
+
findings = (await runBounded(ctx.catalogs, ctx.workersOptions.poolSize, (catalog) => getCatalogSyncFindings(catalog, ctx, workerPool))).flat();
|
|
88
|
+
}
|
|
89
|
+
finally {
|
|
90
|
+
if (workerPool) {
|
|
91
|
+
await workerPool.destroy();
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return finalizeCheckResult("sync", findings, "Catalogs are in sync with extract output.", () => getSyncFailureSummary(findings));
|
|
95
|
+
},
|
|
96
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { LinguiConfigNormalized } from "@lingui/conf";
|
|
2
|
+
import { Catalog } from "../catalog.js";
|
|
3
|
+
import { WorkersOptions } from "../resolveWorkersOptions.js";
|
|
4
|
+
import type { MissingTranslationFinding } from "../catalog/translations.js";
|
|
5
|
+
import type { MissingBehavior } from "../catalog/getTranslationsForCatalog.js";
|
|
6
|
+
export type CheckFindingBase = {
|
|
7
|
+
catalogPath: string;
|
|
8
|
+
message: string;
|
|
9
|
+
};
|
|
10
|
+
export type CheckName = "sync" | "missing";
|
|
11
|
+
export declare const checkSpecificOptions: readonly ["clean", "overwrite", "missingBehavior"];
|
|
12
|
+
export type CheckSpecificOption = (typeof checkSpecificOptions)[number];
|
|
13
|
+
export type CheckCliOptionName = "clean" | "overwrite" | "mode";
|
|
14
|
+
export type CheckCliExample = {
|
|
15
|
+
description: string;
|
|
16
|
+
command: string;
|
|
17
|
+
};
|
|
18
|
+
export type CheckCliOptionDefinition = {
|
|
19
|
+
name: CheckCliOptionName;
|
|
20
|
+
runOption: CheckSpecificOption;
|
|
21
|
+
description: string;
|
|
22
|
+
};
|
|
23
|
+
export type CatalogOutOfSyncFinding = CheckFindingBase & {
|
|
24
|
+
code: "catalog_out_of_sync";
|
|
25
|
+
locale: string;
|
|
26
|
+
};
|
|
27
|
+
export type ExtractFailedFinding = CheckFindingBase & {
|
|
28
|
+
code: "extract_failed";
|
|
29
|
+
};
|
|
30
|
+
export type CheckFinding = MissingTranslationFinding | CatalogOutOfSyncFinding | ExtractFailedFinding;
|
|
31
|
+
export type CheckResult = {
|
|
32
|
+
name: CheckName;
|
|
33
|
+
passed: boolean;
|
|
34
|
+
findings: CheckFinding[];
|
|
35
|
+
summary: string;
|
|
36
|
+
};
|
|
37
|
+
export type CheckContext = {
|
|
38
|
+
config: LinguiConfigNormalized;
|
|
39
|
+
catalogs: Catalog[];
|
|
40
|
+
locales: string[];
|
|
41
|
+
workersOptions: WorkersOptions;
|
|
42
|
+
clean: boolean;
|
|
43
|
+
overwrite: boolean;
|
|
44
|
+
missingBehavior: MissingBehavior;
|
|
45
|
+
};
|
|
46
|
+
export type CheckRunOptions = {
|
|
47
|
+
locale?: string[];
|
|
48
|
+
workersOptions: WorkersOptions;
|
|
49
|
+
clean?: boolean;
|
|
50
|
+
overwrite?: boolean;
|
|
51
|
+
missingBehavior?: MissingBehavior;
|
|
52
|
+
};
|
|
53
|
+
export type CheckDefinition = {
|
|
54
|
+
name: CheckName;
|
|
55
|
+
description: string;
|
|
56
|
+
cli: {
|
|
57
|
+
options: readonly CheckCliOptionDefinition[];
|
|
58
|
+
examples: readonly CheckCliExample[];
|
|
59
|
+
};
|
|
60
|
+
run: (ctx: CheckContext) => Promise<CheckResult>;
|
|
61
|
+
};
|
|
62
|
+
export declare function finalizeCheckResult(name: CheckName, findings: CheckFinding[], passMessage: string, failMessage: (count: number) => string): CheckResult;
|