UNPKG

i18n-ai-translate

Version:

Use LLMs to translate your i18n JSON to any language.

408 lines 19.1 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const constants_1 = require("./constants"); const override_prompt_1 = require("./interfaces/override_prompt"); const dotenv_1 = require("dotenv"); const utils_1 = require("./utils"); const commander_1 = require("commander"); const translate_1 = require("./translate"); const engine_1 = __importDefault(require("./enums/engine")); const fs_1 = __importDefault(require("fs")); const path_1 = __importDefault(require("path")); (0, dotenv_1.config)({ path: path_1.default.resolve(process.cwd(), ".env") }); const processModelArgs = (options) => { let model; let chatParams; let rateLimitMs = Number(options.rateLimitMs); let apiKey; let host; switch (options.engine) { case engine_1.default.Gemini: model = options.model || constants_1.DEFAULT_MODEL[engine_1.default.Gemini]; chatParams = {}; if (!options.rateLimitMs) { // gemini-2.0-flash-exp limits us to 10 RPM => 1 call every 6 seconds rateLimitMs = 6000; } if (!process.env.GEMINI_API_KEY && !options.apiKey) { throw new Error("GEMINI_API_KEY not found in .env file"); } else { apiKey = options.apiKey || process.env.GEMINI_API_KEY; } break; case engine_1.default.ChatGPT: model = options.model || constants_1.DEFAULT_MODEL[engine_1.default.ChatGPT]; chatParams = { messages: [], model, seed: 69420, }; if (!options.rateLimitMs) { // Free-tier rate limits are 3 RPM => 1 call every 20 seconds // Tier 1 is a reasonable 500 RPM => 1 call every 120ms // TODO: token limits rateLimitMs = 120; } if (!process.env.OPENAI_API_KEY && !options.apiKey) { throw new Error("OPENAI_API_KEY not found in .env file"); } else { apiKey = options.apiKey || process.env.OPENAI_API_KEY; } break; case engine_1.default.Ollama: model = options.model || constants_1.DEFAULT_MODEL[engine_1.default.Ollama]; chatParams = { messages: [], model, seed: 69420, }; host = options.host || process.env.OLLAMA_HOSTNAME; break; case engine_1.default.Claude: model = options.model || constants_1.DEFAULT_MODEL[engine_1.default.Claude]; chatParams = { messages: [], model, seed: 69420, }; if (!options.rateLimitMs) { // Anthropic limits us to 50 RPM on the first tier => 1200ms between calls rateLimitMs = 1200; } if (!process.env.ANTHROPIC_API_KEY && !options.apiKey) { throw new Error("ANTHROPIC_API_KEY not found in .env file"); } else { apiKey = options.apiKey || process.env.ANTHROPIC_API_KEY; } break; default: { throw new Error("Invalid engine"); } } return { apiKey, chatParams, host, model: options.model || constants_1.DEFAULT_MODEL[options.engine], rateLimitMs, }; }; const processOverridePromptFile = (overridePromptFilePath) => { const filePath = path_1.default.resolve(process.cwd(), overridePromptFilePath); if (!fs_1.default.existsSync(filePath)) { throw new Error(`The override prompt file does not exist at ${filePath}`); } let overridePrompt; try { overridePrompt = JSON.parse(fs_1.default.readFileSync(filePath, "utf-8")); } catch (err) { throw new Error(`Failed to read the override prompt file. err = ${err}`); } if (Object.keys(overridePrompt).length === 0) { throw new Error(`Received an empty object for the override prompt file. Valid keys are: ${override_prompt_1.OVERRIDE_PROMPT_KEYS.join(", ")}`); } for (const key of Object.keys(overridePrompt)) { if (!override_prompt_1.OVERRIDE_PROMPT_KEYS.includes(key)) { throw new Error(`Received an unexpected key ${key} in the override prompt file. Valid keys are: ${override_prompt_1.OVERRIDE_PROMPT_KEYS.join(", ")}`); } } for (const value of Object.values(overridePrompt)) { if (typeof value !== "string") { throw new Error(`Expected a string as a key for every entry in the override prompt file. Received: ${typeof value}`); } } return overridePrompt; }; commander_1.program .name("i18n-ai-translate") .description("Use ChatGPT, Gemini, Ollama, or Anthropic to translate your i18n JSON to any language") .version(constants_1.VERSION); commander_1.program .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, String(constants_1.DEFAULT_BATCH_SIZE)) .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) .action(async (options) => { const { model, chatParams, rateLimitMs, apiKey, host } = processModelArgs(options); let overridePrompt; if (options.overridePrompt) { overridePrompt = processOverridePromptFile(options.overridePrompt); } if (options.outputLanguages) { if (options.forceLanguageName) { console.error("Cannot use both --output-languages and --force-language"); return; } if (options.allLanguages) { console.error("Cannot use both --all-languages and --output-languages"); return; } if (options.outputLanguages.length === 0) { console.error("No languages specified"); return; } if (options.verbose) { console.log(`Translating to ${options.outputLanguages.join(", ")}...`); } const jsonFolder = path_1.default.resolve(process.cwd(), "jsons"); let inputPath; if (path_1.default.isAbsolute(options.input)) { inputPath = path_1.default.resolve(options.input); } else { inputPath = path_1.default.resolve(jsonFolder, options.input); if (!fs_1.default.existsSync(inputPath)) { inputPath = path_1.default.resolve(process.cwd(), options.input); } } if (fs_1.default.statSync(inputPath).isFile()) { let i = 0; for (const languageCode of options.outputLanguages) { i++; if (options.verbose) { console.log(`Translating ${i}/${options.outputLanguages.length} languages...`); } const output = options.input.replace((0, utils_1.getLanguageCodeFromFilename)(options.input), languageCode); if (options.input === output) { continue; } let outputPath; if (path_1.default.isAbsolute(output)) { outputPath = path_1.default.resolve(output); } else { outputPath = path_1.default.resolve(jsonFolder, output); if (!fs_1.default.existsSync(jsonFolder)) { outputPath = path_1.default.resolve(process.cwd(), output); } } try { // eslint-disable-next-line no-await-in-loop await (0, translate_1.translateFile)({ apiKey, batchSize: options.batchSize, chatParams, engine: options.engine, ensureChangedTranslation: options.ensureChangedTranslation, host, inputFilePath: inputPath, model, outputFilePath: outputPath, overridePrompt, rateLimitMs, skipStylingVerification: options.skipStylingVerification, skipTranslationVerification: options.skipTranslationVerification, templatedStringPrefix: options.templatedStringPrefix, templatedStringSuffix: options.templatedStringSuffix, verbose: options.verbose, }); } catch (err) { console.error(`Failed to translate file to ${languageCode}: ${err}`); } } } else { let i = 0; for (const languageCode of options.outputLanguages) { i++; if (options.verbose) { console.log(`Translating ${i}/${options.outputLanguages.length} languages...`); } const output = options.input.replace((0, utils_1.getLanguageCodeFromFilename)(options.input), languageCode); if (options.input === output) { continue; } try { // eslint-disable-next-line no-await-in-loop await (0, translate_1.translateDirectory)({ apiKey, baseDirectory: path_1.default.resolve(inputPath, ".."), batchSize: options.batchSize, chatParams, engine: options.engine, ensureChangedTranslation: options.ensureChangedTranslation, host, inputLanguage: path_1.default.basename(inputPath), model, outputLanguage: languageCode, overridePrompt, rateLimitMs, skipStylingVerification: options.skipStylingVerification, skipTranslationVerification: options.skipTranslationVerification, templatedStringPrefix: options.templatedStringPrefix, templatedStringSuffix: options.templatedStringSuffix, verbose: options.verbose, }); } catch (err) { console.error(`Failed to translate directory to ${languageCode}: ${err}`); } } } } else { if (options.forceLanguageName) { console.error("Cannot use both --all-languages and --force-language"); return; } console.warn("Some languages may fail to translate due to the model's limitations"); let i = 0; for (const languageCode of (0, utils_1.getAllLanguageCodes)()) { i++; if (options.verbose) { console.log(`Translating ${i}/${(0, utils_1.getAllLanguageCodes)().length} languages...`); } const output = options.input.replace((0, utils_1.getLanguageCodeFromFilename)(options.input), languageCode); if (options.input === output) { continue; } try { // eslint-disable-next-line no-await-in-loop await (0, translate_1.translateFile)({ apiKey, batchSize: options.batchSize, chatParams, engine: options.engine, ensureChangedTranslation: options.ensureChangedTranslation, host, inputFilePath: options.input, model, outputFilePath: output, overridePrompt, rateLimitMs, skipStylingVerification: options.skipStylingVerification, skipTranslationVerification: options.skipTranslationVerification, templatedStringPrefix: options.templatedStringPrefix, templatedStringSuffix: options.templatedStringSuffix, verbose: options.verbose, }); } catch (err) { console.error(`Failed to translate to ${languageCode}: ${err}`); } } } }); commander_1.program .command("diff") .requiredOption("-b, --before <fileOrDirectoryBefore>", "Source i18n file or directory before changes, in the jsons/ directory if a relative path is given") .requiredOption("-a, --after <fileOrDirectoryAfter>", "Source i18n file or directory after changes, in the jsons/ directory if a relative path is given") .requiredOption("-l, --input-language <inputLanguageCode>", "The input language's code, in ISO6391 (e.g. en, fr)") .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("--ensure-changed-translation", constants_1.CLI_HELP.EnsureChangedTranslation, false) .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, String(constants_1.DEFAULT_BATCH_SIZE)) .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) .action(async (options) => { const { model, chatParams, rateLimitMs, apiKey, host } = processModelArgs(options); let overridePrompt; if (options.overridePrompt) { overridePrompt = processOverridePromptFile(options.overridePrompt); } const jsonFolder = path_1.default.resolve(process.cwd(), "jsons"); let beforeInputPath; if (path_1.default.isAbsolute(options.before)) { beforeInputPath = path_1.default.resolve(options.before); } else { beforeInputPath = path_1.default.resolve(jsonFolder, options.before); if (!fs_1.default.existsSync(beforeInputPath)) { beforeInputPath = path_1.default.resolve(process.cwd(), options.before); } } let afterInputPath; if (path_1.default.isAbsolute(options.after)) { afterInputPath = path_1.default.resolve(options.after); } else { afterInputPath = path_1.default.resolve(jsonFolder, options.after); if (!fs_1.default.existsSync(afterInputPath)) { afterInputPath = path_1.default.resolve(process.cwd(), options.after); } } if (fs_1.default.statSync(beforeInputPath).isFile() !== fs_1.default.statSync(afterInputPath).isFile()) { console.error("--before and --after arguments must be both files or both directories"); return; } if (fs_1.default.statSync(beforeInputPath).isFile()) { // Ensure they're in the same path if (path_1.default.dirname(beforeInputPath) !== path_1.default.dirname(afterInputPath)) { console.error("Input files are not in the same directory"); return; } await (0, translate_1.translateFileDiff)({ apiKey, batchSize: options.batchSize, chatParams, engine: options.engine, ensureChangedTranslation: options.ensureChangedTranslation, host, inputAfterFileOrPath: afterInputPath, inputBeforeFileOrPath: beforeInputPath, inputLanguageCode: options.inputLanguage, model, overridePrompt, rateLimitMs, skipStylingVerification: options.skipStylingVerification, skipTranslationVerification: options.skipTranslationVerification, templatedStringPrefix: options.templatedStringPrefix, templatedStringSuffix: options.templatedStringSuffix, verbose: options.verbose, }); } else { await (0, translate_1.translateDirectoryDiff)({ apiKey, baseDirectory: path_1.default.resolve(beforeInputPath, ".."), batchSize: options.batchSize, chatParams, engine: options.engine, ensureChangedTranslation: options.ensureChangedTranslation, host, inputFolderNameAfter: afterInputPath, inputFolderNameBefore: beforeInputPath, inputLanguageCode: options.inputLanguage, model, overridePrompt, rateLimitMs, skipStylingVerification: options.skipStylingVerification, skipTranslationVerification: options.skipTranslationVerification, templatedStringPrefix: options.templatedStringPrefix, templatedStringSuffix: options.templatedStringSuffix, verbose: options.verbose, }); } }); commander_1.program.parse(); //# sourceMappingURL=cli.js.map