@lingui/cli 6.4.0 → 6.5.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 (30) hide show
  1. package/dist/api/compile/compileLocale.js +3 -2
  2. package/dist/api/compile.d.ts +3 -1
  3. package/dist/api/compile.js +4 -4
  4. package/dist/api/pseudoLocalize.d.ts +2 -1
  5. package/dist/api/pseudoLocalize.js +3 -2
  6. package/dist/api/stats.js +1 -1
  7. package/dist/extract-experimental/buildChunkGraph.d.ts +10 -0
  8. package/dist/extract-experimental/buildChunkGraph.js +37 -0
  9. package/dist/extract-experimental/buildContentFilter.d.ts +2 -0
  10. package/dist/extract-experimental/buildContentFilter.js +12 -0
  11. package/dist/extract-experimental/bundlers/esbuild.d.ts +29 -0
  12. package/dist/extract-experimental/bundlers/esbuild.js +134 -0
  13. package/dist/extract-experimental/bundlers/rolldown.d.ts +29 -0
  14. package/dist/extract-experimental/bundlers/rolldown.js +103 -0
  15. package/dist/extract-experimental/constants.d.ts +1 -0
  16. package/dist/extract-experimental/constants.js +21 -0
  17. package/dist/extract-experimental/extractFromChunk.d.ts +5 -0
  18. package/dist/extract-experimental/extractFromChunk.js +8 -0
  19. package/dist/extract-experimental/workers/extractWorker.d.ts +4 -5
  20. package/dist/extract-experimental/workers/extractWorker.js +4 -20
  21. package/dist/extract-experimental/workers/extractWorkerWrapper.prod.d.ts +2 -4
  22. package/dist/lingui-extract-experimental.js +111 -31
  23. package/dist/services/translationIO.js +1 -1
  24. package/package.json +26 -10
  25. package/dist/extract-experimental/bundleSource.d.ts +0 -3
  26. package/dist/extract-experimental/bundleSource.js +0 -75
  27. package/dist/extract-experimental/extractFromBundleAndWrite.d.ts +0 -18
  28. package/dist/extract-experimental/extractFromBundleAndWrite.js +0 -50
  29. package/dist/extract-experimental/linguiEsbuildPlugin.d.ts +0 -5
  30. package/dist/extract-experimental/linguiEsbuildPlugin.js +0 -38
@@ -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) {
@@ -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 {};
@@ -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
  }
@@ -1 +1,2 @@
1
- export default function (message: string): string;
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,2 @@
1
+ import { LinguiConfigNormalized } from "@lingui/conf";
2
+ export declare const buildContentFilterRe: (config: LinguiConfigNormalized) => RegExp;
@@ -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,3 @@
1
1
  export declare const ENTRY_NAME_PH = "{entryName}";
2
2
  export declare const DEFAULT_TEMPLATE_NAME = "messages";
3
+ export declare const DEFAULT_EXCLUDE_EXTENSIONS: string[];
@@ -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,5 @@
1
+ import { ExtractedMessage, LinguiConfigNormalized } from "@lingui/conf";
2
+ export declare function extractFromChunk(filename: string, linguiConfig: LinguiConfigNormalized): Promise<{
3
+ success: boolean;
4
+ messages: ExtractedMessage[];
5
+ }>;
@@ -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, entryPoint: string, bundleFile: string, outputPattern: string, template: boolean, locales: string[], clean: boolean, overwrite: boolean) => Promise<{
3
- success: false;
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 { extractFromBundleAndWrite } from "../extractFromBundleAndWrite.js";
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
- let format;
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
- if (!format) {
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: false;
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 { bundleSource } from "./extract-experimental/bundleSource.js";
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 { extractFromBundleAndWrite } from "./extract-experimental/extractFromBundleAndWrite.js";
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
- options.verbose && console.log("Extracting messages from source files…");
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
- const bundleResult = await bundleSource(linguiConfig, extractorConfig, globSync(extractorConfig.entries), tempDir, linguiConfig.rootDir);
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(Object.keys(bundleResult.outputs).map(async (outFile) => {
50
- const { entryPoint } = bundleResult.outputs[outFile];
51
- const result = await pool.run(resolvedConfigPath, entryPoint, outFile, extractorConfig.output, options.template || false, options.locales || linguiConfig.locales, options.clean || false, options.overwrite || false);
52
- commandSuccess &&= result.success;
53
- if (result.success) {
54
- stats.push({
55
- entry: normalizePath(nodepath.relative(linguiConfig.rootDir, entryPoint)),
56
- content: result.stat,
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
- const format = await getFormat(linguiConfig.format, linguiConfig.sourceLocale);
67
- for (const outFile of Object.keys(bundleResult.outputs)) {
68
- const { entryPoint } = bundleResult.outputs[outFile];
69
- const result = await extractFromBundleAndWrite({
70
- entryPoint: entryPoint,
71
- bundleFile: outFile,
72
- outputPattern: extractorConfig.output,
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
- template: options.template || false,
79
- });
80
- commandSuccess &&= result.success;
81
- if (result.success) {
82
- stats.push({
83
- entry: normalizePath(nodepath.relative(linguiConfig.rootDir, entryPoint)),
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.4.0",
3
+ "version": "6.5.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.4.0",
56
- "@lingui/babel-plugin-lingui-macro": "6.4.0",
57
- "@lingui/conf": "6.4.0",
58
- "@lingui/core": "6.4.0",
59
- "@lingui/format-po": "6.4.0",
60
- "@lingui/message-utils": "6.4.0",
57
+ "@lingui/babel-plugin-extract-messages": "6.5.0",
58
+ "@lingui/babel-plugin-lingui-macro": "6.5.0",
59
+ "@lingui/conf": "6.5.0",
60
+ "@lingui/core": "6.5.0",
61
+ "@lingui/format-po": "6.5.0",
62
+ "@lingui/message-utils": "6.5.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": "e8657bf8cbe6e8abc62a46a24e018ff9812918be"
100
+ "gitHead": "9687149054666bbaf89196573ba597d0d6fd09e4"
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,5 +0,0 @@
1
- import { Plugin } from "esbuild";
2
- import { LinguiConfigNormalized } from "@lingui/conf";
3
- export declare const pluginLinguiMacro: (options: {
4
- linguiConfig: LinguiConfigNormalized;
5
- }) => Plugin;
@@ -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
- });