UNPKG

i18n-ai-translate

Version:

AI-powered localization CLI, Node library, and GitHub Action. Translate i18next JSON, Gettext PO, Java .properties, and iOS .strings with ChatGPT, Claude, Gemini, or local Ollama models.

170 lines 8.98 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = buildCheckCommand; const constants_1 = require("./constants"); const commander_1 = require("commander"); const check_1 = require("./check"); const registry_1 = require("./formats/registry"); const utils_1 = require("./utils"); const cli_helpers_1 = require("./cli_helpers"); const chat_pool_1 = __importDefault(require("./chat_pool")); const rate_limiter_1 = __importDefault(require("./rate_limiter")); const fs_1 = __importDefault(require("fs")); const path_1 = __importDefault(require("path")); /** * Build the `check` subcommand: runs the verification pipeline against * existing translations without writing anything and prints a report. * Exits non-zero when any issue is reported so CI can gate on it. * @returns the commander Command */ function buildCheckCommand() { return new commander_1.Command("check") .requiredOption("-i, --input <input>", "Source i18n file, in the jsons/ directory if a relative path is given") .option("-o, --target-languages [language codes...]", "Language codes to check; if omitted, every sibling JSON file in the source's directory is checked") .requiredOption("-e, --engine <engine>", constants_1.CLI_HELP.Engine) .option("-m, --model <model>", constants_1.CLI_HELP.Model) .option("-r, --rate-limit-ms <rateLimitMs>", constants_1.CLI_HELP.RateLimit) .option("-k, --api-key <API key>", "API key") .option("-h, --host <hostIP:port>", constants_1.CLI_HELP.OllamaHost) .option("-p, --templated-string-prefix <prefix>", "Prefix for templated strings", constants_1.DEFAULT_TEMPLATED_STRING_PREFIX) .option("-s, --templated-string-suffix <suffix>", "Suffix for templated strings", constants_1.DEFAULT_TEMPLATED_STRING_SUFFIX) .option("-n, --batch-size <batchSize>", constants_1.CLI_HELP.BatchSize) .option("--override-prompt <path to JSON file>", constants_1.CLI_HELP.OverridePromptFile) .option("--verbose", constants_1.CLI_HELP.Verbose, false) .option("--prompt-mode <prompt-mode>", constants_1.CLI_HELP.PromptMode) .option("--batch-max-tokens <batch-max-tokens>", constants_1.CLI_HELP.MaxTokens) .option("--concurrency <concurrency>", constants_1.CLI_HELP.Concurrency) .option("--context <context>", constants_1.CLI_HELP.Context) .option("--tokens-per-minute <tpm>", constants_1.CLI_HELP.TokensPerMinute) .option("--format <format>", "Output format: 'table' (default, human-readable) or 'json' (for CI consumption)", "table") .option("--file-format <format>", constants_1.CLI_HELP.FileFormat) .action(async (options) => { const modelArgs = (0, cli_helpers_1.processModelArgs)(options); // Share one pool + limiter across every target file we // check, so the RPM/TPM budgets are honoured across the // batch instead of being reset per target. const sharedRateLimiter = new rate_limiter_1.default(modelArgs.rateLimitMs, Boolean(options.verbose), modelArgs.tokensPerMinute); const sharedPool = chat_pool_1.default.create({ apiKey: modelArgs.apiKey, chatParams: modelArgs.chatParams, concurrency: Math.max(1, modelArgs.concurrency), engine: options.engine, host: modelArgs.host, model: modelArgs.model, rateLimiter: sharedRateLimiter, }); let overridePrompt; if (options.overridePrompt) { overridePrompt = (0, cli_helpers_1.processOverridePromptFile)(options.overridePrompt); } const inputPath = (0, utils_1.resolveInputPath)(options.input); if (!fs_1.default.existsSync(inputPath) || !fs_1.default.statSync(inputPath).isFile()) { (0, utils_1.printError)(`Source file not found: ${inputPath}`); process.exit(2); } // Resolve the format adapter from --file-format, else infer // it from the source extension (JSON by default). const adapter = options.fileFormat ? (0, registry_1.getAdapterByName)(options.fileFormat) : (0, registry_1.getAdapterForFile)(inputPath); if (!adapter) { (0, utils_1.printError)(`Unknown format: ${options.fileFormat}`); process.exit(2); } const inputLanguageCode = (0, utils_1.getLanguageCodeFromFilename)(inputPath); const sourceFlat = adapter.read(fs_1.default.readFileSync(inputPath, "utf-8")).flat; // Determine which target files to check. Siblings are matched // by the adapter's extension(s), not a hardcoded .json. const sourceDir = path_1.default.dirname(inputPath); const inputBase = path_1.default.basename(inputPath); const targetExtension = adapter.extensions[0]; const matchesFormat = (file) => adapter.extensions.some((ext) => file.toLowerCase().endsWith(ext.toLowerCase())); let targetFiles; if (options.targetLanguages && options.targetLanguages.length > 0) { targetFiles = options.targetLanguages.map((code) => path_1.default.join(sourceDir, `${code}${targetExtension}`)); } else { targetFiles = fs_1.default .readdirSync(sourceDir) .filter((f) => matchesFormat(f) && f !== inputBase) .map((f) => path_1.default.join(sourceDir, f)); } if (targetFiles.length === 0) { (0, utils_1.printWarn)("No target files to check."); return; } const allReports = []; let hasIssues = false; for (const targetFile of targetFiles) { if (!fs_1.default.existsSync(targetFile)) { (0, utils_1.printWarn)(`Skipping missing target file: ${targetFile}`); continue; } const outputLanguageCode = (0, utils_1.getLanguageCodeFromFilename)(path_1.default.basename(targetFile)); let targetFlat; try { const raw = fs_1.default.readFileSync(targetFile, "utf-8"); // Formats that separate source from target (PO: // msgstr vs msgid) expose the translated values via // readTranslated, keyed identically to the source. targetFlat = adapter.readTranslated ? adapter.readTranslated(raw).flat : adapter.read(raw).flat; } catch (e) { (0, utils_1.printError)(`Skipping unreadable target ${targetFile}: ${e}`); continue; } if (options.verbose) { (0, utils_1.printInfo)(`\nChecking ${outputLanguageCode} (${path_1.default.basename(targetFile)})...`); } // eslint-disable-next-line no-await-in-loop const report = await (0, check_1.check)({ ...modelArgs, context: options.context, engine: options.engine, inputJSON: sourceFlat, inputLanguageCode, outputLanguageCode, overridePrompt, pool: sharedPool, rateLimiter: sharedRateLimiter, targetJSON: targetFlat, templatedStringPrefix: options.templatedStringPrefix, templatedStringSuffix: options.templatedStringSuffix, verbose: options.verbose, }); allReports.push(report); if (report.issues.length > 0) hasIssues = true; } if (options.format === "json") { // eslint-disable-next-line no-console console.log(JSON.stringify(allReports, null, 2)); } else { for (const report of allReports) { if (report.issues.length === 0) { (0, utils_1.printInfo)(`\n${report.languageCode}: no issues found (${report.totalKeys} keys checked)`); continue; } (0, utils_1.printError)(`\n${report.languageCode}: ${report.issues.length} issue(s) found`); for (const issue of report.issues) { (0, utils_1.printError)(` ${issue.key}:`); (0, utils_1.printError)(` original: ${issue.original}`); (0, utils_1.printError)(` translated: ${issue.translated}`); (0, utils_1.printError)(` issue: ${issue.issue}`); if (issue.suggestion) { (0, utils_1.printError)(` suggestion: ${issue.suggestion}`); } } } } if (hasIssues) process.exit(1); }); } //# sourceMappingURL=cli_check.js.map