@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.
Files changed (50) hide show
  1. package/README.md +27 -6
  2. package/dist/api/catalog/extractFromFiles.d.ts +3 -3
  3. package/dist/api/catalog/extractFromFiles.js +15 -7
  4. package/dist/api/catalog/getTranslationsForCatalog.d.ts +11 -2
  5. package/dist/api/catalog/getTranslationsForCatalog.js +61 -28
  6. package/dist/api/catalog/mergeCatalog.js +3 -1
  7. package/dist/api/catalog/translations.d.ts +16 -0
  8. package/dist/api/catalog/translations.js +35 -0
  9. package/dist/api/catalog.d.ts +0 -1
  10. package/dist/api/catalog.js +8 -62
  11. package/dist/api/check/index.d.ts +8 -0
  12. package/dist/api/check/index.js +39 -0
  13. package/dist/api/check/missing.d.ts +2 -0
  14. package/dist/api/check/missing.js +56 -0
  15. package/dist/api/check/sync.d.ts +2 -0
  16. package/dist/api/check/sync.js +96 -0
  17. package/dist/api/check/types.d.ts +62 -0
  18. package/dist/api/check/types.js +13 -0
  19. package/dist/api/compile/compileLocale.js +11 -10
  20. package/dist/api/extractors/index.d.ts +1 -4
  21. package/dist/api/extractors/index.js +3 -10
  22. package/dist/api/formats/formatterWrapper.d.ts +1 -0
  23. package/dist/api/formats/formatterWrapper.js +12 -5
  24. package/dist/api/index.d.ts +3 -1
  25. package/dist/api/index.js +1 -1
  26. package/dist/api/messages.d.ts +8 -2
  27. package/dist/api/messages.js +29 -2
  28. package/dist/api/resolveWorkersOptions.js +17 -3
  29. package/dist/api/runBounded.d.ts +1 -0
  30. package/dist/api/runBounded.js +13 -0
  31. package/dist/api/stats.js +3 -2
  32. package/dist/api/utils.d.ts +5 -0
  33. package/dist/api/utils.js +7 -0
  34. package/dist/api/workerPools.d.ts +2 -0
  35. package/dist/api/workerPools.js +2 -0
  36. package/dist/index.d.ts +1 -1
  37. package/dist/index.js +1 -0
  38. package/dist/lingui-check.d.ts +6 -0
  39. package/dist/lingui-check.js +137 -0
  40. package/dist/lingui-compile.js +6 -6
  41. package/dist/lingui-extract-experimental.js +1 -1
  42. package/dist/lingui-extract-template.js +1 -1
  43. package/dist/lingui-extract.js +1 -1
  44. package/dist/lingui.js +1 -0
  45. package/dist/services/translationIO.js +2 -2
  46. package/dist/workers/missingWorker.d.ts +4 -0
  47. package/dist/workers/missingWorker.js +23 -0
  48. package/dist/workers/missingWorkerWrapper.prod.d.ts +3 -0
  49. package/dist/workers/missingWorkerWrapper.prod.js +2 -0
  50. 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
- locale !== config.pseudoLocale.locale &&
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", "Missing translations:"));
24
+ logger.error(styleText("red", `Missing translations ${getMissingBehaviorDescription("resolved")}:`));
23
25
  missingMessages.forEach((missing) => {
24
- const source = missing.source || missing.source === missing.id
25
- ? `: (${missing.source})`
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: config.pseudoLocale.locale,
61
- pseudoLocaleOptions: config.pseudoLocale.options,
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 { Experimental__BatchExtractorType, ExtractedMessage, ExtractorType, LinguiConfigNormalized, PerFileExtractorType } from "@lingui/conf";
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 = getConfiguredExtractors(linguiConfig).filter(isPerFileExtractor);
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, writeFileIfChanged } from "../utils.js";
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 write(filename, catalog, locale) {
17
- const content = await this.f.serialize(catalog, {
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: await readFile(filename),
21
+ existing: resolvedExisting,
21
22
  filename,
22
23
  });
23
- await writeFileIfChanged(filename, content);
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);
@@ -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";
@@ -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;
@@ -1,8 +1,35 @@
1
+ import { isMissingBehavior, } from "./catalog/getTranslationsForCatalog.js";
1
2
  import { styleText } from "node:util";
2
- export function createMissingErrorMessage(locale, missingMessages, configurationMsg) {
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
- if (Number(opts.workers) <= 1 || cores === 1) {
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 (!opts.workers) {
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: Number(opts.workers) };
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
- if (locale === config.pseudoLocale.locale)
29
- return; // skip pseudo locale
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
@@ -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>;
@@ -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
@@ -1 +1,2 @@
1
+ // Re-export both the `defineConfig` function and the types it depends on
1
2
  export { defineConfig } from "@lingui/conf";
@@ -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
+ }