i18n-audit 0.1.0

Rust i18n audit library and CLI for scanning translation usage, missing keys, and unused keys
Documentation
use clap::Parser;
use i18n_audit_rust::{AuditOptions, AuditRunner, EnvConfig};

#[derive(Debug, Parser)]
#[command(name = "i18n-audit-rust")]
#[command(about = "Audit translation key usage for Rust codebases")]
struct Cli {
    #[arg(long, default_value = ".env")]
    env_file: String,

    #[arg(long)]
    locales: Option<String>,

    #[arg(long)]
    paths: Option<String>,

    #[arg(long)]
    exclude: Option<String>,

    #[arg(long)]
    format: Option<String>,

    #[arg(long)]
    output: Option<String>,

    #[arg(long, default_value_t = false)]
    only_missing: bool,

    #[arg(long, default_value_t = false)]
    only_unused: bool,

    #[arg(long, default_value_t = false)]
    html: bool,

    #[arg(long)]
    html_output: Option<String>,

    #[arg(long)]
    lang_paths: Option<String>,

    #[arg(long)]
    lang_path: Option<String>,

    #[arg(long, default_value_t = false)]
    follow_symlinks: bool,

    #[arg(long, default_value_t = false)]
    fail_on_missing: bool,

    #[arg(long, default_value_t = false)]
    fail_on_unused: bool,

    #[arg(long)]
    log_path: Option<String>,

    #[arg(long)]
    dashboard_url: Option<String>,
}

fn main() -> std::process::ExitCode {
    let cli = Cli::parse();
    let root_path = std::env::current_dir()
        .ok()
        .map(|path| path.to_string_lossy().to_string())
        .unwrap_or_else(|| ".".to_string());
    let env = EnvConfig::load(&root_path, &cli.env_file);

    let mut options = AuditOptions::default();
    options.root_path = root_path;
    options.locales = cli
        .locales
        .map(split_csv)
        .unwrap_or_else(|| env.get_csv_array("I18N_AUDIT_LOCALES", Vec::new()));
    options.paths = cli.paths.map(split_csv).unwrap_or_else(|| {
        env.get_csv_array(
            "I18N_AUDIT_SCAN_PATHS",
            vec!["src".to_string(), "examples".to_string(), "tests".to_string()],
        )
    });
    options.exclude = cli.exclude.map(split_csv).unwrap_or_else(|| {
        env.get_csv_array(
            "I18N_AUDIT_EXCLUDE_PATHS",
            vec![
                "target".to_string(),
                "vendor".to_string(),
                "node_modules".to_string(),
                ".git".to_string(),
            ],
        )
    });
    options.format = cli
        .format
        .unwrap_or_else(|| env.get_string("I18N_AUDIT_FORMAT", "table"))
        .to_ascii_lowercase();
    options.output = cli.output;
    options.only_missing = cli.only_missing;
    options.only_unused = cli.only_unused;
    options.html = cli.html;
    options.html_output = cli
        .html_output
        .unwrap_or_else(|| env.get_string("I18N_AUDIT_HTML_OUTPUT_PATH", "target/i18n-audit-latest.html"));
    options.lang_paths = if let Some(paths) = cli.lang_paths {
        split_csv(paths)
    } else if let Some(path) = cli.lang_path {
        split_csv(path)
    } else {
        env.get_csv_array("I18N_AUDIT_LANG_PATHS", vec!["locales".to_string()])
    };
    options.follow_symlinks = cli.follow_symlinks || env.get_bool("I18N_AUDIT_FOLLOW_SYMLINKS", false);
    options.fail_on_missing = cli.fail_on_missing;
    options.fail_on_unused = cli.fail_on_unused;
    options.log_path = cli
        .log_path
        .unwrap_or_else(|| env.get_string("I18N_AUDIT_LOG_PATH", "target/i18n-audit.log"));
    options.dashboard_url = cli
        .dashboard_url
        .unwrap_or_else(|| env.get_string("I18N_AUDIT_DASHBOARD_URL", ""));

    let runner = AuditRunner::default();
    let outcome = match runner.run(&options) {
        Ok(outcome) => outcome,
        Err(error) => {
            eprintln!("i18n-audit-rust failed: {error}");
            return std::process::ExitCode::from(1);
        }
    };

    if options.format == "json" {
        println!("{}", outcome.json_output);
    } else {
        print!("{}", outcome.table_output);
    }

    if options.fail_on_missing && outcome.has_missing {
        return std::process::ExitCode::from(1);
    }

    if options.fail_on_unused && outcome.has_unused {
        return std::process::ExitCode::from(1);
    }

    std::process::ExitCode::SUCCESS
}

fn split_csv(value: String) -> Vec<String> {
    value
        .split(',')
        .map(|item| item.trim().to_string())
        .filter(|item| !item.is_empty())
        .collect()
}