roblox-slang 2.0.4

Type-safe internationalization for Roblox experiences
Documentation
use anyhow::{Context, Result};
use colored::Colorize;
use std::path::Path;

use crate::{config, parser, validator};

/// Validate translations
pub fn validate(
    config_path: &Path,
    check_missing: bool,
    check_unused: bool,
    check_conflicts: bool,
    show_coverage: bool,
    source_dir: Option<&Path>,
) -> Result<()> {
    println!("{} Validating translations...", "".blue());

    // Load config
    let config = config::load_config(config_path).context("Failed to load config")?;

    // Parse all translations
    let mut all_translations = Vec::new();

    for locale in &config.supported_locales {
        // Try JSON first
        let json_path = Path::new(&config.input_directory).join(format!("{}.json", locale));

        // Try YAML if JSON doesn't exist
        let yaml_path = Path::new(&config.input_directory).join(format!("{}.yaml", locale));

        let yml_path = Path::new(&config.input_directory).join(format!("{}.yml", locale));

        let translations = if json_path.exists() {
            parser::parse_json_file(&json_path, locale)
                .context(format!("Failed to parse JSON for {}", locale))?
        } else if yaml_path.exists() {
            parser::parse_yaml_file(&yaml_path, locale)
                .context(format!("Failed to parse YAML for {}", locale))?
        } else if yml_path.exists() {
            parser::parse_yaml_file(&yml_path, locale)
                .context(format!("Failed to parse YAML for {}", locale))?
        } else {
            continue;
        };

        all_translations.extend(translations);
    }

    if all_translations.is_empty() {
        println!("{} No translations found", "".yellow());
        return Ok(());
    }

    let mut has_issues = false;

    // Check for missing keys
    if check_missing {
        println!("\n{} Checking for missing translations...", "".blue());
        let missing = validator::missing::detect_missing_keys(
            &all_translations,
            &config.base_locale,
            &config.supported_locales,
        );

        if missing.is_empty() {
            println!("{} No missing translations found", "".green());
        } else {
            has_issues = true;
            for (locale, keys) in &missing {
                println!("\n{} Missing in '{}':", "".red(), locale.yellow());
                for key in keys {
                    println!("  - {}", key);
                }
            }
        }
    }

    // Check for conflicts
    if check_conflicts {
        println!("\n{} Checking for conflicts...", "".blue());
        let conflicts = validator::conflicts::detect_conflicts(&all_translations);

        if conflicts.is_empty() {
            println!("{} No conflicts found", "".green());
        } else {
            has_issues = true;
            println!("\n{} Conflicts found:", "".red());
            for conflict in &conflicts {
                println!("  - {}", conflict);
            }
        }
    }

    // Check for unused keys
    if check_unused {
        if let Some(src_dir) = source_dir {
            println!(
                "\n{} Checking for unused keys in {}...",
                "".blue(),
                src_dir.display()
            );

            // Get all unique keys
            let keys: Vec<String> = all_translations
                .iter()
                .filter(|t| t.locale == config.base_locale)
                .map(|t| t.key.clone())
                .collect();

            let unused = validator::unused::detect_unused_keys(&keys, src_dir)
                .context("Failed to detect unused keys")?;

            if unused.is_empty() {
                println!("{} No unused keys found", "".green());
            } else {
                has_issues = true;
                println!("\n{} Unused keys:", "".yellow());
                for key in &unused {
                    println!("  - {}", key);
                }
            }
        } else {
            println!(
                "{} Skipping unused keys check (no source directory specified)",
                "".yellow()
            );
        }
    }

    // Show coverage report
    if show_coverage {
        println!("\n{} Translation Coverage Report", "".blue());
        println!();

        let coverage = validator::coverage::generate_coverage_report(
            &all_translations,
            &config.base_locale,
            &config.supported_locales,
        );

        // Print header
        println!(
            "{:<10} {:<10} {:<12} {:<10}",
            "Locale", "Keys", "Coverage", "Missing"
        );
        println!("{}", "-".repeat(45));

        // Print each locale
        for locale in &config.supported_locales {
            if let Some(info) = coverage.get(locale) {
                let coverage_str = format!("{:.1}%", info.coverage_percent);
                let missing_str = if info.missing_keys.is_empty() {
                    "-".to_string()
                } else {
                    info.missing_keys.len().to_string()
                };

                println!(
                    "{:<10} {:<10} {:<12} {:<10}",
                    locale, info.translated_keys, coverage_str, missing_str
                );
            }
        }

        // Calculate overall coverage
        let total_keys: usize = coverage.values().map(|c| c.total_keys).max().unwrap_or(0);
        let total_translated: usize = coverage.values().map(|c| c.translated_keys).sum();
        let total_possible = total_keys * config.supported_locales.len();
        let overall_coverage = if total_possible > 0 {
            (total_translated as f64 / total_possible as f64) * 100.0
        } else {
            100.0
        };

        println!();
        println!("Overall: {:.1}% coverage", overall_coverage);
    }

    println!();
    if has_issues {
        println!("{} Validation completed with issues", "".yellow().bold());
    } else {
        println!("{} Validation completed successfully!", "".green().bold());
    }

    Ok(())
}