@lingui/cli 6.4.0 → 6.6.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/dist/api/catalog/getCatalogs.js +9 -1
- package/dist/api/catalog.d.ts +8 -1
- package/dist/api/catalog.js +41 -10
- package/dist/api/compile/compileLocale.js +3 -2
- package/dist/api/compile.d.ts +3 -1
- package/dist/api/compile.js +4 -4
- package/dist/api/getPathsForExtractWatcher.js +3 -2
- package/dist/api/pseudoLocalize.d.ts +2 -1
- package/dist/api/pseudoLocalize.js +3 -2
- package/dist/api/stats.js +1 -1
- package/dist/extract-experimental/buildChunkGraph.d.ts +10 -0
- package/dist/extract-experimental/buildChunkGraph.js +37 -0
- package/dist/extract-experimental/buildContentFilter.d.ts +2 -0
- package/dist/extract-experimental/buildContentFilter.js +12 -0
- package/dist/extract-experimental/bundlers/esbuild.d.ts +29 -0
- package/dist/extract-experimental/bundlers/esbuild.js +134 -0
- package/dist/extract-experimental/bundlers/rolldown.d.ts +29 -0
- package/dist/extract-experimental/bundlers/rolldown.js +103 -0
- package/dist/extract-experimental/constants.d.ts +1 -0
- package/dist/extract-experimental/constants.js +21 -0
- package/dist/extract-experimental/extractFromChunk.d.ts +5 -0
- package/dist/extract-experimental/extractFromChunk.js +8 -0
- package/dist/extract-experimental/workers/extractWorker.d.ts +4 -5
- package/dist/extract-experimental/workers/extractWorker.js +4 -20
- package/dist/extract-experimental/workers/extractWorkerWrapper.prod.d.ts +2 -4
- package/dist/lingui-extract-experimental.js +111 -31
- package/dist/services/translationIO.js +1 -1
- package/package.json +26 -10
- package/dist/extract-experimental/bundleSource.d.ts +0 -3
- package/dist/extract-experimental/bundleSource.js +0 -75
- package/dist/extract-experimental/extractFromBundleAndWrite.d.ts +0 -18
- package/dist/extract-experimental/extractFromBundleAndWrite.js +0 -50
- package/dist/extract-experimental/linguiEsbuildPlugin.d.ts +0 -5
- package/dist/extract-experimental/linguiEsbuildPlugin.js +0 -38
|
@@ -43,13 +43,18 @@ export async function getCatalogs(config) {
|
|
|
43
43
|
});
|
|
44
44
|
candidates.forEach((catalogDir) => {
|
|
45
45
|
const name = path.basename(catalogDir);
|
|
46
|
+
const globName = escapeCatalogNameForGlob(name);
|
|
46
47
|
catalogs.push(new Catalog({
|
|
47
48
|
name,
|
|
48
49
|
path: normalizeRelativePath(replacePlaceholders(catalog.path, { name })),
|
|
49
50
|
include: include.map((path) => replacePlaceholders(path, { name })),
|
|
50
51
|
exclude: exclude.map((path) => replacePlaceholders(path, { name })),
|
|
51
52
|
format,
|
|
52
|
-
}, config
|
|
53
|
+
}, config, {
|
|
54
|
+
path: replacePlaceholders(catalog.path, { name: globName }),
|
|
55
|
+
include: include.map((path) => replacePlaceholders(path, { name: globName })),
|
|
56
|
+
exclude: exclude.map((path) => replacePlaceholders(path, { name: globName })),
|
|
57
|
+
}));
|
|
53
58
|
});
|
|
54
59
|
});
|
|
55
60
|
if (config.experimental?.extractor &&
|
|
@@ -58,6 +63,9 @@ export async function getCatalogs(config) {
|
|
|
58
63
|
}
|
|
59
64
|
return catalogs;
|
|
60
65
|
}
|
|
66
|
+
function escapeCatalogNameForGlob(value) {
|
|
67
|
+
return value.replace(/[[\]()]/g, "[$&]");
|
|
68
|
+
}
|
|
61
69
|
/**
|
|
62
70
|
* Ensure that value is always array. If not, turn it into an array of one element.
|
|
63
71
|
*/
|
package/dist/api/catalog.d.ts
CHANGED
|
@@ -29,7 +29,13 @@ export type CatalogProps = {
|
|
|
29
29
|
templatePath?: string;
|
|
30
30
|
format: FormatterWrapper;
|
|
31
31
|
};
|
|
32
|
+
type CatalogGlobPatterns = {
|
|
33
|
+
path: string;
|
|
34
|
+
include: Array<string>;
|
|
35
|
+
exclude: Array<string>;
|
|
36
|
+
};
|
|
32
37
|
export declare class Catalog {
|
|
38
|
+
#private;
|
|
33
39
|
config: LinguiConfigNormalized;
|
|
34
40
|
name?: string;
|
|
35
41
|
path: string;
|
|
@@ -37,7 +43,7 @@ export declare class Catalog {
|
|
|
37
43
|
exclude: Array<string>;
|
|
38
44
|
format: FormatterWrapper;
|
|
39
45
|
templateFile: string;
|
|
40
|
-
constructor({ name, path, include, templatePath, format, exclude }: CatalogProps, config: LinguiConfigNormalized);
|
|
46
|
+
constructor({ name, path, include, templatePath, format, exclude }: CatalogProps, config: LinguiConfigNormalized, globPatterns?: CatalogGlobPatterns);
|
|
41
47
|
getFilename(locale: string): string;
|
|
42
48
|
make(options: MakeOptions): Promise<AllCatalogsType | false>;
|
|
43
49
|
makeTemplate(options: MakeTemplateOptions): Promise<CatalogType | false>;
|
|
@@ -71,3 +77,4 @@ export declare function cleanObsolete<T extends CatalogType>(messages: T): T;
|
|
|
71
77
|
export declare function order<T extends CatalogType>(by: OrderBy, catalog: T): T;
|
|
72
78
|
export declare function writeCompiled(path: string, locale: string, compiledCatalog: string, namespace?: CompiledCatalogNamespace): Promise<string>;
|
|
73
79
|
export declare const orderByMessage: OrderByFn;
|
|
80
|
+
export {};
|
package/dist/api/catalog.js
CHANGED
|
@@ -17,12 +17,19 @@ export class Catalog {
|
|
|
17
17
|
exclude;
|
|
18
18
|
format;
|
|
19
19
|
templateFile;
|
|
20
|
-
|
|
20
|
+
#includePatterns;
|
|
21
|
+
#excludePatterns;
|
|
22
|
+
constructor({ name, path, include, templatePath, format, exclude = [] }, config, globPatterns = { path, include, exclude }) {
|
|
21
23
|
this.config = config;
|
|
22
24
|
this.name = name;
|
|
23
25
|
this.path = normalizeRelativePath(path);
|
|
24
26
|
this.include = include.map(normalizeRelativePath);
|
|
25
27
|
this.exclude = [this.localeDir, ...exclude.map(normalizeRelativePath)];
|
|
28
|
+
this.#includePatterns = createCatalogPatterns(this.include, globPatterns.include.map(normalizeRelativePath));
|
|
29
|
+
this.#excludePatterns = createCatalogPatterns(this.exclude, [
|
|
30
|
+
getLocaleDir(normalizeRelativePath(globPatterns.path)),
|
|
31
|
+
...globPatterns.exclude.map(normalizeRelativePath),
|
|
32
|
+
]);
|
|
26
33
|
this.format = format;
|
|
27
34
|
this.templateFile =
|
|
28
35
|
templatePath ||
|
|
@@ -194,30 +201,54 @@ export class Catalog {
|
|
|
194
201
|
async readTemplate() {
|
|
195
202
|
return await this.format.read(this.templateFile, undefined);
|
|
196
203
|
}
|
|
204
|
+
/** @internal */
|
|
205
|
+
get sourcePatterns() {
|
|
206
|
+
return {
|
|
207
|
+
include: resolveCatalogPatterns(this.include, this.#includePatterns),
|
|
208
|
+
exclude: resolveCatalogPatterns(this.exclude, this.#excludePatterns),
|
|
209
|
+
};
|
|
210
|
+
}
|
|
197
211
|
get sourcePaths() {
|
|
198
|
-
const
|
|
212
|
+
const sourcePatterns = this.sourcePatterns;
|
|
213
|
+
const includeGlobs = this.include.map((includePath, index) => {
|
|
214
|
+
const includeGlob = sourcePatterns.include[index];
|
|
199
215
|
const isDir = isDirectory(includePath);
|
|
200
216
|
/**
|
|
201
217
|
* glob library results from absolute patterns such as /foo/* are mounted onto the root setting using path.join.
|
|
202
218
|
* On windows, this will by default result in /foo/* matching C:\foo\bar.txt.
|
|
203
219
|
*/
|
|
204
220
|
return isDir
|
|
205
|
-
? normalize(path.resolve(process.cwd(), includePath === "/" ? "" :
|
|
206
|
-
:
|
|
221
|
+
? normalize(path.resolve(process.cwd(), includePath === "/" ? "" : includeGlob, "**/*.*"))
|
|
222
|
+
: includeGlob;
|
|
207
223
|
});
|
|
208
|
-
return globSync(includeGlobs, { exclude:
|
|
224
|
+
return globSync(includeGlobs, { exclude: sourcePatterns.exclude });
|
|
209
225
|
}
|
|
210
226
|
get localeDir() {
|
|
211
|
-
|
|
212
|
-
if (localePatternIndex === -1) {
|
|
213
|
-
throw Error(`Invalid catalog path: ${LOCALE} variable is missing`);
|
|
214
|
-
}
|
|
215
|
-
return this.path.substring(0, localePatternIndex);
|
|
227
|
+
return getLocaleDir(this.path);
|
|
216
228
|
}
|
|
217
229
|
get locales() {
|
|
218
230
|
return this.config.locales;
|
|
219
231
|
}
|
|
220
232
|
}
|
|
233
|
+
function createCatalogPatterns(values, globs) {
|
|
234
|
+
return values.map((value, index) => ({
|
|
235
|
+
value,
|
|
236
|
+
glob: globs[index] ?? value,
|
|
237
|
+
}));
|
|
238
|
+
}
|
|
239
|
+
function resolveCatalogPatterns(values, patterns) {
|
|
240
|
+
return values.map((value, index) => {
|
|
241
|
+
const pattern = patterns[index];
|
|
242
|
+
return pattern?.value === value ? pattern.glob : value;
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
function getLocaleDir(catalogPath) {
|
|
246
|
+
const localePatternIndex = catalogPath.indexOf(LOCALE);
|
|
247
|
+
if (localePatternIndex === -1) {
|
|
248
|
+
throw Error(`Invalid catalog path: ${LOCALE} variable is missing`);
|
|
249
|
+
}
|
|
250
|
+
return catalogPath.substring(0, localePatternIndex);
|
|
251
|
+
}
|
|
221
252
|
function getTemplatePath(ext, path) {
|
|
222
253
|
return path.replace(LOCALE_SUFFIX_RE, "messages" + ext);
|
|
223
254
|
}
|
|
@@ -15,7 +15,7 @@ export async function compileLocale(catalogs, locale, options, config, doMerge,
|
|
|
15
15
|
sourceLocale: config.sourceLocale,
|
|
16
16
|
});
|
|
17
17
|
if (!options.allowEmpty &&
|
|
18
|
-
locale !== config.pseudoLocale &&
|
|
18
|
+
locale !== config.pseudoLocale.locale &&
|
|
19
19
|
missingMessages.length > 0) {
|
|
20
20
|
logger.error(styleText("red", `Error: Failed to compile catalog for locale ${styleText("bold", locale)}!`));
|
|
21
21
|
if (options.verbose) {
|
|
@@ -57,7 +57,8 @@ async function compileAndWrite(locale, config, options, writePath, messages, log
|
|
|
57
57
|
strict: false,
|
|
58
58
|
namespace,
|
|
59
59
|
outputPrefix: options.outputPrefix,
|
|
60
|
-
pseudoLocale: config.pseudoLocale,
|
|
60
|
+
pseudoLocale: config.pseudoLocale.locale,
|
|
61
|
+
pseudoLocaleOptions: config.pseudoLocale.options,
|
|
61
62
|
compilerBabelOptions: config.compilerBabelOptions,
|
|
62
63
|
});
|
|
63
64
|
if (errors.length) {
|
package/dist/api/compile.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { GeneratorOptions } from "@babel/generator";
|
|
2
2
|
import { CompiledMessage } from "@lingui/message-utils/compileMessage";
|
|
3
|
+
import type { PseudoLocaleOptions } from "@lingui/conf";
|
|
3
4
|
export type CompiledCatalogNamespace = "cjs" | "es" | "ts" | "json" | string;
|
|
4
5
|
type CompiledCatalogType = {
|
|
5
6
|
[msgId: string]: string;
|
|
@@ -8,6 +9,7 @@ export type CreateCompileCatalogOptions = {
|
|
|
8
9
|
strict?: boolean;
|
|
9
10
|
namespace?: CompiledCatalogNamespace;
|
|
10
11
|
pseudoLocale?: string;
|
|
12
|
+
pseudoLocaleOptions?: PseudoLocaleOptions;
|
|
11
13
|
compilerBabelOptions?: GeneratorOptions;
|
|
12
14
|
outputPrefix?: string;
|
|
13
15
|
};
|
|
@@ -33,5 +35,5 @@ export declare function createCompiledCatalog(locale: string, messages: Compiled
|
|
|
33
35
|
* Compile string message into AST tree. Message format is parsed/compiled into
|
|
34
36
|
* JS arrays, which are handled in client.
|
|
35
37
|
*/
|
|
36
|
-
export declare function compile(message: string, shouldPseudolocalize?: boolean): CompiledMessage;
|
|
38
|
+
export declare function compile(message: string, shouldPseudolocalize?: boolean, pseudoLocaleOptions?: PseudoLocaleOptions): CompiledMessage;
|
|
37
39
|
export {};
|
package/dist/api/compile.js
CHANGED
|
@@ -3,7 +3,7 @@ import { generate } from "@babel/generator";
|
|
|
3
3
|
import { compileMessageOrThrow, } from "@lingui/message-utils/compileMessage";
|
|
4
4
|
import pseudoLocalize from "./pseudoLocalize.js";
|
|
5
5
|
export function createCompiledCatalog(locale, messages, options) {
|
|
6
|
-
const { strict = false, namespace = "cjs", pseudoLocale, compilerBabelOptions = {}, outputPrefix = "/*eslint-disable*/", } = options;
|
|
6
|
+
const { strict = false, namespace = "cjs", pseudoLocale, pseudoLocaleOptions, compilerBabelOptions = {}, outputPrefix = "/*eslint-disable*/", } = options;
|
|
7
7
|
const shouldPseudolocalize = locale === pseudoLocale;
|
|
8
8
|
const errors = [];
|
|
9
9
|
const compiledMessages = Object.keys(messages)
|
|
@@ -12,7 +12,7 @@ export function createCompiledCatalog(locale, messages, options) {
|
|
|
12
12
|
// Don't use `key` as a fallback translation in strict mode.
|
|
13
13
|
const translation = (messages[key] || (!strict ? key : ""));
|
|
14
14
|
try {
|
|
15
|
-
obj[key] = compile(translation, shouldPseudolocalize);
|
|
15
|
+
obj[key] = compile(translation, shouldPseudolocalize, pseudoLocaleOptions);
|
|
16
16
|
}
|
|
17
17
|
catch (e) {
|
|
18
18
|
errors.push({
|
|
@@ -80,6 +80,6 @@ function buildExportStatement(expression, namespace) {
|
|
|
80
80
|
* Compile string message into AST tree. Message format is parsed/compiled into
|
|
81
81
|
* JS arrays, which are handled in client.
|
|
82
82
|
*/
|
|
83
|
-
export function compile(message, shouldPseudolocalize = false) {
|
|
84
|
-
return compileMessageOrThrow(message, (value) => shouldPseudolocalize ? pseudoLocalize(value) : value);
|
|
83
|
+
export function compile(message, shouldPseudolocalize = false, pseudoLocaleOptions) {
|
|
84
|
+
return compileMessageOrThrow(message, (value) => shouldPseudolocalize ? pseudoLocalize(value, pseudoLocaleOptions) : value);
|
|
85
85
|
}
|
|
@@ -7,8 +7,9 @@ export async function getPathsForExtractWatcher(config) {
|
|
|
7
7
|
const paths = [];
|
|
8
8
|
const ignored = [];
|
|
9
9
|
catalogs.forEach((catalog) => {
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
const sourcePatterns = catalog.sourcePatterns;
|
|
11
|
+
paths.push(...sourcePatterns.include);
|
|
12
|
+
ignored.push(...sourcePatterns.exclude);
|
|
12
13
|
});
|
|
13
14
|
return { paths, ignored };
|
|
14
15
|
}
|
|
@@ -1 +1,2 @@
|
|
|
1
|
-
|
|
1
|
+
import type { PseudoLocaleOptions } from "@lingui/conf";
|
|
2
|
+
export default function (message: string, options?: PseudoLocaleOptions): string;
|
|
@@ -37,14 +37,15 @@ function addDelimitersVariables(message) {
|
|
|
37
37
|
function removeDelimiters(message) {
|
|
38
38
|
return message.replace(new RegExp(delimiter, "g"), "");
|
|
39
39
|
}
|
|
40
|
-
export default function (message) {
|
|
40
|
+
export default function (message, options = {}) {
|
|
41
41
|
message = addDelimitersHTMLTags(message);
|
|
42
42
|
message = addDelimitersMacro(message);
|
|
43
43
|
message = addDelimitersVariables(message);
|
|
44
44
|
message = pseudolocale(message, {
|
|
45
|
-
delimiter,
|
|
46
45
|
prepend: "",
|
|
47
46
|
append: "",
|
|
47
|
+
...options,
|
|
48
|
+
delimiter,
|
|
48
49
|
});
|
|
49
50
|
return removeDelimiters(message);
|
|
50
51
|
}
|
package/dist/api/stats.js
CHANGED
|
@@ -25,7 +25,7 @@ export function printStats(config, catalogs) {
|
|
|
25
25
|
return a.localeCompare(b);
|
|
26
26
|
})
|
|
27
27
|
.forEach((locale) => {
|
|
28
|
-
if (locale === config.pseudoLocale)
|
|
28
|
+
if (locale === config.pseudoLocale.locale)
|
|
29
29
|
return; // skip pseudo locale
|
|
30
30
|
const catalog = catalogs[locale];
|
|
31
31
|
// catalog is null if no catalog exists on disk and the locale
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { BundleChunk } from "@lingui/conf";
|
|
2
|
+
/**
|
|
3
|
+
* Traverse the chunk import graph to determine which entry points depend on each chunk.
|
|
4
|
+
* This lets us extract messages from shared/common chunks once and attribute them to all
|
|
5
|
+
* consuming entry catalogs.
|
|
6
|
+
*/
|
|
7
|
+
export declare function buildChunkGraph(rawChunks: BundleChunk[]): Array<{
|
|
8
|
+
filePath: string;
|
|
9
|
+
entryPoints: string[];
|
|
10
|
+
}>;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Traverse the chunk import graph to determine which entry points depend on each chunk.
|
|
3
|
+
* This lets us extract messages from shared/common chunks once and attribute them to all
|
|
4
|
+
* consuming entry catalogs.
|
|
5
|
+
*/
|
|
6
|
+
export function buildChunkGraph(rawChunks) {
|
|
7
|
+
const chunkById = new Map(rawChunks.map((c) => [c.id, c]));
|
|
8
|
+
const chunkToEntries = new Map();
|
|
9
|
+
for (const chunk of rawChunks) {
|
|
10
|
+
if (!chunk.entryPoint)
|
|
11
|
+
continue;
|
|
12
|
+
const queue = [chunk.id];
|
|
13
|
+
const visited = new Set();
|
|
14
|
+
while (queue.length > 0) {
|
|
15
|
+
const currentId = queue.pop();
|
|
16
|
+
if (visited.has(currentId))
|
|
17
|
+
continue;
|
|
18
|
+
visited.add(currentId);
|
|
19
|
+
if (!chunkToEntries.has(currentId)) {
|
|
20
|
+
chunkToEntries.set(currentId, new Set());
|
|
21
|
+
}
|
|
22
|
+
chunkToEntries.get(currentId).add(chunk.entryPoint);
|
|
23
|
+
const current = chunkById.get(currentId);
|
|
24
|
+
if (current) {
|
|
25
|
+
for (const imp of current.imports) {
|
|
26
|
+
if (chunkById.has(imp)) {
|
|
27
|
+
queue.push(imp);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return Array.from(chunkToEntries.entries()).map(([id, entries]) => ({
|
|
34
|
+
filePath: chunkById.get(id).filePath,
|
|
35
|
+
entryPoints: Array.from(entries),
|
|
36
|
+
}));
|
|
37
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export const buildContentFilterRe = (config) => {
|
|
2
|
+
const macroIds = new Set([
|
|
3
|
+
...config.macro.corePackage,
|
|
4
|
+
...config.macro.jsxPackage,
|
|
5
|
+
]);
|
|
6
|
+
// 1. Escape any special regex characters in the IDs (just in case)
|
|
7
|
+
// 2. Join them with the '|' (OR) operator
|
|
8
|
+
const macroPattern = Array.from(macroIds)
|
|
9
|
+
.map((id) => id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
|
|
10
|
+
.join("|");
|
|
11
|
+
return new RegExp(`from ['"](?:${macroPattern})['"]`);
|
|
12
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { BuildOptions } from "esbuild";
|
|
2
|
+
import type { ExperimentalExtractorBundler } from "@lingui/conf";
|
|
3
|
+
export type EsbuildBundlerOptions = {
|
|
4
|
+
/**
|
|
5
|
+
* List of package name patterns to include for extraction.
|
|
6
|
+
*
|
|
7
|
+
* For example, to include all packages from your monorepo:
|
|
8
|
+
*
|
|
9
|
+
* ["@mycompany"]
|
|
10
|
+
*
|
|
11
|
+
* By default, all imports that look like package imports are ignored.
|
|
12
|
+
* This means imports that do not start with `/`, `./`, `../`, or `#`
|
|
13
|
+
* (used for subpath imports). TypeScript path aliases are also ignored
|
|
14
|
+
* because they look like package imports.
|
|
15
|
+
*
|
|
16
|
+
* Add here the packages you want to include.
|
|
17
|
+
*/
|
|
18
|
+
includeDeps?: string[];
|
|
19
|
+
/**
|
|
20
|
+
* svg, jpg and other files which might be imported in application should be excluded from analysis.
|
|
21
|
+
* By default, extractor provides a comprehensive list of extensions. If you feel like something
|
|
22
|
+
* is missing in this list please fill an issue on GitHub
|
|
23
|
+
*
|
|
24
|
+
* NOTE: changing this param will override default list of extensions.
|
|
25
|
+
*/
|
|
26
|
+
excludeExtensions?: string[];
|
|
27
|
+
resolveEsbuildOptions?: (options: BuildOptions) => BuildOptions;
|
|
28
|
+
};
|
|
29
|
+
export declare function createEsbuildBundler(options?: EsbuildBundlerOptions): ExperimentalExtractorBundler;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import { buildIncludeDepsFilter } from "../buildIncludeDepsFilter.js";
|
|
3
|
+
import { DEFAULT_EXCLUDE_EXTENSIONS } from "../constants.js";
|
|
4
|
+
import { transformAsync } from "@babel/core";
|
|
5
|
+
import fs from "fs";
|
|
6
|
+
import { babelRe, getBabelParserOptions } from "../../api/extractors/babel.js";
|
|
7
|
+
import linguiMacroPlugin from "@lingui/babel-plugin-lingui-macro";
|
|
8
|
+
import { buildContentFilterRe } from "../buildContentFilter.js";
|
|
9
|
+
function createExtRegExp(extensions) {
|
|
10
|
+
return new RegExp("\\.(?:" + extensions.join("|") + ")(?:\\?.*)?$");
|
|
11
|
+
}
|
|
12
|
+
// esbuild metafile reports entryPoint as cwd-relative with forward slashes.
|
|
13
|
+
// Resolve to absolute and filter to only user-specified entries
|
|
14
|
+
// (dynamic imports also get entryPoint set in metafile).
|
|
15
|
+
function resolveIfUserEntry(metaEntryPoint, entryPointSet) {
|
|
16
|
+
const resolved = path.resolve(metaEntryPoint);
|
|
17
|
+
return entryPointSet.has(resolved) ? resolved : undefined;
|
|
18
|
+
}
|
|
19
|
+
export function createEsbuildBundler(options) {
|
|
20
|
+
return {
|
|
21
|
+
async bundle(entryPoints, outDir, linguiConfig) {
|
|
22
|
+
let esbuild;
|
|
23
|
+
try {
|
|
24
|
+
esbuild = await import("esbuild");
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
throw new Error(`"esbuild" is required for createEsbuildBundler but is not installed. ` +
|
|
28
|
+
`Install it with: npm install esbuild`);
|
|
29
|
+
}
|
|
30
|
+
const includeDeps = options?.includeDeps || [];
|
|
31
|
+
const excludeExtensions = options?.excludeExtensions || DEFAULT_EXCLUDE_EXTENSIONS;
|
|
32
|
+
const shouldInclude = buildIncludeDepsFilter(includeDeps);
|
|
33
|
+
let esbuildOptions = {
|
|
34
|
+
entryPoints,
|
|
35
|
+
outExtension: { ".js": ".jsx" },
|
|
36
|
+
jsx: "preserve",
|
|
37
|
+
bundle: true,
|
|
38
|
+
platform: "node",
|
|
39
|
+
target: ["esnext"],
|
|
40
|
+
format: "esm",
|
|
41
|
+
splitting: true,
|
|
42
|
+
treeShaking: true,
|
|
43
|
+
outdir: outDir,
|
|
44
|
+
sourcemap: "inline",
|
|
45
|
+
sourceRoot: outDir,
|
|
46
|
+
sourcesContent: false,
|
|
47
|
+
metafile: true,
|
|
48
|
+
plugins: [
|
|
49
|
+
pluginLinguiMacro({ linguiConfig }),
|
|
50
|
+
{
|
|
51
|
+
name: "externalize-deps",
|
|
52
|
+
setup(build) {
|
|
53
|
+
build.onResolve({ filter: /^[^.#/].*/ }, async (args) => {
|
|
54
|
+
if (shouldInclude(args.path) || args.kind === "entry-point") {
|
|
55
|
+
return { external: false };
|
|
56
|
+
}
|
|
57
|
+
return { external: true };
|
|
58
|
+
});
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
name: "externalize-files",
|
|
63
|
+
setup(build) {
|
|
64
|
+
build.onResolve({ filter: createExtRegExp(excludeExtensions) }, () => ({
|
|
65
|
+
external: true,
|
|
66
|
+
}));
|
|
67
|
+
// for some dynamic imports, for example
|
|
68
|
+
// await import(`../locales/${locale}.po`)
|
|
69
|
+
// esbuild skips resolve, because files crawled from the disk
|
|
70
|
+
// to exclude those files from build, load an empty object instead
|
|
71
|
+
build.onLoad({ filter: createExtRegExp(excludeExtensions) }, () => {
|
|
72
|
+
return {
|
|
73
|
+
contents: JSON.stringify({}),
|
|
74
|
+
loader: "json",
|
|
75
|
+
};
|
|
76
|
+
});
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
};
|
|
81
|
+
if (options?.resolveEsbuildOptions) {
|
|
82
|
+
esbuildOptions = options.resolveEsbuildOptions(esbuildOptions);
|
|
83
|
+
}
|
|
84
|
+
const bundleResult = await esbuild.build(esbuildOptions);
|
|
85
|
+
const metafile = bundleResult.metafile;
|
|
86
|
+
const entryPointSet = new Set(entryPoints.map((ep) => path.resolve(ep)));
|
|
87
|
+
const allOutputPaths = new Set(Object.keys(metafile.outputs));
|
|
88
|
+
const chunks = Object.entries(metafile.outputs).map(([outputPath, meta]) => ({
|
|
89
|
+
id: outputPath,
|
|
90
|
+
filePath: outputPath,
|
|
91
|
+
entryPoint: meta.entryPoint
|
|
92
|
+
? resolveIfUserEntry(meta.entryPoint, entryPointSet)
|
|
93
|
+
: undefined,
|
|
94
|
+
imports: meta.imports
|
|
95
|
+
.filter((imp) => allOutputPaths.has(imp.path))
|
|
96
|
+
.map((imp) => imp.path),
|
|
97
|
+
}));
|
|
98
|
+
return { chunks };
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
const pluginLinguiMacro = (options) => ({
|
|
103
|
+
name: "linguiMacro",
|
|
104
|
+
setup(build) {
|
|
105
|
+
build.onLoad({ filter: babelRe, namespace: "" }, async (args) => {
|
|
106
|
+
const filename = path.relative(process.cwd(), args.path);
|
|
107
|
+
const contents = await fs.promises.readFile(args.path, "utf8");
|
|
108
|
+
const hasMacroRe = buildContentFilterRe(options.linguiConfig);
|
|
109
|
+
if (!hasMacroRe.test(contents)) {
|
|
110
|
+
// let esbuild process file as usual
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
const result = await transformAsync(contents, {
|
|
114
|
+
babelrc: false,
|
|
115
|
+
configFile: false,
|
|
116
|
+
filename,
|
|
117
|
+
sourceMaps: "inline",
|
|
118
|
+
parserOpts: {
|
|
119
|
+
plugins: getBabelParserOptions(filename, {}),
|
|
120
|
+
},
|
|
121
|
+
plugins: [
|
|
122
|
+
[
|
|
123
|
+
linguiMacroPlugin,
|
|
124
|
+
{
|
|
125
|
+
descriptorFields: "all",
|
|
126
|
+
linguiConfig: options.linguiConfig,
|
|
127
|
+
},
|
|
128
|
+
],
|
|
129
|
+
],
|
|
130
|
+
});
|
|
131
|
+
return { contents: result.code, loader: "tsx" };
|
|
132
|
+
});
|
|
133
|
+
},
|
|
134
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { BuildOptions } from "rolldown";
|
|
2
|
+
import type { ExperimentalExtractorBundler } from "@lingui/conf";
|
|
3
|
+
export type RolldownBundlerOptions = {
|
|
4
|
+
/**
|
|
5
|
+
* List of package name patterns to include for extraction.
|
|
6
|
+
*
|
|
7
|
+
* For example, to include all packages from your monorepo:
|
|
8
|
+
*
|
|
9
|
+
* ["@mycompany"]
|
|
10
|
+
*
|
|
11
|
+
* By default, all imports that look like package imports are ignored.
|
|
12
|
+
* This means imports that do not start with `/`, `./`, `../`, or `#`
|
|
13
|
+
* (used for subpath imports). TypeScript path aliases are also ignored
|
|
14
|
+
* because they look like package imports.
|
|
15
|
+
*
|
|
16
|
+
* Add here the packages you want to include.
|
|
17
|
+
*/
|
|
18
|
+
includeDeps?: string[];
|
|
19
|
+
/**
|
|
20
|
+
* svg, jpg and other files which might be imported in application should be excluded from analysis.
|
|
21
|
+
* By default, extractor provides a comprehensive list of extensions. If you feel like something
|
|
22
|
+
* is missing in this list please fill an issue on GitHub
|
|
23
|
+
*
|
|
24
|
+
* NOTE: changing this param will override default list of extensions.
|
|
25
|
+
*/
|
|
26
|
+
excludeExtensions?: string[];
|
|
27
|
+
resolveRolldownOptions?: (options: BuildOptions) => BuildOptions;
|
|
28
|
+
};
|
|
29
|
+
export declare function createRolldownBundler(options?: RolldownBundlerOptions): ExperimentalExtractorBundler;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import { buildIncludeDepsFilter } from "../buildIncludeDepsFilter.js";
|
|
3
|
+
import { DEFAULT_EXCLUDE_EXTENSIONS } from "../constants.js";
|
|
4
|
+
import { buildContentFilterRe } from "../buildContentFilter.js";
|
|
5
|
+
import linguiMacroPlugin from "@lingui/babel-plugin-lingui-macro";
|
|
6
|
+
import { transformAsync } from "@babel/core";
|
|
7
|
+
import { getBabelParserOptions } from "../../api/extractors/babel.js";
|
|
8
|
+
function createExtRegExp(extensions) {
|
|
9
|
+
return new RegExp("\\.(?:" + extensions.join("|") + ")(?:\\?.*)?$");
|
|
10
|
+
}
|
|
11
|
+
export function createRolldownBundler(options) {
|
|
12
|
+
return {
|
|
13
|
+
async bundle(entryPoints, outDir, linguiConfig) {
|
|
14
|
+
let rolldown;
|
|
15
|
+
try {
|
|
16
|
+
rolldown = await import("rolldown");
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
throw new Error(`"rolldown" is required for createRolldownBundler but is not installed. ` +
|
|
20
|
+
`Install it with: npm install rolldown`);
|
|
21
|
+
}
|
|
22
|
+
const includeDeps = options?.includeDeps || [];
|
|
23
|
+
const excludeExtensions = options?.excludeExtensions || DEFAULT_EXCLUDE_EXTENSIONS;
|
|
24
|
+
const shouldInclude = buildIncludeDepsFilter(includeDeps);
|
|
25
|
+
const extRegExp = createExtRegExp(excludeExtensions);
|
|
26
|
+
const hasMacroRe = buildContentFilterRe(linguiConfig);
|
|
27
|
+
const macroPlugin = {
|
|
28
|
+
name: "lingui:macro-transform",
|
|
29
|
+
transform: {
|
|
30
|
+
filter: {
|
|
31
|
+
id: /\.(?:[jt]sx?|[cm][jt]s)(?:$|\?)/,
|
|
32
|
+
code: hasMacroRe,
|
|
33
|
+
},
|
|
34
|
+
handler: async (code, filename, meta) => {
|
|
35
|
+
const result = await transformAsync(code, {
|
|
36
|
+
babelrc: false,
|
|
37
|
+
configFile: false,
|
|
38
|
+
filename,
|
|
39
|
+
sourceMaps: true,
|
|
40
|
+
parserOpts: {
|
|
41
|
+
plugins: getBabelParserOptions(filename, {}),
|
|
42
|
+
},
|
|
43
|
+
plugins: [
|
|
44
|
+
[
|
|
45
|
+
linguiMacroPlugin,
|
|
46
|
+
{
|
|
47
|
+
descriptorFields: "all",
|
|
48
|
+
linguiConfig,
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
],
|
|
52
|
+
});
|
|
53
|
+
return { code: result?.code ?? undefined, map: result?.map };
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
let rolldownOptions = {
|
|
58
|
+
input: entryPoints,
|
|
59
|
+
output: {
|
|
60
|
+
dir: outDir,
|
|
61
|
+
format: "esm",
|
|
62
|
+
sourcemap: "inline",
|
|
63
|
+
entryFileNames: "[name].jsx",
|
|
64
|
+
sourcemapPathTransform: (relativeSourcePath, sourcemapPath) => {
|
|
65
|
+
const sourcemapDir = path.dirname(sourcemapPath);
|
|
66
|
+
const absoluteSource = path.resolve(sourcemapDir, relativeSourcePath);
|
|
67
|
+
return path.relative(process.cwd(), absoluteSource);
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
platform: "node",
|
|
71
|
+
treeshake: true,
|
|
72
|
+
transform: {
|
|
73
|
+
jsx: "preserve",
|
|
74
|
+
},
|
|
75
|
+
external: (id, importer) => {
|
|
76
|
+
if (extRegExp.test(id)) {
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
if (importer && !path.isAbsolute(id) && /^[^.#/]/.test(id)) {
|
|
80
|
+
return !shouldInclude(id);
|
|
81
|
+
}
|
|
82
|
+
return false;
|
|
83
|
+
},
|
|
84
|
+
plugins: [macroPlugin],
|
|
85
|
+
};
|
|
86
|
+
if (options?.resolveRolldownOptions) {
|
|
87
|
+
rolldownOptions = options.resolveRolldownOptions(rolldownOptions);
|
|
88
|
+
}
|
|
89
|
+
const result = await rolldown.build(rolldownOptions);
|
|
90
|
+
const outputChunks = result.output.filter((item) => item.type === "chunk");
|
|
91
|
+
const outputFileNames = new Set(outputChunks.map((c) => c.fileName));
|
|
92
|
+
const chunks = outputChunks.map((chunk) => ({
|
|
93
|
+
id: chunk.fileName,
|
|
94
|
+
filePath: path.join(outDir, chunk.fileName),
|
|
95
|
+
entryPoint: chunk.isEntry && chunk.facadeModuleId
|
|
96
|
+
? chunk.facadeModuleId.replace(/\\/g, "/")
|
|
97
|
+
: undefined,
|
|
98
|
+
imports: [...chunk.imports, ...chunk.dynamicImports].filter((imp) => outputFileNames.has(imp)),
|
|
99
|
+
}));
|
|
100
|
+
return { chunks };
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
}
|
|
@@ -1,2 +1,23 @@
|
|
|
1
1
|
export const ENTRY_NAME_PH = "{entryName}";
|
|
2
2
|
export const DEFAULT_TEMPLATE_NAME = "messages";
|
|
3
|
+
export const DEFAULT_EXCLUDE_EXTENSIONS = [
|
|
4
|
+
"ico",
|
|
5
|
+
"pot",
|
|
6
|
+
"po",
|
|
7
|
+
"webp",
|
|
8
|
+
"xliff",
|
|
9
|
+
"woff2",
|
|
10
|
+
"woff",
|
|
11
|
+
"eot",
|
|
12
|
+
"gif",
|
|
13
|
+
"otf",
|
|
14
|
+
"ttf",
|
|
15
|
+
"mp4",
|
|
16
|
+
"svg",
|
|
17
|
+
"png",
|
|
18
|
+
"css",
|
|
19
|
+
"sass",
|
|
20
|
+
"scss",
|
|
21
|
+
"less",
|
|
22
|
+
"jpg",
|
|
23
|
+
];
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import extract from "../api/extractors/index.js";
|
|
2
|
+
export async function extractFromChunk(filename, linguiConfig) {
|
|
3
|
+
const messages = [];
|
|
4
|
+
const success = await extract(filename, (msg) => {
|
|
5
|
+
messages.push(msg);
|
|
6
|
+
}, linguiConfig);
|
|
7
|
+
return { success, messages };
|
|
8
|
+
}
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
+
import { ExtractedMessage } from "@lingui/conf";
|
|
1
2
|
export type ExtractWorkerFunction = typeof extractWorker;
|
|
2
|
-
declare const extractWorker: (linguiConfigPath: string,
|
|
3
|
-
success:
|
|
4
|
-
|
|
5
|
-
success: true;
|
|
6
|
-
stat: string;
|
|
3
|
+
declare const extractWorker: (linguiConfigPath: string, bundleFile: string) => Promise<{
|
|
4
|
+
success: boolean;
|
|
5
|
+
messages: ExtractedMessage[];
|
|
7
6
|
}>;
|
|
8
7
|
export { extractWorker };
|
|
@@ -1,29 +1,13 @@
|
|
|
1
|
-
import { getConfig } from "@lingui/conf";
|
|
2
|
-
import {
|
|
3
|
-
import { getFormat } from "../../api/formats/index.js";
|
|
1
|
+
import { getConfig, } from "@lingui/conf";
|
|
2
|
+
import { extractFromChunk } from "../extractFromChunk.js";
|
|
4
3
|
let linguiConfig;
|
|
5
|
-
|
|
6
|
-
const extractWorker = async (linguiConfigPath, entryPoint, bundleFile, outputPattern, template, locales, clean, overwrite) => {
|
|
4
|
+
const extractWorker = async (linguiConfigPath, bundleFile) => {
|
|
7
5
|
if (!linguiConfig) {
|
|
8
|
-
// initialize config once per worker, speed up workers follow execution
|
|
9
6
|
linguiConfig = getConfig({
|
|
10
7
|
configPath: linguiConfigPath,
|
|
11
8
|
skipValidation: true,
|
|
12
9
|
});
|
|
13
10
|
}
|
|
14
|
-
|
|
15
|
-
format = await getFormat(linguiConfig.format, linguiConfig.sourceLocale);
|
|
16
|
-
}
|
|
17
|
-
return await extractFromBundleAndWrite({
|
|
18
|
-
entryPoint,
|
|
19
|
-
bundleFile,
|
|
20
|
-
outputPattern,
|
|
21
|
-
format,
|
|
22
|
-
linguiConfig,
|
|
23
|
-
locales,
|
|
24
|
-
overwrite,
|
|
25
|
-
clean,
|
|
26
|
-
template,
|
|
27
|
-
});
|
|
11
|
+
return await extractFromChunk(bundleFile, linguiConfig);
|
|
28
12
|
};
|
|
29
13
|
export { extractWorker };
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { extractWorker } from "./extractWorker.js";
|
|
2
2
|
declare const _default: (args: Parameters<typeof extractWorker>) => Promise<{
|
|
3
|
-
success:
|
|
4
|
-
|
|
5
|
-
success: true;
|
|
6
|
-
stat: string;
|
|
3
|
+
success: boolean;
|
|
4
|
+
messages: import("packages/conf/dist/index.mjs").ExtractedMessage[];
|
|
7
5
|
}>;
|
|
8
6
|
export default _default;
|
|
@@ -1,17 +1,22 @@
|
|
|
1
1
|
import { program } from "commander";
|
|
2
|
-
import { getConfig } from "@lingui/conf";
|
|
2
|
+
import { getConfig, } from "@lingui/conf";
|
|
3
3
|
import nodepath from "path";
|
|
4
4
|
import { getFormat } from "./api/formats/index.js";
|
|
5
5
|
import fs from "fs/promises";
|
|
6
6
|
import normalizePath from "normalize-path";
|
|
7
|
-
import {
|
|
7
|
+
import { createEsbuildBundler } from "./extract-experimental/bundlers/esbuild.js";
|
|
8
8
|
import { globSync } from "node:fs";
|
|
9
9
|
import { styleText } from "node:util";
|
|
10
10
|
import { resolveWorkersOptions, } from "./api/resolveWorkersOptions.js";
|
|
11
|
-
import {
|
|
11
|
+
import { extractFromChunk } from "./extract-experimental/extractFromChunk.js";
|
|
12
|
+
import { writeCatalogs, writeTemplate, } from "./extract-experimental/writeCatalogs.js";
|
|
12
13
|
import { createExtractExperimentalWorkerPool } from "./api/workerPools.js";
|
|
14
|
+
import { buildChunkGraph } from "./extract-experimental/buildChunkGraph.js";
|
|
15
|
+
import { mergeExtractedMessage } from "./api/catalog/extractFromFiles.js";
|
|
16
|
+
import ora from "ora";
|
|
17
|
+
import ms from "ms";
|
|
13
18
|
export default async function command(linguiConfig, options) {
|
|
14
|
-
|
|
19
|
+
const startTime = Date.now();
|
|
15
20
|
const extractorConfig = linguiConfig.experimental?.extractor;
|
|
16
21
|
if (!extractorConfig) {
|
|
17
22
|
throw new Error("The configuration for experimental extractor is empty. Please read the docs.");
|
|
@@ -22,6 +27,23 @@ export default async function command(linguiConfig, options) {
|
|
|
22
27
|
" Use at your own risk.",
|
|
23
28
|
"",
|
|
24
29
|
].join("\n")));
|
|
30
|
+
// important to initialize ora before worker pool, otherwise it causes
|
|
31
|
+
// MaxListenersExceededWarning when workers >= 10
|
|
32
|
+
const spinner = ora();
|
|
33
|
+
// Phase: Resolve entry points
|
|
34
|
+
spinner.start("Resolving entry points...");
|
|
35
|
+
let phaseStart = Date.now();
|
|
36
|
+
const entryPoints = globSync(extractorConfig.entries);
|
|
37
|
+
if (entryPoints.length === 0) {
|
|
38
|
+
spinner.warn(`No entry points found (${ms(Date.now() - phaseStart)})`);
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
const displayEntries = entryPoints
|
|
42
|
+
.map((e) => normalizePath(nodepath.relative(linguiConfig.rootDir, e)))
|
|
43
|
+
.slice(0, 10);
|
|
44
|
+
const moreCount = entryPoints.length - displayEntries.length;
|
|
45
|
+
const entrySummary = displayEntries.join(", ") + (moreCount > 0 ? ` and ${moreCount} more` : "");
|
|
46
|
+
spinner.succeed(`Found ${entryPoints.length} entry point(s) (${ms(Date.now() - phaseStart)}): ${entrySummary}`);
|
|
25
47
|
// unfortunately we can't use os.tmpdir() in this case
|
|
26
48
|
// on windows it might create a folder on a different disk then source code is stored
|
|
27
49
|
// (tmpdir would be always on C: but code could be stored on D:)
|
|
@@ -32,9 +54,29 @@ export default async function command(linguiConfig, options) {
|
|
|
32
54
|
await fs.mkdir(tmpPrefix, { recursive: true });
|
|
33
55
|
const tempDir = await fs.mkdtemp(tmpPrefix);
|
|
34
56
|
await fs.rm(tempDir, { recursive: true, force: true });
|
|
35
|
-
|
|
57
|
+
let bundler;
|
|
58
|
+
if (extractorConfig.bundler) {
|
|
59
|
+
bundler = extractorConfig.bundler;
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
bundler = createEsbuildBundler({
|
|
63
|
+
includeDeps: extractorConfig.includeDeps,
|
|
64
|
+
excludeExtensions: extractorConfig.excludeExtensions,
|
|
65
|
+
resolveEsbuildOptions: extractorConfig.resolveEsbuildOptions,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
// Phase: Bundling
|
|
69
|
+
spinner.start("Bundling...");
|
|
70
|
+
phaseStart = Date.now();
|
|
71
|
+
const bundleResult = await bundler.bundle(entryPoints, tempDir, linguiConfig);
|
|
72
|
+
spinner.succeed(`Bundling done (${ms(Date.now() - phaseStart)})`);
|
|
73
|
+
const resolvedChunks = buildChunkGraph(bundleResult.chunks);
|
|
36
74
|
const stats = [];
|
|
37
75
|
let commandSuccess = true;
|
|
76
|
+
// Phase: Extract messages from each chunk
|
|
77
|
+
spinner.start("Extracting messages...");
|
|
78
|
+
phaseStart = Date.now();
|
|
79
|
+
const messagesByEntry = new Map();
|
|
38
80
|
if (options.workersOptions.poolSize) {
|
|
39
81
|
const resolvedConfigPath = linguiConfig.resolvedConfigPath;
|
|
40
82
|
if (!resolvedConfigPath) {
|
|
@@ -46,14 +88,17 @@ export default async function command(linguiConfig, options) {
|
|
|
46
88
|
poolSize: options.workersOptions.poolSize,
|
|
47
89
|
});
|
|
48
90
|
try {
|
|
49
|
-
await Promise.all(
|
|
50
|
-
const {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
91
|
+
await Promise.all(resolvedChunks.map(async ({ filePath, entryPoints }) => {
|
|
92
|
+
const { messages, success } = await pool.run(resolvedConfigPath, filePath);
|
|
93
|
+
if (!success) {
|
|
94
|
+
commandSuccess = false;
|
|
95
|
+
}
|
|
96
|
+
for (const entryPoint of entryPoints) {
|
|
97
|
+
if (!messagesByEntry.has(entryPoint)) {
|
|
98
|
+
messagesByEntry.set(entryPoint, {});
|
|
99
|
+
}
|
|
100
|
+
messages.forEach((message) => {
|
|
101
|
+
mergeExtractedMessage(message, messagesByEntry.get(entryPoint), linguiConfig);
|
|
57
102
|
});
|
|
58
103
|
}
|
|
59
104
|
}));
|
|
@@ -63,29 +108,57 @@ export default async function command(linguiConfig, options) {
|
|
|
63
108
|
}
|
|
64
109
|
}
|
|
65
110
|
else {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
111
|
+
await Promise.all(resolvedChunks.map(async ({ filePath, entryPoints }) => {
|
|
112
|
+
const { messages, success } = await extractFromChunk(filePath, linguiConfig);
|
|
113
|
+
if (!success) {
|
|
114
|
+
commandSuccess = false;
|
|
115
|
+
}
|
|
116
|
+
for (const entryPoint of entryPoints) {
|
|
117
|
+
if (!messagesByEntry.has(entryPoint)) {
|
|
118
|
+
messagesByEntry.set(entryPoint, {});
|
|
119
|
+
}
|
|
120
|
+
messages.forEach((message) => {
|
|
121
|
+
mergeExtractedMessage(message, messagesByEntry.get(entryPoint), linguiConfig);
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
}));
|
|
125
|
+
}
|
|
126
|
+
spinner.succeed(`Extracting done (${ms(Date.now() - phaseStart)})`);
|
|
127
|
+
// Phase: Write catalogs per entry point
|
|
128
|
+
spinner.start("Writing catalogs...");
|
|
129
|
+
phaseStart = Date.now();
|
|
130
|
+
const format = await getFormat(linguiConfig.format, linguiConfig.sourceLocale);
|
|
131
|
+
const locales = options.locales || linguiConfig.locales;
|
|
132
|
+
for (const [entryPoint, messages] of messagesByEntry) {
|
|
133
|
+
let stat;
|
|
134
|
+
if (options.template) {
|
|
135
|
+
stat = (await writeTemplate({
|
|
136
|
+
linguiConfig,
|
|
137
|
+
clean: options.clean || false,
|
|
73
138
|
format,
|
|
139
|
+
messages,
|
|
140
|
+
entryPoint,
|
|
141
|
+
outputPattern: extractorConfig.output,
|
|
142
|
+
})).statMessage;
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
stat = (await writeCatalogs({
|
|
146
|
+
locales,
|
|
74
147
|
linguiConfig,
|
|
75
|
-
locales: options.locales || linguiConfig.locales,
|
|
76
|
-
overwrite: options.overwrite || false,
|
|
77
148
|
clean: options.clean || false,
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
content: result.stat,
|
|
85
|
-
});
|
|
86
|
-
}
|
|
149
|
+
format,
|
|
150
|
+
messages,
|
|
151
|
+
entryPoint,
|
|
152
|
+
overwrite: options.overwrite || false,
|
|
153
|
+
outputPattern: extractorConfig.output,
|
|
154
|
+
})).statMessage;
|
|
87
155
|
}
|
|
156
|
+
stats.push({
|
|
157
|
+
entry: normalizePath(nodepath.relative(linguiConfig.rootDir, entryPoint)),
|
|
158
|
+
content: stat,
|
|
159
|
+
});
|
|
88
160
|
}
|
|
161
|
+
spinner.succeed(`Writing catalogs done (${ms(Date.now() - phaseStart)})`);
|
|
89
162
|
// cleanup temp directory
|
|
90
163
|
await fs.rm(tempDir, { recursive: true, force: true });
|
|
91
164
|
stats
|
|
@@ -93,6 +166,13 @@ export default async function command(linguiConfig, options) {
|
|
|
93
166
|
.forEach(({ entry, content }) => {
|
|
94
167
|
console.log([`Catalog statistics for ${entry}:`, content, ""].join("\n"));
|
|
95
168
|
});
|
|
169
|
+
const totalTime = Date.now() - startTime;
|
|
170
|
+
if (commandSuccess) {
|
|
171
|
+
console.log(styleText("green", `Extraction completed successfully in ${ms(totalTime)}`));
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
console.log(styleText("red", `Extraction completed with errors in ${ms(totalTime)}`));
|
|
175
|
+
}
|
|
96
176
|
return commandSuccess;
|
|
97
177
|
}
|
|
98
178
|
if (import.meta.main) {
|
|
@@ -7,7 +7,7 @@ import { readFileSync } from "node:fs";
|
|
|
7
7
|
import path from "node:path";
|
|
8
8
|
const getTargetLocales = (config) => {
|
|
9
9
|
const sourceLocale = config.sourceLocale || "en";
|
|
10
|
-
const pseudoLocale = config.pseudoLocale || "pseudo";
|
|
10
|
+
const pseudoLocale = config.pseudoLocale.locale || "pseudo";
|
|
11
11
|
return config.locales.filter((value) => value != sourceLocale && value != pseudoLocale);
|
|
12
12
|
};
|
|
13
13
|
// Main sync method, call "Init" or "Sync" depending on the project context
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lingui/cli",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.6.0",
|
|
4
4
|
"description": "Lingui CLI to extract messages, compile catalogs, and manage translation workflows",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
@@ -33,7 +33,9 @@
|
|
|
33
33
|
"exports": {
|
|
34
34
|
".": "./dist/index.js",
|
|
35
35
|
"./api": "./dist/api/index.js",
|
|
36
|
-
"./api/extractors/babel": "./dist/api/extractors/babel.js"
|
|
36
|
+
"./api/extractors/babel": "./dist/api/extractors/babel.js",
|
|
37
|
+
"./bundlers/esbuild": "./dist/extract-experimental/bundlers/esbuild.js",
|
|
38
|
+
"./bundlers/rolldown": "./dist/extract-experimental/bundlers/rolldown.js"
|
|
37
39
|
},
|
|
38
40
|
"scripts": {
|
|
39
41
|
"build": "tsc -p tsconfig.build.json",
|
|
@@ -52,16 +54,15 @@
|
|
|
52
54
|
"@babel/generator": "^7.28.5",
|
|
53
55
|
"@babel/parser": "^7.22.0",
|
|
54
56
|
"@babel/types": "^7.21.2",
|
|
55
|
-
"@lingui/babel-plugin-extract-messages": "6.
|
|
56
|
-
"@lingui/babel-plugin-lingui-macro": "6.
|
|
57
|
-
"@lingui/conf": "6.
|
|
58
|
-
"@lingui/core": "6.
|
|
59
|
-
"@lingui/format-po": "6.
|
|
60
|
-
"@lingui/message-utils": "6.
|
|
57
|
+
"@lingui/babel-plugin-extract-messages": "6.6.0",
|
|
58
|
+
"@lingui/babel-plugin-lingui-macro": "6.6.0",
|
|
59
|
+
"@lingui/conf": "6.6.0",
|
|
60
|
+
"@lingui/core": "6.6.0",
|
|
61
|
+
"@lingui/format-po": "6.6.0",
|
|
62
|
+
"@lingui/message-utils": "6.6.0",
|
|
61
63
|
"chokidar": "5.0.0",
|
|
62
64
|
"cli-table3": "^0.6.5",
|
|
63
65
|
"commander": "^14.0.2",
|
|
64
|
-
"esbuild": "^0.25.1",
|
|
65
66
|
"jiti": "^2.6.1",
|
|
66
67
|
"micromatch": "^4.0.7",
|
|
67
68
|
"ms": "^2.1.3",
|
|
@@ -71,15 +72,30 @@
|
|
|
71
72
|
"source-map": "^0.7.6",
|
|
72
73
|
"tinypool": "^2.1.0"
|
|
73
74
|
},
|
|
75
|
+
"peerDependencies": {
|
|
76
|
+
"esbuild": "^0.28.1",
|
|
77
|
+
"rolldown": "^1.0.0"
|
|
78
|
+
},
|
|
79
|
+
"peerDependenciesMeta": {
|
|
80
|
+
"esbuild": {
|
|
81
|
+
"optional": true
|
|
82
|
+
},
|
|
83
|
+
"rolldown": {
|
|
84
|
+
"optional": true
|
|
85
|
+
}
|
|
86
|
+
},
|
|
74
87
|
"devDependencies": {
|
|
75
88
|
"@lingui/test-utils": "3.0.3",
|
|
89
|
+
"@rolldown/plugin-babel": "^0.2.3",
|
|
76
90
|
"@types/babel__generator": "^7.27.0",
|
|
77
91
|
"@types/micromatch": "^4.0.1",
|
|
78
92
|
"@types/ms": "^2.1.0",
|
|
79
93
|
"@types/normalize-path": "^3.0.0",
|
|
94
|
+
"esbuild": "^0.28.1",
|
|
80
95
|
"mock-fs": "^5.2.0",
|
|
81
96
|
"msw": "^2.12.7",
|
|
97
|
+
"rolldown": "^1.1.1",
|
|
82
98
|
"vitest": "catalog:"
|
|
83
99
|
},
|
|
84
|
-
"gitHead": "
|
|
100
|
+
"gitHead": "665a19815378dedd89346bb7707bdb0e28df79e7"
|
|
85
101
|
}
|
|
@@ -1,3 +0,0 @@
|
|
|
1
|
-
import { ExperimentalExtractorOptions, LinguiConfigNormalized } from "@lingui/conf";
|
|
2
|
-
import { Metafile } from "esbuild";
|
|
3
|
-
export declare function bundleSource(linguiConfig: LinguiConfigNormalized, extractorConfig: ExperimentalExtractorOptions, entryPoints: string[], outDir: string, rootDir: string): Promise<Metafile>;
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
import { pluginLinguiMacro } from "./linguiEsbuildPlugin.js";
|
|
2
|
-
import { buildIncludeDepsFilter } from "./buildIncludeDepsFilter.js";
|
|
3
|
-
function createExtRegExp(extensions) {
|
|
4
|
-
return new RegExp("\\.(?:" + extensions.join("|") + ")(?:\\?.*)?$");
|
|
5
|
-
}
|
|
6
|
-
export async function bundleSource(linguiConfig, extractorConfig, entryPoints, outDir, rootDir) {
|
|
7
|
-
const esbuild = await import("esbuild");
|
|
8
|
-
const excludeExtensions = extractorConfig.excludeExtensions || [
|
|
9
|
-
"ico",
|
|
10
|
-
"pot",
|
|
11
|
-
"xliff",
|
|
12
|
-
"woff2",
|
|
13
|
-
"woff",
|
|
14
|
-
"eot",
|
|
15
|
-
"gif",
|
|
16
|
-
"otf",
|
|
17
|
-
"ttf",
|
|
18
|
-
"mp4",
|
|
19
|
-
"svg",
|
|
20
|
-
"png",
|
|
21
|
-
"css",
|
|
22
|
-
"sass",
|
|
23
|
-
"scss",
|
|
24
|
-
"less",
|
|
25
|
-
"jpg",
|
|
26
|
-
];
|
|
27
|
-
const esbuildOptions = {
|
|
28
|
-
entryPoints: entryPoints,
|
|
29
|
-
outExtension: { ".js": ".jsx" },
|
|
30
|
-
jsx: "preserve",
|
|
31
|
-
bundle: true,
|
|
32
|
-
platform: "node",
|
|
33
|
-
target: ["esnext"],
|
|
34
|
-
format: "esm",
|
|
35
|
-
splitting: false,
|
|
36
|
-
treeShaking: true,
|
|
37
|
-
outdir: outDir,
|
|
38
|
-
sourcemap: "inline",
|
|
39
|
-
sourceRoot: outDir,
|
|
40
|
-
sourcesContent: false,
|
|
41
|
-
metafile: true,
|
|
42
|
-
plugins: [
|
|
43
|
-
pluginLinguiMacro({ linguiConfig }),
|
|
44
|
-
{
|
|
45
|
-
name: "externalize-deps",
|
|
46
|
-
setup(build) {
|
|
47
|
-
const shouldInclude = buildIncludeDepsFilter(extractorConfig.includeDeps || []);
|
|
48
|
-
// considers all import paths that "look like" package imports in the original source code to be package imports.
|
|
49
|
-
// Specifically import paths that don't start with a path segment of / or . or .. are considered to be package imports.
|
|
50
|
-
// The only two exceptions to this rule are subpath imports (which start with a # character) and deps specified in the `includeDeps`
|
|
51
|
-
build.onResolve({ filter: /^[^.#/].*/ }, async (args) => {
|
|
52
|
-
if (shouldInclude(args.path) || args.kind === "entry-point") {
|
|
53
|
-
return { external: false };
|
|
54
|
-
}
|
|
55
|
-
return {
|
|
56
|
-
external: true,
|
|
57
|
-
};
|
|
58
|
-
});
|
|
59
|
-
},
|
|
60
|
-
},
|
|
61
|
-
{
|
|
62
|
-
name: "externalize-files",
|
|
63
|
-
setup(build) {
|
|
64
|
-
build.onResolve({ filter: createExtRegExp(excludeExtensions) }, () => ({
|
|
65
|
-
external: true,
|
|
66
|
-
}));
|
|
67
|
-
},
|
|
68
|
-
},
|
|
69
|
-
],
|
|
70
|
-
};
|
|
71
|
-
const bundleResult = await esbuild.build(extractorConfig.resolveEsbuildOptions
|
|
72
|
-
? extractorConfig.resolveEsbuildOptions(esbuildOptions)
|
|
73
|
-
: esbuildOptions);
|
|
74
|
-
return bundleResult.metafile;
|
|
75
|
-
}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import { LinguiConfigNormalized } from "@lingui/conf";
|
|
2
|
-
import { FormatterWrapper } from "../api/formats/index.js";
|
|
3
|
-
export declare function extractFromBundleAndWrite(params: {
|
|
4
|
-
entryPoint: string;
|
|
5
|
-
bundleFile: string;
|
|
6
|
-
linguiConfig: LinguiConfigNormalized;
|
|
7
|
-
outputPattern: string;
|
|
8
|
-
format: FormatterWrapper;
|
|
9
|
-
template: boolean;
|
|
10
|
-
locales: string[];
|
|
11
|
-
clean: boolean;
|
|
12
|
-
overwrite: boolean;
|
|
13
|
-
}): Promise<{
|
|
14
|
-
success: false;
|
|
15
|
-
} | {
|
|
16
|
-
success: true;
|
|
17
|
-
stat: string;
|
|
18
|
-
}>;
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import { mergeExtractedMessage } from "../api/catalog/extractFromFiles.js";
|
|
2
|
-
import { writeCatalogs, writeTemplate } from "./writeCatalogs.js";
|
|
3
|
-
import extract from "../api/extractors/index.js";
|
|
4
|
-
async function extractFromBundle(filename, linguiConfig) {
|
|
5
|
-
const messages = {};
|
|
6
|
-
let success;
|
|
7
|
-
try {
|
|
8
|
-
await extract(filename, (msg) => {
|
|
9
|
-
mergeExtractedMessage(msg, messages, linguiConfig);
|
|
10
|
-
}, linguiConfig);
|
|
11
|
-
success = true;
|
|
12
|
-
}
|
|
13
|
-
catch (e) {
|
|
14
|
-
console.error(`Cannot process file ${filename} ${e.message}`);
|
|
15
|
-
console.error(e.stack);
|
|
16
|
-
success = false;
|
|
17
|
-
}
|
|
18
|
-
return { success, messages };
|
|
19
|
-
}
|
|
20
|
-
export async function extractFromBundleAndWrite(params) {
|
|
21
|
-
const { linguiConfig, entryPoint, format, outputPattern, locales, overwrite, clean, template, } = params;
|
|
22
|
-
const { messages, success } = await extractFromBundle(params.bundleFile, params.linguiConfig);
|
|
23
|
-
if (!success) {
|
|
24
|
-
return { success: false };
|
|
25
|
-
}
|
|
26
|
-
let stat;
|
|
27
|
-
if (template) {
|
|
28
|
-
stat = (await writeTemplate({
|
|
29
|
-
linguiConfig,
|
|
30
|
-
clean,
|
|
31
|
-
format,
|
|
32
|
-
messages,
|
|
33
|
-
entryPoint,
|
|
34
|
-
outputPattern,
|
|
35
|
-
})).statMessage;
|
|
36
|
-
}
|
|
37
|
-
else {
|
|
38
|
-
stat = (await writeCatalogs({
|
|
39
|
-
locales,
|
|
40
|
-
linguiConfig,
|
|
41
|
-
clean,
|
|
42
|
-
format,
|
|
43
|
-
messages,
|
|
44
|
-
entryPoint,
|
|
45
|
-
overwrite,
|
|
46
|
-
outputPattern,
|
|
47
|
-
})).statMessage;
|
|
48
|
-
}
|
|
49
|
-
return { success: true, stat };
|
|
50
|
-
}
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import { transformAsync } from "@babel/core";
|
|
2
|
-
import fs from "fs";
|
|
3
|
-
import path from "path";
|
|
4
|
-
import { babelRe, getBabelParserOptions } from "../api/extractors/babel.js";
|
|
5
|
-
import linguiMacroPlugin from "@lingui/babel-plugin-lingui-macro";
|
|
6
|
-
export const pluginLinguiMacro = (options) => ({
|
|
7
|
-
name: "linguiMacro",
|
|
8
|
-
setup(build) {
|
|
9
|
-
build.onLoad({ filter: babelRe, namespace: "" }, async (args) => {
|
|
10
|
-
const filename = path.relative(process.cwd(), args.path);
|
|
11
|
-
const contents = await fs.promises.readFile(args.path, "utf8");
|
|
12
|
-
const hasMacroRe = /from ["']@lingui(\/.+)?\/macro["']/g;
|
|
13
|
-
if (!hasMacroRe.test(contents)) {
|
|
14
|
-
// let esbuild process file as usual
|
|
15
|
-
return undefined;
|
|
16
|
-
}
|
|
17
|
-
const result = await transformAsync(contents, {
|
|
18
|
-
babelrc: false,
|
|
19
|
-
configFile: false,
|
|
20
|
-
filename: filename,
|
|
21
|
-
sourceMaps: "inline",
|
|
22
|
-
parserOpts: {
|
|
23
|
-
plugins: getBabelParserOptions(filename, {}),
|
|
24
|
-
},
|
|
25
|
-
plugins: [
|
|
26
|
-
[
|
|
27
|
-
linguiMacroPlugin,
|
|
28
|
-
{
|
|
29
|
-
descriptorFields: "all",
|
|
30
|
-
linguiConfig: options.linguiConfig,
|
|
31
|
-
},
|
|
32
|
-
],
|
|
33
|
-
],
|
|
34
|
-
});
|
|
35
|
-
return { contents: result.code, loader: "tsx" };
|
|
36
|
-
});
|
|
37
|
-
},
|
|
38
|
-
});
|