@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
package/README.md
CHANGED
|
@@ -4,22 +4,43 @@
|
|
|
4
4
|
|
|
5
5
|
# @lingui/cli
|
|
6
6
|
|
|
7
|
-
>
|
|
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 [
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
4
|
-
export declare function extractFromFiles(paths: string[],
|
|
3
|
+
import { ExtractWorkerPool } from "../workerPools.js";
|
|
4
|
+
export declare function extractFromFiles(paths: string[], config: LinguiConfigNormalized): Promise<ExtractedCatalogType | undefined>;
|
|
5
5
|
export declare function mergeExtractedMessage(next: ExtractedMessage, messages: ExtractedCatalogType, config: LinguiConfigNormalized): void;
|
|
6
|
-
export declare function extractFromFilesWithWorkerPool(workerPool: ExtractWorkerPool, paths: string[],
|
|
6
|
+
export declare function extractFromFilesWithWorkerPool(workerPool: ExtractWorkerPool, paths: string[], config: LinguiConfigNormalized): Promise<ExtractedCatalogType | undefined>;
|
|
@@ -33,13 +33,18 @@ function mergePlaceholders(prev, next) {
|
|
|
33
33
|
});
|
|
34
34
|
return res;
|
|
35
35
|
}
|
|
36
|
-
export async function extractFromFiles(paths,
|
|
36
|
+
export async function extractFromFiles(paths, config) {
|
|
37
|
+
const messages = {};
|
|
37
38
|
let catalogSuccess = true;
|
|
38
39
|
for (const filename of paths) {
|
|
39
|
-
const fileSuccess = await extract(filename,
|
|
40
|
+
const fileSuccess = await extract(filename, (next) => {
|
|
41
|
+
mergeExtractedMessage(next, messages, config);
|
|
42
|
+
}, config);
|
|
40
43
|
catalogSuccess &&= fileSuccess;
|
|
41
44
|
}
|
|
42
|
-
|
|
45
|
+
if (!catalogSuccess)
|
|
46
|
+
return undefined;
|
|
47
|
+
return messages;
|
|
43
48
|
}
|
|
44
49
|
export function mergeExtractedMessage(next, messages, config) {
|
|
45
50
|
if (!messages[next.id]) {
|
|
@@ -74,12 +79,13 @@ export function mergeExtractedMessage(next, messages, config) {
|
|
|
74
79
|
placeholders: mergePlaceholders(prev.placeholders, next.placeholders),
|
|
75
80
|
};
|
|
76
81
|
}
|
|
77
|
-
export async function extractFromFilesWithWorkerPool(workerPool, paths,
|
|
82
|
+
export async function extractFromFilesWithWorkerPool(workerPool, paths, config) {
|
|
83
|
+
const messages = {};
|
|
84
|
+
let catalogSuccess = true;
|
|
78
85
|
const resolvedConfigPath = config.resolvedConfigPath;
|
|
79
86
|
if (!resolvedConfigPath) {
|
|
80
87
|
throw new Error("Multithreading is only supported when lingui config loaded from file system, not passed by API");
|
|
81
88
|
}
|
|
82
|
-
let catalogSuccess = true;
|
|
83
89
|
const results = await Promise.all(paths.map((filename) => workerPool.run(filename, resolvedConfigPath)));
|
|
84
90
|
results.forEach((result) => {
|
|
85
91
|
if (!result.success) {
|
|
@@ -87,9 +93,11 @@ export async function extractFromFilesWithWorkerPool(workerPool, paths, onMessag
|
|
|
87
93
|
}
|
|
88
94
|
else {
|
|
89
95
|
result.messages.forEach((message) => {
|
|
90
|
-
|
|
96
|
+
mergeExtractedMessage(message, messages, config);
|
|
91
97
|
});
|
|
92
98
|
}
|
|
93
99
|
});
|
|
94
|
-
|
|
100
|
+
if (!catalogSuccess)
|
|
101
|
+
return undefined;
|
|
102
|
+
return messages;
|
|
95
103
|
}
|
|
@@ -1,16 +1,25 @@
|
|
|
1
|
-
import { Catalog } from "../catalog.js";
|
|
2
1
|
import { FallbackLocales } from "@lingui/conf";
|
|
2
|
+
import type { AllCatalogsType, CatalogType } from "../types.js";
|
|
3
3
|
export type TranslationMissingEvent = {
|
|
4
4
|
source: string;
|
|
5
5
|
id: string;
|
|
6
6
|
};
|
|
7
|
+
export type MissingBehavior = "resolved" | "catalog";
|
|
8
|
+
export declare function isMissingBehavior(value: string): value is MissingBehavior;
|
|
7
9
|
export type GetTranslationsOptions = {
|
|
8
10
|
sourceLocale: string;
|
|
9
11
|
fallbackLocales: FallbackLocales;
|
|
12
|
+
missingBehavior?: MissingBehavior;
|
|
13
|
+
ignoreObsolete?: boolean;
|
|
10
14
|
};
|
|
11
|
-
|
|
15
|
+
type CatalogTranslationsReader = {
|
|
16
|
+
readAll(locales: string[]): Promise<AllCatalogsType>;
|
|
17
|
+
readTemplate(): Promise<CatalogType | undefined>;
|
|
18
|
+
};
|
|
19
|
+
export declare function getTranslationsForCatalog(catalog: CatalogTranslationsReader, locale: string, options: GetTranslationsOptions): Promise<{
|
|
12
20
|
missing: TranslationMissingEvent[];
|
|
13
21
|
messages: {
|
|
14
22
|
[id: string]: string;
|
|
15
23
|
};
|
|
16
24
|
}>;
|
|
25
|
+
export {};
|
|
@@ -1,21 +1,25 @@
|
|
|
1
1
|
import { getFallbackListForLocale } from "./getFallbackListForLocale.js";
|
|
2
|
+
export function isMissingBehavior(value) {
|
|
3
|
+
return value === "resolved" || value === "catalog";
|
|
4
|
+
}
|
|
2
5
|
export async function getTranslationsForCatalog(catalog, locale, options) {
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
...getFallbackListForLocale(options.fallbackLocales, locale),
|
|
7
|
-
]);
|
|
8
|
-
const [catalogs, template] = await Promise.all([
|
|
6
|
+
const fallbackList = getFallbackListForLocale(options.fallbackLocales, locale);
|
|
7
|
+
const locales = new Set([locale, options.sourceLocale, ...fallbackList]);
|
|
8
|
+
const [rawCatalogs, rawTemplate] = await Promise.all([
|
|
9
9
|
catalog.readAll(Array.from(locales)),
|
|
10
10
|
catalog.readTemplate(),
|
|
11
11
|
]);
|
|
12
|
+
const ignoreObsolete = options.ignoreObsolete ?? false;
|
|
13
|
+
const catalogs = withoutObsolete(rawCatalogs, ignoreObsolete);
|
|
14
|
+
const template = withoutObsoleteCatalog(rawTemplate, ignoreObsolete);
|
|
12
15
|
const sourceLocaleCatalog = catalogs[options.sourceLocale] || {};
|
|
13
16
|
const input = { ...template, ...sourceLocaleCatalog, ...catalogs[locale] };
|
|
14
17
|
const missing = [];
|
|
15
|
-
const
|
|
16
|
-
|
|
18
|
+
const missingBehavior = options.missingBehavior ?? "resolved";
|
|
19
|
+
const messages = Object.entries(input).reduce((acc, [key, msg]) => {
|
|
20
|
+
acc[key] = getTranslation(catalogs, msg, locale, key, options.sourceLocale, fallbackList, ignoreObsolete, missingBehavior, (event) => {
|
|
17
21
|
missing.push(event);
|
|
18
|
-
}
|
|
22
|
+
});
|
|
19
23
|
return acc;
|
|
20
24
|
}, {});
|
|
21
25
|
return {
|
|
@@ -23,25 +27,47 @@ export async function getTranslationsForCatalog(catalog, locale, options) {
|
|
|
23
27
|
messages,
|
|
24
28
|
};
|
|
25
29
|
}
|
|
26
|
-
function
|
|
27
|
-
|
|
30
|
+
function isActiveMessage(message, ignoreObsolete) {
|
|
31
|
+
return Boolean(message && (!ignoreObsolete || !message.obsolete));
|
|
32
|
+
}
|
|
33
|
+
function withoutObsolete(catalogs, ignoreObsolete) {
|
|
34
|
+
return Object.fromEntries(Object.entries(catalogs).map(([locale, catalog]) => [
|
|
35
|
+
locale,
|
|
36
|
+
withoutObsoleteCatalog(catalog, ignoreObsolete),
|
|
37
|
+
]));
|
|
38
|
+
}
|
|
39
|
+
function withoutObsoleteCatalog(catalog, ignoreObsolete) {
|
|
40
|
+
const activeCatalog = {};
|
|
41
|
+
Object.entries(catalog ?? {}).forEach(([id, message]) => {
|
|
42
|
+
if (isActiveMessage(message, ignoreObsolete)) {
|
|
43
|
+
activeCatalog[id] = message;
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
return activeCatalog;
|
|
47
|
+
}
|
|
48
|
+
function sourceLocaleFallback(catalog, key, ignoreObsolete) {
|
|
49
|
+
const message = catalog?.[key];
|
|
50
|
+
if (!isActiveMessage(message, ignoreObsolete)) {
|
|
28
51
|
return undefined;
|
|
29
52
|
}
|
|
30
|
-
return
|
|
53
|
+
return message.translation || message.message;
|
|
31
54
|
}
|
|
32
|
-
function getTranslation(catalogs, msg, locale, key,
|
|
33
|
-
const
|
|
34
|
-
const getTranslation = (_locale) => {
|
|
55
|
+
function getTranslation(catalogs, msg, locale, key, sourceLocale, fallbackList, ignoreObsolete, missingBehavior, onMissing) {
|
|
56
|
+
const getCatalogTranslation = (_locale) => {
|
|
35
57
|
const localeCatalog = catalogs[_locale];
|
|
36
|
-
|
|
58
|
+
const message = localeCatalog?.[key];
|
|
59
|
+
if (!isActiveMessage(message, ignoreObsolete)) {
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
return message.translation;
|
|
37
63
|
};
|
|
38
|
-
const getMultipleFallbacks = (
|
|
39
|
-
|
|
40
|
-
if (!fL.length)
|
|
64
|
+
const getMultipleFallbacks = () => {
|
|
65
|
+
if (!fallbackList.length)
|
|
41
66
|
return null;
|
|
42
|
-
for (const fallbackLocale of
|
|
43
|
-
|
|
44
|
-
|
|
67
|
+
for (const fallbackLocale of fallbackList) {
|
|
68
|
+
const fallbackTranslation = getCatalogTranslation(fallbackLocale);
|
|
69
|
+
if (catalogs[fallbackLocale] && fallbackTranslation) {
|
|
70
|
+
return fallbackTranslation;
|
|
45
71
|
}
|
|
46
72
|
}
|
|
47
73
|
};
|
|
@@ -51,22 +77,29 @@ function getTranslation(catalogs, msg, locale, key, onMissing, options) {
|
|
|
51
77
|
// -> template message
|
|
52
78
|
// ** last resort **
|
|
53
79
|
// -> id
|
|
80
|
+
const catalogTranslation = getCatalogTranslation(locale);
|
|
54
81
|
const translation =
|
|
55
82
|
// Get translation in target locale
|
|
56
|
-
|
|
83
|
+
catalogTranslation ||
|
|
57
84
|
// We search in fallbackLocales as dependent of each locale
|
|
58
|
-
getMultipleFallbacks(
|
|
85
|
+
getMultipleFallbacks() ||
|
|
59
86
|
(sourceLocale &&
|
|
60
87
|
sourceLocale === locale &&
|
|
61
|
-
sourceLocaleFallback(catalogs[sourceLocale], key));
|
|
62
|
-
|
|
88
|
+
sourceLocaleFallback(catalogs[sourceLocale], key, ignoreObsolete));
|
|
89
|
+
const isMissingTranslation = missingBehavior === "catalog"
|
|
90
|
+
? locale !== sourceLocale && !catalogTranslation
|
|
91
|
+
: !translation;
|
|
92
|
+
if (isMissingTranslation) {
|
|
63
93
|
onMissing({
|
|
64
94
|
id: key,
|
|
65
|
-
source: msg.message ||
|
|
95
|
+
source: msg.message ||
|
|
96
|
+
sourceLocaleFallback(catalogs[sourceLocale], key, ignoreObsolete) ||
|
|
97
|
+
"",
|
|
66
98
|
});
|
|
67
99
|
}
|
|
68
100
|
return (translation ||
|
|
69
|
-
(sourceLocale &&
|
|
101
|
+
(sourceLocale &&
|
|
102
|
+
sourceLocaleFallback(catalogs[sourceLocale], key, ignoreObsolete)) ||
|
|
70
103
|
// take from template
|
|
71
104
|
msg.message ||
|
|
72
105
|
key);
|
|
@@ -20,11 +20,13 @@ export function mergeCatalog(prevCatalog, nextCatalog, forSourceLocale, options)
|
|
|
20
20
|
obsoleteKeys = prevKeys;
|
|
21
21
|
}
|
|
22
22
|
// Initialize new catalog with new keys
|
|
23
|
+
// `translation` is added last, so a message keeps the same key order once it
|
|
24
|
+
// is merged from the previous catalog on the next extract
|
|
23
25
|
const newMessages = Object.fromEntries(newKeys.map((key) => [
|
|
24
26
|
key,
|
|
25
27
|
{
|
|
26
|
-
translation: forSourceLocale ? nextCatalog[key].message || key : "",
|
|
27
28
|
...nextCatalog[key],
|
|
29
|
+
translation: forSourceLocale ? nextCatalog[key].message || key : "",
|
|
28
30
|
},
|
|
29
31
|
]));
|
|
30
32
|
// Merge translations from previous catalog
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Catalog } from "../catalog.js";
|
|
2
|
+
import type { CheckFindingBase } from "../check/types.js";
|
|
3
|
+
import { TranslationMissingEvent } from "./getTranslationsForCatalog.js";
|
|
4
|
+
import type { MissingBehavior } from "./getTranslationsForCatalog.js";
|
|
5
|
+
export type MissingTranslationFinding = CheckFindingBase & {
|
|
6
|
+
code: "missing_translation";
|
|
7
|
+
locale: string;
|
|
8
|
+
};
|
|
9
|
+
export declare function getMissingTranslationFindings(catalog: Catalog, locale: string, missingBehavior?: MissingBehavior): Promise<MissingTranslationFinding[]>;
|
|
10
|
+
export declare function getCatalogTranslationsWithMissing(catalog: Catalog, locale: string, missingBehavior?: MissingBehavior): Promise<{
|
|
11
|
+
messages: {
|
|
12
|
+
[id: string]: string;
|
|
13
|
+
};
|
|
14
|
+
missing: TranslationMissingEvent[];
|
|
15
|
+
}>;
|
|
16
|
+
export declare function createMissingTranslationFinding(catalog: Catalog, locale: string, missing: TranslationMissingEvent): MissingTranslationFinding;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { toRootRelativePath } from "../utils.js";
|
|
2
|
+
import { getTranslationsForCatalog, } from "./getTranslationsForCatalog.js";
|
|
3
|
+
export async function getMissingTranslationFindings(catalog, locale, missingBehavior = "resolved") {
|
|
4
|
+
if (catalog.config.pseudoLocale.some((item) => item.locale === locale)) {
|
|
5
|
+
return [];
|
|
6
|
+
}
|
|
7
|
+
const { missing } = await getCatalogTranslationsWithMissing(catalog, locale, missingBehavior);
|
|
8
|
+
return missing.map((entry) => createMissingTranslationFinding(catalog, locale, entry));
|
|
9
|
+
}
|
|
10
|
+
function createMissingTranslationMessage(messageId, source) {
|
|
11
|
+
return source || source === messageId
|
|
12
|
+
? `${messageId}: (${source})`
|
|
13
|
+
: messageId;
|
|
14
|
+
}
|
|
15
|
+
export async function getCatalogTranslationsWithMissing(catalog, locale, missingBehavior = "resolved") {
|
|
16
|
+
const { messages, missing } = await getTranslationsForCatalog(catalog, locale, {
|
|
17
|
+
fallbackLocales: catalog.config.fallbackLocales,
|
|
18
|
+
sourceLocale: catalog.config.sourceLocale,
|
|
19
|
+
missingBehavior,
|
|
20
|
+
ignoreObsolete: true,
|
|
21
|
+
});
|
|
22
|
+
return {
|
|
23
|
+
messages,
|
|
24
|
+
missing,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export function createMissingTranslationFinding(catalog, locale, missing) {
|
|
28
|
+
const catalogPath = toRootRelativePath(catalog.config.rootDir, catalog.getFilename(locale));
|
|
29
|
+
return {
|
|
30
|
+
code: "missing_translation",
|
|
31
|
+
locale,
|
|
32
|
+
catalogPath,
|
|
33
|
+
message: createMissingTranslationMessage(missing.id, missing.source),
|
|
34
|
+
};
|
|
35
|
+
}
|
package/dist/api/catalog.d.ts
CHANGED
|
@@ -54,7 +54,6 @@ export declare class Catalog {
|
|
|
54
54
|
files?: string[];
|
|
55
55
|
workerPool?: ExtractWorkerPool;
|
|
56
56
|
}): Promise<ExtractedCatalogType | undefined>;
|
|
57
|
-
private collectWithExtractors;
|
|
58
57
|
merge(prevCatalogs: AllCatalogsType, nextCatalog: ExtractedCatalogType, options: MergeOptions): {
|
|
59
58
|
[k: string]: CatalogType;
|
|
60
59
|
};
|
package/dist/api/catalog.js
CHANGED
|
@@ -4,9 +4,8 @@ import { globSync } from "node:fs";
|
|
|
4
4
|
import normalize from "normalize-path";
|
|
5
5
|
import { getTranslationsForCatalog, } from "./catalog/getTranslationsForCatalog.js";
|
|
6
6
|
import { mergeCatalog } from "./catalog/mergeCatalog.js";
|
|
7
|
-
import { extractFromFiles, extractFromFilesWithWorkerPool,
|
|
7
|
+
import { extractFromFiles, extractFromFilesWithWorkerPool, } from "./catalog/extractFromFiles.js";
|
|
8
8
|
import { isDirectory, makePathRegexSafe, normalizeRelativePath, replacePlaceholders, writeFile, } from "./utils.js";
|
|
9
|
-
import { getConfiguredExtractors, isBatchExtractor, } from "./extractors/index.js";
|
|
10
9
|
const LOCALE = "{locale}";
|
|
11
10
|
const LOCALE_SUFFIX_RE = /\{locale\}.*$/;
|
|
12
11
|
export class Catalog {
|
|
@@ -83,63 +82,10 @@ export class Catalog {
|
|
|
83
82
|
const regex = new RegExp(options.files.join("|"), "i");
|
|
84
83
|
paths = paths.filter((path) => regex.test(normalize(path)));
|
|
85
84
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
mergeExtractedMessage(next, messages, this.config);
|
|
89
|
-
};
|
|
90
|
-
const extractors = getConfiguredExtractors(this.config);
|
|
91
|
-
// Optimized hot path: if there are no batch extractors defined, skip the more complex logic.
|
|
92
|
-
if (!extractors.some(isBatchExtractor)) {
|
|
93
|
-
const success = options.workerPool
|
|
94
|
-
? await extractFromFilesWithWorkerPool(options.workerPool, paths, onMessageExtracted, this.config)
|
|
95
|
-
: await extractFromFiles(paths, onMessageExtracted, this.config);
|
|
96
|
-
return success ? messages : undefined;
|
|
97
|
-
}
|
|
98
|
-
return await this.collectWithExtractors(extractors, paths, onMessageExtracted, messages, options);
|
|
99
|
-
}
|
|
100
|
-
async collectWithExtractors(extractors, paths, onMessageExtracted, messages, options) {
|
|
101
|
-
let remaining = paths;
|
|
102
|
-
let catalogSuccess = true;
|
|
103
|
-
for (const extractor of extractors) {
|
|
104
|
-
if (remaining.length === 0)
|
|
105
|
-
break;
|
|
106
|
-
const matched = [];
|
|
107
|
-
const unmatched = [];
|
|
108
|
-
for (const f of remaining) {
|
|
109
|
-
if (extractor.match(f)) {
|
|
110
|
-
matched.push(f);
|
|
111
|
-
}
|
|
112
|
-
else {
|
|
113
|
-
unmatched.push(f);
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
if (matched.length === 0)
|
|
117
|
-
continue;
|
|
118
|
-
remaining = unmatched;
|
|
119
|
-
if (isBatchExtractor(extractor)) {
|
|
120
|
-
try {
|
|
121
|
-
await extractor.extractFromFiles(matched, onMessageExtracted, {
|
|
122
|
-
linguiConfig: this.config,
|
|
123
|
-
});
|
|
124
|
-
}
|
|
125
|
-
catch (e) {
|
|
126
|
-
console.error(`Extractor failed: ${e.message}`);
|
|
127
|
-
console.error(e.stack);
|
|
128
|
-
catalogSuccess = false;
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
else {
|
|
132
|
-
const success = options.workerPool
|
|
133
|
-
? await extractFromFilesWithWorkerPool(options.workerPool, matched, onMessageExtracted, this.config)
|
|
134
|
-
: await extractFromFiles(matched, onMessageExtracted, this.config);
|
|
135
|
-
if (!success) {
|
|
136
|
-
catalogSuccess = false;
|
|
137
|
-
}
|
|
138
|
-
}
|
|
85
|
+
if (options.workerPool) {
|
|
86
|
+
return await extractFromFilesWithWorkerPool(options.workerPool, paths, this.config);
|
|
139
87
|
}
|
|
140
|
-
|
|
141
|
-
return undefined;
|
|
142
|
-
return messages;
|
|
88
|
+
return await extractFromFiles(paths, this.config);
|
|
143
89
|
}
|
|
144
90
|
/*
|
|
145
91
|
*
|
|
@@ -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
|
|
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,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
|
+
};
|