@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.
Files changed (47) hide show
  1. package/dist/api/catalog/extractFromFiles.d.ts +3 -3
  2. package/dist/api/catalog/extractFromFiles.js +15 -7
  3. package/dist/api/catalog/getTranslationsForCatalog.d.ts +11 -2
  4. package/dist/api/catalog/getTranslationsForCatalog.js +61 -28
  5. package/dist/api/catalog/mergeCatalog.js +3 -1
  6. package/dist/api/catalog/translations.d.ts +16 -0
  7. package/dist/api/catalog/translations.js +35 -0
  8. package/dist/api/catalog.d.ts +0 -1
  9. package/dist/api/catalog.js +4 -58
  10. package/dist/api/check/index.d.ts +8 -0
  11. package/dist/api/check/index.js +39 -0
  12. package/dist/api/check/missing.d.ts +2 -0
  13. package/dist/api/check/missing.js +56 -0
  14. package/dist/api/check/sync.d.ts +2 -0
  15. package/dist/api/check/sync.js +96 -0
  16. package/dist/api/check/types.d.ts +62 -0
  17. package/dist/api/check/types.js +13 -0
  18. package/dist/api/compile/compileLocale.js +6 -7
  19. package/dist/api/extractors/index.d.ts +1 -4
  20. package/dist/api/extractors/index.js +3 -10
  21. package/dist/api/formats/formatterWrapper.d.ts +1 -0
  22. package/dist/api/formats/formatterWrapper.js +12 -5
  23. package/dist/api/index.d.ts +3 -1
  24. package/dist/api/index.js +1 -1
  25. package/dist/api/messages.d.ts +8 -2
  26. package/dist/api/messages.js +29 -2
  27. package/dist/api/resolveWorkersOptions.js +17 -3
  28. package/dist/api/runBounded.d.ts +1 -0
  29. package/dist/api/runBounded.js +13 -0
  30. package/dist/api/utils.d.ts +5 -0
  31. package/dist/api/utils.js +7 -0
  32. package/dist/api/workerPools.d.ts +2 -0
  33. package/dist/api/workerPools.js +2 -0
  34. package/dist/index.d.ts +1 -1
  35. package/dist/index.js +1 -0
  36. package/dist/lingui-check.d.ts +6 -0
  37. package/dist/lingui-check.js +137 -0
  38. package/dist/lingui-compile.js +6 -6
  39. package/dist/lingui-extract-experimental.js +1 -1
  40. package/dist/lingui-extract-template.js +1 -1
  41. package/dist/lingui-extract.js +1 -1
  42. package/dist/lingui.js +1 -0
  43. package/dist/workers/missingWorker.d.ts +4 -0
  44. package/dist/workers/missingWorker.js +23 -0
  45. package/dist/workers/missingWorkerWrapper.prod.d.ts +3 -0
  46. package/dist/workers/missingWorkerWrapper.prod.js +2 -0
  47. 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 type { ExtractWorkerPool } from "../workerPools.js";
4
- export declare function extractFromFiles(paths: string[], onMessageExtracted: (msg: ExtractedMessage) => void, config: LinguiConfigNormalized): Promise<boolean>;
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[], onMessageExtracted: (msg: ExtractedMessage) => void, config: LinguiConfigNormalized): Promise<boolean>;
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, onMessageExtracted, config) {
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, onMessageExtracted, config);
40
+ const fileSuccess = await extract(filename, (next) => {
41
+ mergeExtractedMessage(next, messages, config);
42
+ }, config);
40
43
  catalogSuccess &&= fileSuccess;
41
44
  }
42
- return catalogSuccess;
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, onMessageExtracted, config) {
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
- onMessageExtracted(message);
96
+ mergeExtractedMessage(message, messages, config);
91
97
  });
92
98
  }
93
99
  });
94
- return catalogSuccess;
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
- export declare function getTranslationsForCatalog(catalog: Catalog, locale: string, options: GetTranslationsOptions): Promise<{
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 locales = new Set([
4
- locale,
5
- options.sourceLocale,
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 messages = Object.keys(input).reduce((acc, key) => {
16
- acc[key] = getTranslation(catalogs, input[key], locale, key, (event) => {
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
- }, options);
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 sourceLocaleFallback(catalog, key) {
27
- if (!catalog?.[key]) {
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 catalog[key].translation || catalog[key].message;
53
+ return message.translation || message.message;
31
54
  }
32
- function getTranslation(catalogs, msg, locale, key, onMissing, options) {
33
- const { fallbackLocales, sourceLocale } = options;
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
- return localeCatalog?.[key]?.translation;
58
+ const message = localeCatalog?.[key];
59
+ if (!isActiveMessage(message, ignoreObsolete)) {
60
+ return undefined;
61
+ }
62
+ return message.translation;
37
63
  };
38
- const getMultipleFallbacks = (_locale) => {
39
- const fL = getFallbackListForLocale(fallbackLocales, _locale);
40
- if (!fL.length)
64
+ const getMultipleFallbacks = () => {
65
+ if (!fallbackList.length)
41
66
  return null;
42
- for (const fallbackLocale of fL) {
43
- if (catalogs[fallbackLocale] && getTranslation(fallbackLocale)) {
44
- return getTranslation(fallbackLocale);
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
- getTranslation(locale) ||
83
+ catalogTranslation ||
57
84
  // We search in fallbackLocales as dependent of each locale
58
- getMultipleFallbacks(locale) ||
85
+ getMultipleFallbacks() ||
59
86
  (sourceLocale &&
60
87
  sourceLocale === locale &&
61
- sourceLocaleFallback(catalogs[sourceLocale], key));
62
- if (!translation) {
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 || sourceLocaleFallback(catalogs[sourceLocale], key) || "",
95
+ source: msg.message ||
96
+ sourceLocaleFallback(catalogs[sourceLocale], key, ignoreObsolete) ||
97
+ "",
66
98
  });
67
99
  }
68
100
  return (translation ||
69
- (sourceLocale && sourceLocaleFallback(catalogs[sourceLocale], key)) ||
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
+ }
@@ -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
  };
@@ -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, mergeExtractedMessage, } from "./catalog/extractFromFiles.js";
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
- const messages = {};
87
- const onMessageExtracted = (next) => {
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
- if (!catalogSuccess)
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,2 @@
1
+ import { CheckDefinition } from "./types.js";
2
+ export declare const missingCheck: CheckDefinition;
@@ -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,2 @@
1
+ import { CheckDefinition } from "./types.js";
2
+ export declare const syncCheck: CheckDefinition;
@@ -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;