UNPKG

i18n-ai-translate

Version:

Use LLMs to translate your i18n JSON to any language.

406 lines (380 loc) 17.1 kB
import { CLI_HELP, DEFAULT_TEMPLATED_STRING_PREFIX, DEFAULT_TEMPLATED_STRING_SUFFIX, } from "./constants"; import { Command } from "commander"; import { getAllLanguageCodes, getOutputPathFromInputPath, printError, printInfo, printWarn, } from "./utils"; import { processModelArgs, processOverridePromptFile } from "./cli_helpers"; import { translateDirectory } from "./translate_directory"; import { translateFile } from "./translate_file"; import fs, { mkdtempSync } from "fs"; import path from "path"; import type DryRun from "./interfaces/dry_run"; import type OverridePrompt from "./interfaces/override_prompt"; /** * Builds the translate command for translating i18n files or directories. * @returns the translate command with its options and action. */ export default function buildTranslateCommand(): Command { return new 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>", CLI_HELP.Engine) .option("-m, --model <model>", CLI_HELP.Model) .option("-r, --rate-limit-ms <rateLimitMs>", 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", DEFAULT_TEMPLATED_STRING_PREFIX, ) .option( "-s, --templated-string-suffix <suffix>", "Suffix for templated strings", DEFAULT_TEMPLATED_STRING_SUFFIX, ) .option("-k, --api-key <API key>", "API key") .option("-h, --host <hostIP:port>", CLI_HELP.OllamaHost) .option( "--ensure-changed-translation", CLI_HELP.EnsureChangedTranslation, false, ) .option("-n, --batch-size <batchSize>", CLI_HELP.BatchSize) .option( "--skip-translation-verification", CLI_HELP.SkipTranslationVerification, false, ) .option( "--skip-styling-verification", CLI_HELP.SkipStylingVerification, false, ) .option( "--override-prompt <path to JSON file>", CLI_HELP.OverridePromptFile, ) .option("--verbose", CLI_HELP.Verbose, false) .option("--prompt-mode <prompt-mode>", CLI_HELP.PromptMode) .option("--batch-max-tokens <batch-max-tokens>", CLI_HELP.MaxTokens) .option("--dry-run", CLI_HELP.DryRun, false) .action(async (options: any) => { const { model, chatParams, rateLimitMs, apiKey, host, promptMode, batchSize, batchMaxTokens, } = processModelArgs(options); let overridePrompt: OverridePrompt | undefined; if (options.overridePrompt) { overridePrompt = processOverridePromptFile( options.overridePrompt, ); } let dryRun: DryRun | undefined; if (options.dryRun) { dryRun = { basePath: mkdtempSync( `/tmp/i18n-ai-translate-${new Date().toISOString().replace(/[:.]/g, "-")}-`, ), }; } if (options.outputLanguages) { if (options.forceLanguageName) { printError( "Cannot use both --output-languages and --force-language", ); return; } if (options.allLanguages) { printError( "Cannot use both --all-languages and --output-languages", ); return; } if (options.outputLanguages.length === 0) { printError("No languages specified"); return; } if (options.verbose) { printInfo( `Translating to ${options.outputLanguages.join(", ")}...`, ); } const jsonFolder = path.resolve(process.cwd(), "jsons"); let inputPath: string; if (path.isAbsolute(options.input)) { inputPath = path.resolve(options.input); } else { inputPath = path.resolve(jsonFolder, options.input); if (!fs.existsSync(inputPath)) { inputPath = path.resolve(process.cwd(), options.input); } } if (fs.statSync(inputPath).isFile()) { let i = 0; for (const languageCode of options.outputLanguages) { i++; if (options.verbose) { printInfo( `Translating ${i}/${options.outputLanguages.length} languages...`, ); } const output = getOutputPathFromInputPath( inputPath, languageCode, ); if (options.input === output) { continue; } let outputPath: string; if (path.isAbsolute(output)) { outputPath = path.resolve(output); } else { outputPath = path.resolve(jsonFolder, output); if (!fs.existsSync(jsonFolder)) { outputPath = path.resolve( process.cwd(), output, ); } } try { // eslint-disable-next-line no-await-in-loop await translateFile({ apiKey, batchMaxTokens, batchSize, chatParams, dryRun, engine: options.engine, ensureChangedTranslation: options.ensureChangedTranslation, host, inputFilePath: inputPath, model, outputFilePath: outputPath, overridePrompt, promptMode, rateLimitMs, skipStylingVerification: options.skipStylingVerification, skipTranslationVerification: options.skipTranslationVerification, templatedStringPrefix: options.templatedStringPrefix, templatedStringSuffix: options.templatedStringSuffix, verbose: options.verbose, }); } catch (err) { printError( `Failed to translate file to ${languageCode}: ${err}`, ); } } } else { let i = 0; for (const languageCode of options.outputLanguages) { i++; if (options.verbose) { printInfo( `Translating ${i}/${options.outputLanguages.length} languages...`, ); } const output = getOutputPathFromInputPath( inputPath, languageCode, ); if (options.input === output) { continue; } try { // eslint-disable-next-line no-await-in-loop await translateDirectory({ apiKey, baseDirectory: path.resolve(inputPath, ".."), batchMaxTokens: options.batchMaxTokens, batchSize: options.batchSize, chatParams, dryRun, engine: options.engine, ensureChangedTranslation: options.ensureChangedTranslation, host, inputLanguage: path.basename(inputPath), model, outputLanguage: languageCode, overridePrompt, promptMode, rateLimitMs, skipStylingVerification: options.skipStylingVerification, skipTranslationVerification: options.skipTranslationVerification, templatedStringPrefix: options.templatedStringPrefix, templatedStringSuffix: options.templatedStringSuffix, verbose: options.verbose, }); } catch (err) { printError( `Failed to translate directory to ${languageCode}: ${err}`, ); } } } } else { if (options.forceLanguageName) { printError( "Cannot use both --all-languages and --force-language", ); return; } printWarn( "Some languages may fail to translate due to the model's limitations", ); let i = 0; for (const languageCode of getAllLanguageCodes()) { i++; if (options.verbose) { printInfo( `Translating ${i}/${getAllLanguageCodes().length} languages...`, ); } const jsonFolder = path.resolve(process.cwd(), "jsons"); let inputPath: string; if (path.isAbsolute(options.input)) { inputPath = path.resolve(options.input); } else { inputPath = path.resolve(jsonFolder, options.input); if (!fs.existsSync(inputPath)) { inputPath = path.resolve( process.cwd(), options.input, ); } } if (fs.statSync(inputPath).isFile()) { const output = getOutputPathFromInputPath( inputPath, languageCode, ); if (options.input === output) { continue; } let outputPath: string; if (path.isAbsolute(output)) { outputPath = path.resolve(output); } else { outputPath = path.resolve(jsonFolder, output); if (!fs.existsSync(jsonFolder)) { outputPath = path.resolve( process.cwd(), output, ); } } try { // eslint-disable-next-line no-await-in-loop await translateFile({ apiKey, batchMaxTokens: options.batchMaxTokens, batchSize: options.batchSize, chatParams, dryRun, engine: options.engine, ensureChangedTranslation: options.ensureChangedTranslation, host, inputFilePath: inputPath, model, outputFilePath: outputPath, overridePrompt, promptMode, rateLimitMs, skipStylingVerification: options.skipStylingVerification, skipTranslationVerification: options.skipTranslationVerification, templatedStringPrefix: options.templatedStringPrefix, templatedStringSuffix: options.templatedStringSuffix, verbose: options.verbose, }); } catch (err) { printError( `Failed to translate to ${languageCode}: ${err}`, ); } } else { const output = getOutputPathFromInputPath( inputPath, languageCode, ); if (options.input === output) { continue; } try { // eslint-disable-next-line no-await-in-loop await translateDirectory({ apiKey, baseDirectory: path.resolve(inputPath, ".."), batchMaxTokens: options.batchMaxTokens, batchSize: options.batchSize, chatParams, dryRun, engine: options.engine, ensureChangedTranslation: options.ensureChangedTranslation, host, inputLanguage: path.basename(inputPath), model, outputLanguage: languageCode, overridePrompt, promptMode, rateLimitMs, skipStylingVerification: options.skipStylingVerification, skipTranslationVerification: options.skipTranslationVerification, templatedStringPrefix: options.templatedStringPrefix, templatedStringSuffix: options.templatedStringSuffix, verbose: options.verbose, }); } catch (err) { printError( `Failed to translate directory to ${languageCode}: ${err}`, ); } } } } }); }