@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
package/README.md CHANGED
@@ -4,22 +4,43 @@
4
4
 
5
5
  # @lingui/cli
6
6
 
7
- > lingui command line library for manipulating message catalogues
7
+ > Lingui CLI: extracts i18n messages from source code into catalogs and compiles the translated catalogs for the runtime
8
8
 
9
- `@lingui/cli` is part of [LinguiJS][linguijs]. See the [documentation][documentation] for all information, tutorials and examples.
9
+ `@lingui/cli` is part of [Lingui][documentation]. Lingui is a lightweight, open-source internationalization (i18n) library for JavaScript and TypeScript. It brings compile-time macros and a CLI for message extraction to React, React Native, Vue, SolidJS, Astro, Svelte, and Node.js.
10
10
 
11
- ## Installation & Usage
11
+ The `lingui` command reads `lingui.config.js` (or `.ts`) from the project root: the source locale, the target locales and where the catalogs live. The [installation guide][installation] shows the minimal file and the [configuration reference][conf] lists every option.
12
12
 
13
- See the [reference][reference] documentation.
13
+ ## Installation
14
+
15
+ ```sh
16
+ npm install --save-dev @lingui/cli
17
+ ```
18
+
19
+ ## Usage
20
+
21
+ - `lingui extract` finds the messages in the source, merges them into one catalog per locale, keeps existing translations and marks removed messages as obsolete.
22
+ - `lingui compile` compiles the translated catalogs into JavaScript modules for the runtime and reports messages with invalid ICU syntax.
23
+ - `lingui extract-template` writes a single `.pot` template with the source messages, for translation platforms that create the locale files themselves.
24
+ - `lingui extract-experimental` follows the imports from each entry point instead of a glob, so a multi-page app gets one catalog per page.
25
+
26
+ Extraction parses JavaScript and TypeScript out of the box; Vue single-file components need [`@lingui/extractor-vue`][extractor-vue]. Catalogs are PO files by default, with [other formats][catalog-formats] available as separate packages. To let the bundler compile catalogs instead of running `lingui compile`, use [`@lingui/vite-plugin`][vite-plugin], [`@lingui/loader`][loader] or [`@lingui/metro-transformer`][metro-transformer].
27
+
28
+ See the [CLI reference][reference] for all commands and options.
14
29
 
15
30
  ## License
16
31
 
17
- This package is licensed under [MIT][license] license.
32
+ This package is licensed under the [MIT][license] license.
18
33
 
19
34
  [license]: https://github.com/lingui/js-lingui/blob/main/LICENSE
20
- [linguijs]: https://github.com/lingui/js-lingui
21
35
  [documentation]: https://lingui.dev
36
+ [installation]: https://lingui.dev/installation
37
+ [conf]: https://lingui.dev/ref/conf
22
38
  [reference]: https://lingui.dev/ref/cli
39
+ [catalog-formats]: https://lingui.dev/ref/catalog-formats
40
+ [extractor-vue]: https://lingui.dev/ref/extractor-vue
41
+ [vite-plugin]: https://lingui.dev/ref/vite-plugin
42
+ [loader]: https://lingui.dev/ref/loader
43
+ [metro-transformer]: https://lingui.dev/ref/metro-transformer
23
44
  [package]: https://www.npmjs.com/package/@lingui/cli
24
45
  [badge-downloads]: https://img.shields.io/npm/dw/@lingui/cli.svg
25
46
  [badge-version]: https://img.shields.io/npm/v/@lingui/cli.svg
@@ -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
  *
@@ -273,12 +219,15 @@ export function order(by, catalog) {
273
219
  return acc;
274
220
  }, {});
275
221
  }
222
+ // hardcoded en-US locale to have consistent sorting
223
+ // @see https://github.com/lingui/js-lingui/pull/1808
224
+ const collator = new Intl.Collator("en-US");
276
225
  /**
277
226
  * Object keys are in the same order as they were created
278
227
  * https://stackoverflow.com/a/31102605/1535540
279
228
  */
280
229
  const orderByMessageId = (a, b) => {
281
- return a.messageId.localeCompare(b.messageId);
230
+ return collator.compare(a.messageId, b.messageId);
282
231
  };
283
232
  const orderByOrigin = (a, b) => {
284
233
  if (!a.entry.origin || !b.entry.origin) {
@@ -323,9 +272,6 @@ export async function writeCompiled(path, locale, compiledCatalog, namespace) {
323
272
  await writeFile(filename, compiledCatalog);
324
273
  return filename;
325
274
  }
326
- // hardcoded en-US locale to have consistent sorting
327
- // @see https://github.com/lingui/js-lingui/pull/1808
328
- const collator = new Intl.Collator("en-US");
329
275
  export const orderByMessage = (a, b) => {
330
276
  const aMsg = a.entry.message || "";
331
277
  const bMsg = b.entry.message || "";
@@ -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;