UNPKG

@lingui/cli

Version:

Lingui CLI to extract messages, compile catalogs, and manage translation workflows

193 lines (192 loc) 7.96 kB
import { styleText } from "node:util"; import { watch } from "chokidar"; import { program } from "commander"; import nodepath from "path"; import { getConfig } from "@lingui/conf"; import { getCatalogs } from "./api/index.js"; import { printStats } from "./api/stats.js"; import { helpRun } from "./api/help.js"; import ora from "ora"; import normalizePath from "normalize-path"; import { resolveWorkersOptions, } from "./api/resolveWorkersOptions.js"; import { createExtractWorkerPool, } from "./api/workerPools.js"; import ms from "ms"; import { getPathsForExtractWatcher } from "./api/getPathsForExtractWatcher.js"; import { glob } from "node:fs/promises"; import micromatch from "micromatch"; export default async function command(config, options) { const startTime = Date.now(); options.verbose && console.log("Extracting messages from source files…"); const catalogs = await getCatalogs(config); const catalogStats = {}; let commandSuccess = true; let workerPool; // important to initialize ora before worker pool, otherwise it cause // MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 unpipe listeners added to [WriteStream]. MaxListeners is 10. Use emitter.setMaxListeners() to increase limit // when workers >= 10 const spinner = ora(); if (options.workersOptions.poolSize) { options.verbose && console.log(`Use worker pool of size ${options.workersOptions.poolSize}`); workerPool = createExtractWorkerPool(options.workersOptions); } spinner.start(); let extractionResult; try { extractionResult = await Promise.all(catalogs.map(async (catalog) => { const result = await catalog.make({ ...options, orderBy: config.orderBy, workerPool, }); catalogStats[normalizePath(nodepath.relative(config.rootDir, catalog.path))] = result || {}; commandSuccess &&= Boolean(result); return { catalog, messagesByLocale: result }; })); } finally { if (workerPool) { await workerPool.destroy(); } } const doneMsg = `Done in ${ms(Date.now() - startTime)}`; if (commandSuccess) { spinner.succeed(doneMsg); } else { spinner.fail(doneMsg); } Object.entries(catalogStats).forEach(([key, value]) => { console.log(`Catalog statistics for ${key}: `); console.log(printStats(config, value).toString()); console.log(); }); if (!options.watch) { console.log(`(Use "${styleText("yellow", helpRun("extract"))}" to update catalogs with new messages.)`); console.log(`(Use "${styleText("yellow", helpRun("compile"))}" to compile catalogs for production. Alternatively, use bundler plugins: https://lingui.dev/ref/cli#compiling-catalogs-in-ci)`); } // If service key is present in configuration, synchronize with cloud translation platform if (config.service?.name?.length) { const moduleName = config.service.name.charAt(0).toLowerCase() + config.service.name.slice(1); const services = { translationIO: () => import("./services/translationIO.js"), }; if (!services[moduleName]) { console.error(`Can't load service module ${moduleName}`); return false; } try { const module = await services[moduleName](); await module .default(config, options, extractionResult) .then(console.log) .catch(console.error); } catch (err) { console.error(`Can't load service module ${moduleName}`, err); } } return commandSuccess; } if (import.meta.main) { program .option("--config <path>", "Path to the config file") .option("--locale <locale, [...]>", "Only extract the specified locales", (value) => { return value .split(",") .map((s) => s.trim()) .filter(Boolean); }) .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") .option("--overwrite", "Overwrite translations for source locale") .option("--clean", "Remove obsolete translations") .option("--debounce <delay>", "Debounces extraction for given amount of milliseconds") .option("--verbose", "Verbose output") .option("--watch", "Enables Watch Mode") .argument("[files...]", "Filter source paths to extract messages only from specific files") .parse(process.argv); const options = program.opts(); const config = getConfig({ configPath: options.config, }); let hasErrors = false; if (options.locale) { const missingLocale = options.locale.find((l) => !config.locales.includes(l)); if (missingLocale) { hasErrors = true; console.error(`Locale ${styleText("bold", missingLocale)} does not exist.`); console.error(); } } if (hasErrors) process.exit(1); const extract = (filePath) => { return command(config, { verbose: options.watch || options.verbose || false, clean: options.watch ? false : options.clean || false, overwrite: options.watch || options.overwrite || false, locale: options.locale, watch: options.watch || false, files: filePath?.length ? filePath : undefined, workersOptions: resolveWorkersOptions(options), }); }; const changedPaths = new Set(); let debounceTimer; let previousExtract = Promise.resolve(true); const dispatchExtract = (filePath) => { // Skip debouncing if not enabled but still chain them so no racing issue // on deleting the tmp folder. if (!options.debounce) { previousExtract = previousExtract.then(() => extract(filePath)); return previousExtract; } filePath?.forEach((path) => changedPaths.add(path)); // CLear the previous timer if there is any, and schedule the next debounceTimer && clearTimeout(debounceTimer); debounceTimer = setTimeout(async () => { const filePath = [...changedPaths]; changedPaths.clear(); await extract(filePath); }, options.debounce); }; // Check if Watch Mode is enabled if (options.watch) { console.info(styleText("bold", "Initializing Watch Mode...")); (async function initWatch() { const { paths, ignored } = await getPathsForExtractWatcher(config); const matchedPaths = []; for await (const path of glob(paths)) { matchedPaths.push(path); } const watcher = watch(matchedPaths, { ignored: [ "/(^|[/\\])../", (path) => micromatch.any(path, ignored), ], persistent: true, }); const onReady = () => { console.info(styleText(["green", "bold"], "Watcher is ready!")); watcher .on("add", (path) => dispatchExtract([path])) .on("change", (path) => dispatchExtract([path])); }; watcher.on("ready", () => onReady()); })(); } else if (program.args) { // this behaviour occurs when we extract files by his name // for ex: lingui extract src/app, this will extract only files included in src/app extract(program.args).then((result) => { if (!result) process.exit(1); }); } else { extract().then((result) => { if (!result) process.exit(1); }); } }