UNPKG

@drone1/alt

Version:

An AI-powered localization tool

153 lines (135 loc) 6.81 kB
import * as path from 'path' import { program } from 'commander' import { fileURLToPath } from 'url' import { initLocalizer } from './localizer/localize.js' import { DEFAULT_CONFIG_FILENAME, DEFAULT_LLM_MODELS, ENV_VARS, LANGTAG_DEFAULT, LOCALIZATION_SRC_DIR } from './lib/consts.js' import { readJsonFile } from './lib/io.js' import { printLogo } from './lib/logo.js' import { createLog, initLogFromOptions } from './lib/logging.js' import { keyList, languageList } from './lib/options.js' import { runTranslation } from './commands/translate.js' import { registerSignalHandlers } from './shutdown.js' import { runListModels } from './commands/list-models.js' const __dirname = path.dirname( fileURLToPath(import.meta.url) ) // Main function export async function run() { const log = createLog(process.argv.includes('--dev')) const appState = { __dirname: path.dirname(fileURLToPath(import.meta.url)), lang: null, // The app language, for output display (unrelated to translator) tmpDir: null, filesToWrite: {}, // Map of file path => JSON data to write errors: [], log } try { registerSignalHandlers(appState) appState.lang = await initLocalizer({ defaultAppLanguage: LANGTAG_DEFAULT, appLanguage: process.env?.ALT_LANGUAGE, srcDir: path.resolve(__dirname, '..', LOCALIZATION_SRC_DIR), log }) const p = await readJsonFile(path.resolve(__dirname, '../package.json')) if (!p) throw new Error(`Couldn't read 'package.json'`) // Define CLI options program .version(p.version) .description(p.description) .on('--help', () => { log.I() log.I('Environment variables:') ENV_VARS.forEach(v => { log.I(` ${v.name.padEnd(37)} ${v.description}`) }) }) const SHARED_OPTIONS = { 'provider': { flags: '-p, --provider <name>', description: `AI provider to use for translations (anthropic, openai); overrides any 'provider' config setting` } } const addSharedOptions = ({ required, notRequired, program }) => { required = (required || []).map(k => ({ k, f: 'requiredOption' })) notRequired = (notRequired || []).map(k => ({ k, f: 'option' })) ;[ ...required, ...notRequired ].forEach(({ k, f }) => { const so = SHARED_OPTIONS[k] program[f](so.flags, so.description, so?.defaultValue) }) } const runCommand = async function() { const command = this.name() const options = this.opts() if (options.logo && process.env.ALT_TEST !== '1') { await printLogo({ fontsSrcDir: path.resolve(__dirname, '../assets/figlet-fonts/'), tagline: p.description, log }) } initLogFromOptions({ options, log }) switch (command) { case 'translate': await runTranslation({ appState, options, log }) break case 'list-models': await runListModels({ appState, options, log }) break } } addSharedOptions({ notRequired: [ 'provider' ], program: program .command('translate', { isDefault: true }) .option('-c, --config-file <path>', `Path to config file; defaults to "${DEFAULT_CONFIG_FILENAME}" in the current working directory if not specified`) .option('-r, --reference-file <path>', `Path to reference file of source strings to be translated. This file can be in .js, .mjs, .json, or .jsonc formats and is presumed to be` + ` in the reference language specified by --reference-language; overrides any 'referenceFile' config setting`) .option('-o, --output-dir <path>', `Output directory for localized files; overrides any 'outputDir' config setting`) .option('-rl, --reference-language <language>', `The reference file's language; overrides any 'referenceLanguage' config setting`) .option('-tl, --target-languages <list>', `Comma-separated list of language codes; overrides any 'targetLanguages' config setting`, value => languageList(value, log)) .option('-k, --keys <list>', 'Comma-separated list of keys to process; if none are processed, all keys in the reference file will be processed', keyList) .option('-R, --reference-exported-var-name <var name>', `For .js or .mjs reference files only, this will be the exported variable, e.g. for 'export default = {...}' you'd use 'default' here, or 'data' for 'export const data = { ... }'. For .json or .jsonc reference files, this value is ignored.`, 'default') .option('-m, --app-context-message <message>', `Description of your app, to be passed along to the AI, per translation request; overrides any 'appContextMessage' config setting`) .option('-f, --force', `Force regeneration of all keys; if no '--keys' argument is specified, all keys will be processed`, false) .option('-rtw, --realtime-writes', 'Write updates to disk immediately, rather than on shutdown', false) .option('-y, --tty', 'Use tty/simple renderer; useful for CI', false) .option('-M, --model <name>', `LLM model name to use; defaults are: ${Object.keys(DEFAULT_LLM_MODELS).map(p => `for "${p}": "${DEFAULT_LLM_MODELS[p]}"`).join(', ')}; use the 'list-models' command to view all models`) .option('-x, --max-retries <integer>', 'Maximum retries on failure', 3) .option('-n, --normalize-output-filenames', `Normalizes output filenames (to all lower-case); overrides any 'normalizeOutputFilenames' in config setting`, false) .option('-N, --no-logo', `Suppress logo printout`, true) // NB: maps to options.logo, not options.noLogo .option('-cp, --context-prefix <value>', `String to be prefixed to all keys to search for additional context, which are passed along to the AI for context`) .option('-cs, --context-suffix <value>', `String to be suffixed to all keys to search for additional context, which are passed along to the AI for context`) .option('-L, --look-for-context-data', `If specified, ALT will pass any context data specified in the reference file to the AI provider for translation. At least one of --contextPrefix or --contextSuffix must be specified`, false) .option('-v, --verbose', `Enables verbose spew; forces --tty mode`, false) .option('-d, --debug', `Enables debug spew; forces --tty mode`, false) .option('-t, --trace', `Enables trace spew; forces --tty mode`, false) .option('--dev', `Enable dev mode, which prints stack traces with errors`, false) .hook('preAction', cmd => { const opts = cmd.opts() if (opts.lookForContextData && !(opts.contextPrefix?.length || opts.contextSuffix?.length)) { cmd.error('--lookForContextData requires at least 1 of --contextPrefix or --contextSuffix be defined and non-empty') } }) .action(runCommand) }) addSharedOptions({ required: [ 'provider' ], program: program .command('list-models') .action(runCommand) }) program.parse(process.argv) } catch (error) { log.E(error) } }