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.

292 lines 15 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = buildTranslateCommand; const constants_1 = require("./constants"); const commander_1 = require("commander"); const cache_1 = require("./cache"); const glossary_1 = require("./glossary"); const utils_1 = require("./utils"); const cli_helpers_1 = require("./cli_helpers"); const semaphore_1 = require("./semaphore"); const translate_directory_1 = require("./translate_directory"); const translate_file_1 = require("./translate_file"); const chat_pool_1 = __importDefault(require("./chat_pool")); const rate_limiter_1 = __importDefault(require("./rate_limiter")); const fs_1 = __importStar(require("fs")); const path_1 = __importDefault(require("path")); /** * Builds the translate command for translating i18n files or directories. * @returns the translate command with its options and action. */ function buildTranslateCommand() { return new commander_1.Command("translate") .requiredOption("-i, --input <input>", "Source i18n file or path of source language, in the jsons/ directory if a relative path is given") .option("-o, --output-languages [language codes...]", "A list of languages to translate to") .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("-f, --force-language-name <language name>", "Force output language name") .option("-A, --all-languages", "Translate to all supported languages") .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("-k, --api-key <API key>", "API key") .option("-h, --host <hostIP:port>", constants_1.CLI_HELP.OllamaHost) .option("--ensure-changed-translation", constants_1.CLI_HELP.EnsureChangedTranslation, false) .option("-n, --batch-size <batchSize>", constants_1.CLI_HELP.BatchSize) .option("--skip-translation-verification", constants_1.CLI_HELP.SkipTranslationVerification, false) .option("--skip-styling-verification", constants_1.CLI_HELP.SkipStylingVerification, false) .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("--dry-run", constants_1.CLI_HELP.DryRun, false) .option("--no-continue-on-error", constants_1.CLI_HELP.NoContinueOnError) .option("--concurrency <concurrency>", constants_1.CLI_HELP.Concurrency) .option("--context <context>", constants_1.CLI_HELP.Context) .option("--exclude-languages [language codes...]", constants_1.CLI_HELP.ExcludeLanguages) .option("--tokens-per-minute <tpm>", constants_1.CLI_HELP.TokensPerMinute) .option("--language-concurrency <n>", constants_1.CLI_HELP.LanguageConcurrency) .option("--file-format <format>", constants_1.CLI_HELP.FileFormat) .option("--cache [path]", constants_1.CLI_HELP.Cache) .option("--glossary <path>", constants_1.CLI_HELP.Glossary) .action(async (options) => { const modelArgs = (0, cli_helpers_1.processModelArgs)(options); const languageConcurrency = Math.max(1, Number(options.languageConcurrency) || 1); // Build a single pool + limiter up front. Every language // runs against these shared instances, so concurrent // languages share one RPM budget and one TPM cap — raising // --language-concurrency doesn't multiply provider traffic. 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, }); // Load the translation memory once up front (if --cache was // given) and share the in-memory object across every language // and file in this run; it's written back to disk at the end. let cachePath; let cache; if (options.cache) { const resolvedPath = typeof options.cache === "string" ? options.cache : cache_1.DEFAULT_CACHE_PATH; cachePath = resolvedPath; cache = (0, cache_1.loadCache)(resolvedPath); } let glossary; if (options.glossary) { try { glossary = (0, glossary_1.loadGlossary)(options.glossary); } catch (e) { (0, utils_1.printError)(`${e}`); process.exit(2); } } // The commander options object carries CLI-only booleans that // processModelArgs doesn't re-expose; forward them by spreading // the subset the translate*() wrappers actually consume. const sharedOptions = { ...modelArgs, cache, context: options.context, continueOnError: options.continueOnError, ensureChangedTranslation: options.ensureChangedTranslation, excludeLanguages: options.excludeLanguages, format: options.fileFormat, glossary, pool: sharedPool, rateLimiter: sharedRateLimiter, skipStylingVerification: options.skipStylingVerification, skipTranslationVerification: options.skipTranslationVerification, templatedStringPrefix: options.templatedStringPrefix, templatedStringSuffix: options.templatedStringSuffix, verbose: options.verbose, }; let overridePrompt; if (options.overridePrompt) { overridePrompt = (0, cli_helpers_1.processOverridePromptFile)(options.overridePrompt); } let dryRun; if (options.dryRun) { dryRun = { basePath: (0, fs_1.mkdtempSync)(`/tmp/i18n-ai-translate-${new Date().toISOString().replace(/[:.]/g, "-")}-`), }; } if (options.outputLanguages) { if (options.forceLanguageName) { (0, utils_1.printError)("Cannot use both --output-languages and --force-language"); return; } if (options.allLanguages) { (0, utils_1.printError)("Cannot use both --all-languages and --output-languages"); return; } if (options.outputLanguages.length === 0) { (0, utils_1.printError)("No languages specified"); return; } if (options.excludeLanguages) { const excluded = new Set(options.excludeLanguages); options.outputLanguages = options.outputLanguages.filter((code) => !excluded.has(code)); if (options.outputLanguages.length === 0) { (0, utils_1.printWarn)("Every requested language was excluded; nothing to translate."); return; } } if (options.verbose) { (0, utils_1.printInfo)(`Translating to ${options.outputLanguages.join(", ")}...`); } const inputPath = (0, utils_1.resolveInputPath)(options.input); if (fs_1.default.statSync(inputPath).isFile()) { await (0, semaphore_1.runWithConcurrency)(options.outputLanguages, languageConcurrency, async (languageCode, idx) => { if (options.verbose) { (0, utils_1.printInfo)(`Translating ${idx + 1}/${options.outputLanguages.length} languages...`); } const output = (0, utils_1.getOutputPathFromInputPath)(inputPath, languageCode); if (options.input === output) return; const outputPath = (0, utils_1.resolveOutputPath)(output); try { await (0, translate_file_1.translateFile)({ ...sharedOptions, dryRun, engine: options.engine, inputFilePath: inputPath, outputFilePath: outputPath, overridePrompt, }); } catch (err) { (0, utils_1.printError)(`Failed to translate file to ${languageCode}: ${err}`); } }); } else { await (0, semaphore_1.runWithConcurrency)(options.outputLanguages, languageConcurrency, async (languageCode, idx) => { if (options.verbose) { (0, utils_1.printInfo)(`Translating ${idx + 1}/${options.outputLanguages.length} languages...`); } const output = (0, utils_1.getOutputPathFromInputPath)(inputPath, languageCode); if (options.input === output) return; try { await (0, translate_directory_1.translateDirectory)({ ...sharedOptions, baseDirectory: path_1.default.resolve(inputPath, ".."), dryRun, engine: options.engine, inputLanguageCode: path_1.default.basename(inputPath), outputLanguageCode: languageCode, overridePrompt, }); } catch (err) { (0, utils_1.printError)(`Failed to translate directory to ${languageCode}: ${err}`); } }); } } else { if (options.forceLanguageName) { (0, utils_1.printError)("Cannot use both --all-languages and --force-language"); return; } (0, utils_1.printWarn)("Some languages may fail to translate due to the model's limitations"); const excludedSet = new Set(options.excludeLanguages ?? []); const allLanguages = (0, utils_1.getAllLanguageCodes)().filter((code) => !excludedSet.has(code)); const inputPath = (0, utils_1.resolveInputPath)(options.input); const isFile = fs_1.default.statSync(inputPath).isFile(); await (0, semaphore_1.runWithConcurrency)(allLanguages, languageConcurrency, async (languageCode, idx) => { if (options.verbose) { (0, utils_1.printInfo)(`Translating ${idx + 1}/${allLanguages.length} languages...`); } const output = (0, utils_1.getOutputPathFromInputPath)(inputPath, languageCode); if (options.input === output) return; if (isFile) { const outputPath = (0, utils_1.resolveOutputPath)(output); try { await (0, translate_file_1.translateFile)({ ...sharedOptions, dryRun, engine: options.engine, inputFilePath: inputPath, outputFilePath: outputPath, overridePrompt, }); } catch (err) { (0, utils_1.printError)(`Failed to translate to ${languageCode}: ${err}`); } } else { try { await (0, translate_directory_1.translateDirectory)({ ...sharedOptions, baseDirectory: path_1.default.resolve(inputPath, ".."), dryRun, engine: options.engine, inputLanguageCode: path_1.default.basename(inputPath), outputLanguageCode: languageCode, overridePrompt, }); } catch (err) { (0, utils_1.printError)(`Failed to translate directory to ${languageCode}: ${err}`); } } }); } // Persist the translation memory once every language is done. // Dry-run is a no-write preview, so leave the cache untouched. if (cache && cachePath && !options.dryRun) { (0, cache_1.saveCache)(cachePath, cache); if (options.verbose) { (0, utils_1.printInfo)(`Wrote translation cache to ${cachePath}`); } } }); } //# sourceMappingURL=cli_translate.js.map