@lingui/cli 6.6.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/README.md +27 -6
- 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 +8 -62
- 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 +11 -10
- 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/stats.js +3 -2
- 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/services/translationIO.js +2 -2
- 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 +10 -10
|
@@ -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;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export const checkSpecificOptions = [
|
|
2
|
+
"clean",
|
|
3
|
+
"overwrite",
|
|
4
|
+
"missingBehavior",
|
|
5
|
+
];
|
|
6
|
+
export function finalizeCheckResult(name, findings, passMessage, failMessage) {
|
|
7
|
+
return {
|
|
8
|
+
name,
|
|
9
|
+
passed: findings.length === 0,
|
|
10
|
+
findings,
|
|
11
|
+
summary: findings.length === 0 ? passMessage : failMessage(findings.length),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
@@ -5,8 +5,9 @@ import { ProgramExit } from "../ProgramExit.js";
|
|
|
5
5
|
import { createCompiledCatalog } from "../compile.js";
|
|
6
6
|
import normalizePath from "normalize-path";
|
|
7
7
|
import nodepath from "path";
|
|
8
|
-
import { createCompilationErrorMessage } from "../messages.js";
|
|
8
|
+
import { createCompilationErrorMessage, getMissingBehaviorDescription, } from "../messages.js";
|
|
9
9
|
import { getTranslationsForCatalog } from "../catalog/getTranslationsForCatalog.js";
|
|
10
|
+
import { createMissingTranslationFinding } from "../catalog/translations.js";
|
|
10
11
|
export async function compileLocale(catalogs, locale, options, config, doMerge, logger) {
|
|
11
12
|
let mergedCatalogs = {};
|
|
12
13
|
for (const catalog of catalogs) {
|
|
@@ -14,21 +15,20 @@ export async function compileLocale(catalogs, locale, options, config, doMerge,
|
|
|
14
15
|
fallbackLocales: config.fallbackLocales,
|
|
15
16
|
sourceLocale: config.sourceLocale,
|
|
16
17
|
});
|
|
18
|
+
const pseudoLocaleConfig = config.pseudoLocale.find((item) => item.locale === locale);
|
|
17
19
|
if (!options.allowEmpty &&
|
|
18
|
-
|
|
20
|
+
!pseudoLocaleConfig &&
|
|
19
21
|
missingMessages.length > 0) {
|
|
20
22
|
logger.error(styleText("red", `Error: Failed to compile catalog for locale ${styleText("bold", locale)}!`));
|
|
21
23
|
if (options.verbose) {
|
|
22
|
-
logger.error(styleText("red",
|
|
24
|
+
logger.error(styleText("red", `Missing translations ${getMissingBehaviorDescription("resolved")}:`));
|
|
23
25
|
missingMessages.forEach((missing) => {
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
: "";
|
|
27
|
-
logger.error(`${missing.id}${source}`);
|
|
26
|
+
const finding = createMissingTranslationFinding(catalog, locale, missing);
|
|
27
|
+
logger.error(`${finding.catalogPath}: ${finding.message}`);
|
|
28
28
|
});
|
|
29
29
|
}
|
|
30
30
|
else {
|
|
31
|
-
logger.error(styleText("red", `Missing ${missingMessages.length} translation(s)`));
|
|
31
|
+
logger.error(styleText("red", `Missing ${missingMessages.length} translation(s) ${getMissingBehaviorDescription("resolved")}`));
|
|
32
32
|
}
|
|
33
33
|
logger.error("");
|
|
34
34
|
throw new ProgramExit();
|
|
@@ -53,12 +53,13 @@ async function compileAndWrite(locale, config, options, writePath, messages, log
|
|
|
53
53
|
const namespace = options.typescript
|
|
54
54
|
? "ts"
|
|
55
55
|
: options.namespace || config.compileNamespace;
|
|
56
|
+
const pseudoLocaleConfig = config.pseudoLocale.find((item) => item.locale === locale);
|
|
56
57
|
const { source: compiledCatalog, errors } = createCompiledCatalog(locale, messages, {
|
|
57
58
|
strict: false,
|
|
58
59
|
namespace,
|
|
59
60
|
outputPrefix: options.outputPrefix,
|
|
60
|
-
pseudoLocale:
|
|
61
|
-
pseudoLocaleOptions:
|
|
61
|
+
pseudoLocale: pseudoLocaleConfig?.locale,
|
|
62
|
+
pseudoLocaleOptions: pseudoLocaleConfig?.options,
|
|
62
63
|
compilerBabelOptions: config.compilerBabelOptions,
|
|
63
64
|
});
|
|
64
65
|
if (errors.length) {
|
|
@@ -1,5 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export declare function isBatchExtractor(ext: ExtractorType): ext is Experimental__BatchExtractorType;
|
|
3
|
-
export declare function isPerFileExtractor(ext: ExtractorType): ext is PerFileExtractorType;
|
|
4
|
-
export declare const getConfiguredExtractors: (linguiConfig: LinguiConfigNormalized) => ExtractorType[];
|
|
1
|
+
import { ExtractedMessage, LinguiConfigNormalized } from "@lingui/conf";
|
|
5
2
|
export default function extract(filename: string, onMessageExtracted: (msg: ExtractedMessage) => void, linguiConfig: LinguiConfigNormalized): Promise<boolean>;
|
|
@@ -9,17 +9,10 @@ function createDefaultExtractor(linguiConfig) {
|
|
|
9
9
|
}
|
|
10
10
|
return defaultExtractor;
|
|
11
11
|
}
|
|
12
|
-
export function isBatchExtractor(ext) {
|
|
13
|
-
return "extractFromFiles" in ext && typeof ext.extractFromFiles === "function";
|
|
14
|
-
}
|
|
15
|
-
export function isPerFileExtractor(ext) {
|
|
16
|
-
return "extract" in ext && typeof ext.extract === "function";
|
|
17
|
-
}
|
|
18
|
-
export const getConfiguredExtractors = (linguiConfig) => {
|
|
19
|
-
return linguiConfig.extractors ?? [createDefaultExtractor(linguiConfig)];
|
|
20
|
-
};
|
|
21
12
|
export default async function extract(filename, onMessageExtracted, linguiConfig) {
|
|
22
|
-
const extractorsToExtract =
|
|
13
|
+
const extractorsToExtract = linguiConfig.extractors ?? [
|
|
14
|
+
createDefaultExtractor(linguiConfig),
|
|
15
|
+
];
|
|
23
16
|
for (const ext of extractorsToExtract) {
|
|
24
17
|
if (!ext.match(filename))
|
|
25
18
|
continue;
|
|
@@ -5,6 +5,7 @@ export declare class FormatterWrapper {
|
|
|
5
5
|
constructor(f: CatalogFormatter, sourceLocale: string);
|
|
6
6
|
getCatalogExtension(): string;
|
|
7
7
|
getTemplateExtension(): string;
|
|
8
|
+
serialize(filename: string, catalog: CatalogType, locale?: string, existing?: string): Promise<string>;
|
|
8
9
|
write(filename: string, catalog: CatalogType, locale?: string): Promise<void>;
|
|
9
10
|
read(filename: string, locale: string | undefined): Promise<CatalogType | undefined>;
|
|
10
11
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readFile,
|
|
1
|
+
import { readFile, writeFile } from "../utils.js";
|
|
2
2
|
import { RethrownError } from "../rethrownError.js";
|
|
3
3
|
export class FormatterWrapper {
|
|
4
4
|
f;
|
|
@@ -13,14 +13,21 @@ export class FormatterWrapper {
|
|
|
13
13
|
getTemplateExtension() {
|
|
14
14
|
return this.f.templateExtension || this.f.catalogExtension;
|
|
15
15
|
}
|
|
16
|
-
async
|
|
17
|
-
const
|
|
16
|
+
async serialize(filename, catalog, locale, existing) {
|
|
17
|
+
const resolvedExisting = arguments.length >= 4 ? existing : await readFile(filename);
|
|
18
|
+
return await this.f.serialize(catalog, {
|
|
18
19
|
locale,
|
|
19
20
|
sourceLocale: this.sourceLocale,
|
|
20
|
-
existing:
|
|
21
|
+
existing: resolvedExisting,
|
|
21
22
|
filename,
|
|
22
23
|
});
|
|
23
|
-
|
|
24
|
+
}
|
|
25
|
+
async write(filename, catalog, locale) {
|
|
26
|
+
const existing = await readFile(filename);
|
|
27
|
+
const content = await this.serialize(filename, catalog, locale, existing);
|
|
28
|
+
if (content !== existing) {
|
|
29
|
+
await writeFile(filename, content);
|
|
30
|
+
}
|
|
24
31
|
}
|
|
25
32
|
async read(filename, locale) {
|
|
26
33
|
const content = await readFile(filename);
|
package/dist/api/index.d.ts
CHANGED
|
@@ -3,5 +3,7 @@ export { getCatalogForFile, getCatalogs } from "./catalog/getCatalogs.js";
|
|
|
3
3
|
export { createCompiledCatalog } from "./compile.js";
|
|
4
4
|
export { default as extractor, extractFromFileWithBabel, } from "./extractors/babel.js";
|
|
5
5
|
export { getCatalogDependentFiles } from "./catalog/getCatalogDependentFiles.js";
|
|
6
|
-
export { createMissingErrorMessage, createCompilationErrorMessage, } from "./messages.js";
|
|
6
|
+
export { createMissingErrorMessage, createCompilationErrorMessage, isFailOnMissingEnabled, getFailOnMissingBehavior, formatFailOnMissingOption, } from "./messages.js";
|
|
7
|
+
export type { FailOnMissingOption } from "./messages.js";
|
|
8
|
+
export type { MissingBehavior } from "./catalog/getTranslationsForCatalog.js";
|
|
7
9
|
export * from "./types.js";
|
package/dist/api/index.js
CHANGED
|
@@ -3,5 +3,5 @@ export { getCatalogForFile, getCatalogs } from "./catalog/getCatalogs.js";
|
|
|
3
3
|
export { createCompiledCatalog } from "./compile.js";
|
|
4
4
|
export { default as extractor, extractFromFileWithBabel, } from "./extractors/babel.js";
|
|
5
5
|
export { getCatalogDependentFiles } from "./catalog/getCatalogDependentFiles.js";
|
|
6
|
-
export { createMissingErrorMessage, createCompilationErrorMessage, } from "./messages.js";
|
|
6
|
+
export { createMissingErrorMessage, createCompilationErrorMessage, isFailOnMissingEnabled, getFailOnMissingBehavior, formatFailOnMissingOption, } from "./messages.js";
|
|
7
7
|
export * from "./types.js";
|
package/dist/api/messages.d.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import { TranslationMissingEvent } from "./catalog/getTranslationsForCatalog.js";
|
|
2
|
-
import { MessageCompilationError } from "./compile.js";
|
|
1
|
+
import { type MissingBehavior, type TranslationMissingEvent } from "./catalog/getTranslationsForCatalog.js";
|
|
2
|
+
import type { MessageCompilationError } from "./compile.js";
|
|
3
|
+
export declare function getMissingBehaviorDescription(missingBehavior: MissingBehavior): "before applying fallbackLocales" | "after applying fallbackLocales";
|
|
4
|
+
export type FailOnMissingOption = boolean | MissingBehavior;
|
|
5
|
+
export declare function isFailOnMissingEnabled(option: FailOnMissingOption | undefined): option is true | "resolved" | "catalog";
|
|
6
|
+
export declare function getFailOnMissingBehavior(option: FailOnMissingOption | undefined): MissingBehavior;
|
|
7
|
+
export declare function formatFailOnMissingOption(option: FailOnMissingOption | undefined): "true" | "\"resolved\"" | "\"catalog\"" | "false";
|
|
3
8
|
export declare function createMissingErrorMessage(locale: string, missingMessages: TranslationMissingEvent[], configurationMsg: string): string;
|
|
9
|
+
export declare function createMissingErrorMessage(locale: string, missingMessages: TranslationMissingEvent[], missingBehavior?: MissingBehavior): string;
|
|
4
10
|
export declare function createCompilationErrorMessage(locale: string, errors: MessageCompilationError[]): string;
|
package/dist/api/messages.js
CHANGED
|
@@ -1,8 +1,35 @@
|
|
|
1
|
+
import { isMissingBehavior, } from "./catalog/getTranslationsForCatalog.js";
|
|
1
2
|
import { styleText } from "node:util";
|
|
2
|
-
export function
|
|
3
|
+
export function getMissingBehaviorDescription(missingBehavior) {
|
|
4
|
+
return missingBehavior === "catalog"
|
|
5
|
+
? "before applying fallbackLocales"
|
|
6
|
+
: "after applying fallbackLocales";
|
|
7
|
+
}
|
|
8
|
+
export function isFailOnMissingEnabled(option) {
|
|
9
|
+
return option === true || option === "resolved" || option === "catalog";
|
|
10
|
+
}
|
|
11
|
+
export function getFailOnMissingBehavior(option) {
|
|
12
|
+
return option === "catalog" ? "catalog" : "resolved";
|
|
13
|
+
}
|
|
14
|
+
export function formatFailOnMissingOption(option) {
|
|
15
|
+
if (option === true)
|
|
16
|
+
return "true";
|
|
17
|
+
if (option === "resolved")
|
|
18
|
+
return '"resolved"';
|
|
19
|
+
if (option === "catalog")
|
|
20
|
+
return '"catalog"';
|
|
21
|
+
return "false";
|
|
22
|
+
}
|
|
23
|
+
export function createMissingErrorMessage(locale, missingMessages, missingBehaviorOrConfigurationMsg = "resolved") {
|
|
24
|
+
const missingBehavior = isMissingBehavior(missingBehaviorOrConfigurationMsg ?? "")
|
|
25
|
+
? missingBehaviorOrConfigurationMsg
|
|
26
|
+
: undefined;
|
|
27
|
+
const missingBehaviorDescription = missingBehavior
|
|
28
|
+
? ` ${getMissingBehaviorDescription(missingBehavior)}`
|
|
29
|
+
: "";
|
|
3
30
|
let message = `Failed to compile catalog for locale ${styleText("bold", locale)}!
|
|
4
31
|
|
|
5
|
-
Missing ${missingMessages.length} translation(s):
|
|
32
|
+
Missing ${missingMessages.length} translation(s)${missingBehaviorDescription}:
|
|
6
33
|
\n`;
|
|
7
34
|
missingMessages.forEach((missing) => {
|
|
8
35
|
const source = missing.source || missing.source === missing.id
|
|
@@ -1,15 +1,29 @@
|
|
|
1
1
|
import * as os from "node:os";
|
|
2
|
+
function parseWorkers(workers) {
|
|
3
|
+
if (workers === undefined) {
|
|
4
|
+
return undefined;
|
|
5
|
+
}
|
|
6
|
+
const parsedWorkers = Number(workers);
|
|
7
|
+
if (!Number.isFinite(parsedWorkers) || !Number.isInteger(parsedWorkers)) {
|
|
8
|
+
throw new Error("The `--workers` option must be an integer.");
|
|
9
|
+
}
|
|
10
|
+
return parsedWorkers;
|
|
11
|
+
}
|
|
2
12
|
export function resolveWorkersOptions(opts) {
|
|
3
13
|
const cores = os.availableParallelism();
|
|
4
|
-
|
|
14
|
+
const workers = parseWorkers(opts.workers);
|
|
15
|
+
if (workers !== undefined && workers <= 1) {
|
|
16
|
+
return { poolSize: 0 };
|
|
17
|
+
}
|
|
18
|
+
if (cores === 1) {
|
|
5
19
|
return { poolSize: 0 };
|
|
6
20
|
}
|
|
7
|
-
if (
|
|
21
|
+
if (workers === undefined) {
|
|
8
22
|
if (cores <= 2) {
|
|
9
23
|
return { poolSize: cores }; // on tiny machines, use all
|
|
10
24
|
}
|
|
11
25
|
// on big machines cap to 8, to avoid trashing
|
|
12
26
|
return { poolSize: Math.min(cores - 1, 8) };
|
|
13
27
|
}
|
|
14
|
-
return { poolSize:
|
|
28
|
+
return { poolSize: workers };
|
|
15
29
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runBounded<T, R>(items: readonly T[], concurrency: number, worker: (item: T, index: number) => Promise<R>): Promise<R[]>;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export async function runBounded(items, concurrency, worker) {
|
|
2
|
+
const results = new Array(items.length);
|
|
3
|
+
const workerCount = Math.min(Math.max(concurrency, 1), items.length);
|
|
4
|
+
let nextIndex = 0;
|
|
5
|
+
const pool = Array.from({ length: workerCount }, async () => {
|
|
6
|
+
while (nextIndex < items.length) {
|
|
7
|
+
const currentIndex = nextIndex++;
|
|
8
|
+
results[currentIndex] = await worker(items[currentIndex], currentIndex);
|
|
9
|
+
}
|
|
10
|
+
});
|
|
11
|
+
await Promise.all(pool);
|
|
12
|
+
return results;
|
|
13
|
+
}
|
package/dist/api/stats.js
CHANGED
|
@@ -25,8 +25,9 @@ export function printStats(config, catalogs) {
|
|
|
25
25
|
return a.localeCompare(b);
|
|
26
26
|
})
|
|
27
27
|
.forEach((locale) => {
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
// skip pseudo locale
|
|
29
|
+
if (config.pseudoLocale.some((item) => item.locale === locale))
|
|
30
|
+
return;
|
|
30
31
|
const catalog = catalogs[locale];
|
|
31
32
|
// catalog is null if no catalog exists on disk and the locale
|
|
32
33
|
// was not extracted due to a `--locale` filter
|
package/dist/api/utils.d.ts
CHANGED
|
@@ -14,6 +14,11 @@ export declare function makeInstall(packageName: string, dev?: boolean): string;
|
|
|
14
14
|
* Preserve absolute paths: /absolute/path => /absolute/path
|
|
15
15
|
*/
|
|
16
16
|
export declare function normalizeRelativePath(sourcePath: string): string;
|
|
17
|
+
/**
|
|
18
|
+
* Normalize a path relative to the project rootDir, for display purposes
|
|
19
|
+
* (e.g. finding.catalogPath).
|
|
20
|
+
*/
|
|
21
|
+
export declare function toRootRelativePath(rootDir: string, filePath: string): string;
|
|
17
22
|
/**
|
|
18
23
|
* Escape special regex characters used in file-based routing systems
|
|
19
24
|
*/
|
package/dist/api/utils.js
CHANGED
|
@@ -87,6 +87,13 @@ export function normalizeRelativePath(sourcePath) {
|
|
|
87
87
|
return (normalizePath(path.relative(process.cwd(), sourcePath), false) +
|
|
88
88
|
(isDir ? "/" : ""));
|
|
89
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* Normalize a path relative to the project rootDir, for display purposes
|
|
92
|
+
* (e.g. finding.catalogPath).
|
|
93
|
+
*/
|
|
94
|
+
export function toRootRelativePath(rootDir, filePath) {
|
|
95
|
+
return normalizePath(path.relative(rootDir, filePath));
|
|
96
|
+
}
|
|
90
97
|
/**
|
|
91
98
|
* Escape special regex characters used in file-based routing systems
|
|
92
99
|
*/
|
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
import { WorkerPool } from "./typedPool.js";
|
|
2
2
|
import type { ExtractWorkerFunction } from "../workers/extractWorker.js";
|
|
3
|
+
import type { MissingWorkerFunction } from "../workers/missingWorker.js";
|
|
3
4
|
export type ExtractWorkerPool = WorkerPool<ExtractWorkerFunction>;
|
|
5
|
+
export type MissingWorkerPool = WorkerPool<MissingWorkerFunction>;
|
package/dist/api/workerPools.js
CHANGED
|
@@ -5,3 +5,5 @@ export const createExtractWorkerPool = (opts) => createWorkerPool("../workers/ex
|
|
|
5
5
|
export const createExtractExperimentalWorkerPool = (opts) => createWorkerPool("../extract-experimental/workers/extractWorkerWrapper", import.meta.url, opts.poolSize);
|
|
6
6
|
/** @internal */
|
|
7
7
|
export const createCompileWorkerPool = (opts) => createWorkerPool("../workers/compileWorkerWrapper", import.meta.url, opts.poolSize);
|
|
8
|
+
/** @internal */
|
|
9
|
+
export const createMissingWorkerPool = (opts) => createWorkerPool("../workers/missingWorkerWrapper", import.meta.url, opts.poolSize);
|
package/dist/index.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { defineConfig } from "@lingui/conf";
|
|
1
|
+
export { defineConfig, type LinguiConfig } from "@lingui/conf";
|
package/dist/index.js
CHANGED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { LinguiConfigNormalized } from "@lingui/conf";
|
|
3
|
+
import { CheckName, CheckRunOptions, CheckResult } from "./api/check/types.js";
|
|
4
|
+
export declare function renderCheckResult(result: CheckResult, verbose: boolean): string[];
|
|
5
|
+
export declare function runCheck(config: LinguiConfigNormalized, check: CheckName, options: CheckRunOptions): Promise<CheckResult>;
|
|
6
|
+
export declare function createProgram(): Command;
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { styleText } from "node:util";
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import ms from "ms";
|
|
4
|
+
import { getConfig } from "@lingui/conf";
|
|
5
|
+
import { helpRun } from "./api/help.js";
|
|
6
|
+
import { getCatalogs } from "./api/index.js";
|
|
7
|
+
import { getCheck, getRegisteredChecks, validateSupportedOptions, } from "./api/check/index.js";
|
|
8
|
+
import { resolveWorkersOptions } from "./api/resolveWorkersOptions.js";
|
|
9
|
+
import { isMissingBehavior } from "./api/catalog/getTranslationsForCatalog.js";
|
|
10
|
+
export function renderCheckResult(result, verbose) {
|
|
11
|
+
const status = result.passed
|
|
12
|
+
? styleText("green", "PASS")
|
|
13
|
+
: styleText("red", "FAIL");
|
|
14
|
+
const lines = [`${status} ${result.name}: ${result.summary}`];
|
|
15
|
+
if (verbose) {
|
|
16
|
+
lines.push(...result.findings.map((finding) => `${finding.catalogPath}: ${finding.message}`));
|
|
17
|
+
}
|
|
18
|
+
return lines;
|
|
19
|
+
}
|
|
20
|
+
function validateLocales(config, locales) {
|
|
21
|
+
if (!locales?.length) {
|
|
22
|
+
return config.locales;
|
|
23
|
+
}
|
|
24
|
+
const missingLocale = locales.find((locale) => !config.locales.includes(locale));
|
|
25
|
+
if (missingLocale) {
|
|
26
|
+
throw new Error(`Locale ${styleText("bold", missingLocale)} does not exist.`);
|
|
27
|
+
}
|
|
28
|
+
return locales;
|
|
29
|
+
}
|
|
30
|
+
export async function runCheck(config, check, options) {
|
|
31
|
+
const checkDefinition = getCheck(check);
|
|
32
|
+
validateSupportedOptions(checkDefinition, options);
|
|
33
|
+
const locales = validateLocales(config, options.locale);
|
|
34
|
+
const catalogs = await getCatalogs(config);
|
|
35
|
+
return await checkDefinition.run({
|
|
36
|
+
config,
|
|
37
|
+
catalogs,
|
|
38
|
+
locales,
|
|
39
|
+
workersOptions: options.workersOptions,
|
|
40
|
+
clean: options.clean ?? false,
|
|
41
|
+
overwrite: options.overwrite ?? false,
|
|
42
|
+
missingBehavior: options.missingBehavior ?? "resolved",
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
function parseLocales(value) {
|
|
46
|
+
return value
|
|
47
|
+
.split(",")
|
|
48
|
+
.map((locale) => locale.trim())
|
|
49
|
+
.filter(Boolean);
|
|
50
|
+
}
|
|
51
|
+
function addCommonOptions(command) {
|
|
52
|
+
return command
|
|
53
|
+
.option("--config <path>", "Path to the config file")
|
|
54
|
+
.option("--locale <locale, [...]>", "Only check the specified locales", parseLocales)
|
|
55
|
+
.option("--workers <n>", "Number of worker threads to use (default: CPU count - 1, capped at 8; on 1-2 core machines, all cores). Pass `--workers 1` to disable worker threads and run everything in a single process")
|
|
56
|
+
.option("--verbose", "Verbose output");
|
|
57
|
+
}
|
|
58
|
+
function parseMissingBehavior(value) {
|
|
59
|
+
if (isMissingBehavior(value)) {
|
|
60
|
+
return value;
|
|
61
|
+
}
|
|
62
|
+
throw new Error("Option `--mode` must be either `resolved` or `catalog`.");
|
|
63
|
+
}
|
|
64
|
+
function renderHelpExamples(examples) {
|
|
65
|
+
if (!examples.length) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
console.log("\n Examples:\n");
|
|
69
|
+
examples.forEach((example, index) => {
|
|
70
|
+
console.log(` # ${example.description}`);
|
|
71
|
+
console.log(` $ ${helpRun(example.command)}`);
|
|
72
|
+
if (index < examples.length - 1) {
|
|
73
|
+
console.log("");
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
async function runCliCommand(check, options) {
|
|
78
|
+
const startTime = Date.now();
|
|
79
|
+
const config = getConfig({ configPath: options.config });
|
|
80
|
+
const verbose = options.verbose ?? false;
|
|
81
|
+
console.log("Checking message catalogs…");
|
|
82
|
+
const result = await runCheck(config, check, {
|
|
83
|
+
locale: options.locale,
|
|
84
|
+
workersOptions: resolveWorkersOptions(options),
|
|
85
|
+
clean: options.clean,
|
|
86
|
+
overwrite: options.overwrite,
|
|
87
|
+
missingBehavior: options.mode,
|
|
88
|
+
});
|
|
89
|
+
const output = result.passed ? console.log : console.error;
|
|
90
|
+
renderCheckResult(result, verbose).forEach((line) => {
|
|
91
|
+
output(line);
|
|
92
|
+
});
|
|
93
|
+
console.log(`Done in ${ms(Date.now() - startTime)}`);
|
|
94
|
+
return result.passed;
|
|
95
|
+
}
|
|
96
|
+
function registerCheckCommand(checkProgram, check) {
|
|
97
|
+
const command = addCommonOptions(checkProgram.command(check.name).description(check.description));
|
|
98
|
+
check.cli.options.forEach((option) => {
|
|
99
|
+
if (option.name === "mode") {
|
|
100
|
+
command.option(`--${option.name} <mode>`, option.description, parseMissingBehavior);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
command.option(`--${option.name}`, option.description);
|
|
104
|
+
});
|
|
105
|
+
command
|
|
106
|
+
.on("--help", function () {
|
|
107
|
+
renderHelpExamples(check.cli.examples);
|
|
108
|
+
})
|
|
109
|
+
.action(async (options) => {
|
|
110
|
+
if (!(await runCliCommand(check.name, options))) {
|
|
111
|
+
process.exitCode = 1;
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
export function createProgram() {
|
|
116
|
+
const checkProgram = new Command()
|
|
117
|
+
.name("lingui check")
|
|
118
|
+
.description("Check message catalogs.")
|
|
119
|
+
.action(() => {
|
|
120
|
+
checkProgram.help({ error: true });
|
|
121
|
+
})
|
|
122
|
+
.on("--help", function () {
|
|
123
|
+
renderHelpExamples(getRegisteredChecks().flatMap((check) => check.cli.examples));
|
|
124
|
+
});
|
|
125
|
+
getRegisteredChecks().forEach((check) => {
|
|
126
|
+
registerCheckCommand(checkProgram, check);
|
|
127
|
+
});
|
|
128
|
+
return checkProgram;
|
|
129
|
+
}
|
|
130
|
+
if (import.meta.main) {
|
|
131
|
+
createProgram()
|
|
132
|
+
.parseAsync(process.argv)
|
|
133
|
+
.catch((error) => {
|
|
134
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
135
|
+
process.exit(1);
|
|
136
|
+
});
|
|
137
|
+
}
|