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
JavaScript
;
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