@lingui/cli
Version:
Lingui CLI to extract messages, compile catalogs, and manage translation workflows
124 lines (123 loc) • 5.88 kB
JavaScript
import { program } from "commander";
import { getConfig } from "@lingui/conf";
import nodepath from "path";
import { getFormat } from "./api/formats/index.js";
import fs from "fs/promises";
import normalizePath from "normalize-path";
import { bundleSource } from "./extract-experimental/bundleSource.js";
import { globSync } from "node:fs";
import { styleText } from "node:util";
import { resolveWorkersOptions, } from "./api/resolveWorkersOptions.js";
import { extractFromBundleAndWrite } from "./extract-experimental/extractFromBundleAndWrite.js";
import { createExtractExperimentalWorkerPool } from "./api/workerPools.js";
export default async function command(linguiConfig, options) {
options.verbose && console.log("Extracting messages from source files…");
const extractorConfig = linguiConfig.experimental?.extractor;
if (!extractorConfig) {
throw new Error("The configuration for experimental extractor is empty. Please read the docs.");
}
console.log(styleText("yellow", [
"You have using an experimental feature",
"Experimental features are not covered by semver, and may cause unexpected or broken application behavior." +
" Use at your own risk.",
"",
].join("\n")));
// unfortunately we can't use os.tmpdir() in this case
// on windows it might create a folder on a different disk then source code is stored
// (tmpdir would be always on C: but code could be stored on D:)
// and then relative path in sourcemaps produced by esbuild will be broken.
// sourcemaps itself doesn't allow to have absolute windows path, because they are not URL compatible.
// that's why we store esbuild bundles in .lingui folder
const tmpPrefix = ".lingui/";
await fs.mkdir(tmpPrefix, { recursive: true });
const tempDir = await fs.mkdtemp(tmpPrefix);
await fs.rm(tempDir, { recursive: true, force: true });
const bundleResult = await bundleSource(linguiConfig, extractorConfig, globSync(extractorConfig.entries), tempDir, linguiConfig.rootDir);
const stats = [];
let commandSuccess = true;
if (options.workersOptions.poolSize) {
const resolvedConfigPath = linguiConfig.resolvedConfigPath;
if (!resolvedConfigPath) {
throw new Error("Multithreading is only supported when lingui config loaded from file system, not passed by API");
}
options.verbose &&
console.log(`Use worker pool of size ${options.workersOptions.poolSize}`);
const pool = createExtractExperimentalWorkerPool({
poolSize: options.workersOptions.poolSize,
});
try {
await Promise.all(Object.keys(bundleResult.outputs).map(async (outFile) => {
const { entryPoint } = bundleResult.outputs[outFile];
const result = await pool.run(resolvedConfigPath, entryPoint, outFile, extractorConfig.output, options.template || false, options.locales || linguiConfig.locales, options.clean || false, options.overwrite || false);
commandSuccess &&= result.success;
if (result.success) {
stats.push({
entry: normalizePath(nodepath.relative(linguiConfig.rootDir, entryPoint)),
content: result.stat,
});
}
}));
}
finally {
await pool.destroy();
}
}
else {
const format = await getFormat(linguiConfig.format, linguiConfig.sourceLocale);
for (const outFile of Object.keys(bundleResult.outputs)) {
const { entryPoint } = bundleResult.outputs[outFile];
const result = await extractFromBundleAndWrite({
entryPoint: entryPoint,
bundleFile: outFile,
outputPattern: extractorConfig.output,
format,
linguiConfig,
locales: options.locales || linguiConfig.locales,
overwrite: options.overwrite || false,
clean: options.clean || false,
template: options.template || false,
});
commandSuccess &&= result.success;
if (result.success) {
stats.push({
entry: normalizePath(nodepath.relative(linguiConfig.rootDir, entryPoint)),
content: result.stat,
});
}
}
}
// cleanup temp directory
await fs.rm(tempDir, { recursive: true, force: true });
stats
.sort((a, b) => a.entry.localeCompare(b.entry))
.forEach(({ entry, content }) => {
console.log([`Catalog statistics for ${entry}:`, content, ""].join("\n"));
});
return commandSuccess;
}
if (import.meta.main) {
program
.option("--config <path>", "Path to the config file")
.option("--template", "Extract to template")
.option("--overwrite", "Overwrite translations for source locale")
.option("--clean", "Remove obsolete translations")
.option("--locale <locale, [...]>", "Only extract the specified locales")
.option("--verbose", "Verbose output")
.option("--workers <n>", "Number of worker threads to use (default: CPU count - 1, capped at 8). Pass `--workers 1` to disable worker threads and run everything in a single process")
.parse(process.argv);
const options = program.opts();
const config = getConfig({
configPath: options.config,
});
const result = command(config, {
verbose: options.verbose || false,
template: options.template,
locales: options.locale?.split(","),
overwrite: options.overwrite,
clean: options.clean,
workersOptions: resolveWorkersOptions(options),
}).then(() => {
if (!result)
process.exit(1);
});
}