i18n-ai-translate
Version:
Use LLMs to translate your i18n JSON to any language.
408 lines • 19.1 kB
JavaScript
"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