roblox-slang 2.0.4

Type-safe internationalization for Roblox experiences
Documentation
use crate::config;
use crate::roblox::{AuthConfig, RobloxCloudClient, SyncOrchestrator};
use crate::{parser, validator};
use anyhow::{Context, Result};
use colored::Colorize;
use std::path::Path;

/// Upload local translations to Roblox Cloud
pub async fn upload(table_id: Option<String>, dry_run: bool, skip_validation: bool) -> Result<()> {
    // Load configuration
    let config_path = Path::new("slang-roblox.yaml");
    let config = config::load_config(config_path).context("Failed to load configuration")?;

    // Get table_id from CLI or config
    let table_id = table_id
        .or_else(|| config.cloud.as_ref().and_then(|c| c.table_id.clone()))
        .context("Table ID not provided. Specify via --table-id or set cloud.table_id in config")?;

    // Run validation unless skipped
    if !skip_validation {
        println!("{} Running pre-upload validation...", "".blue());

        // Validate config structure
        if let Err(e) = config.validate() {
            eprintln!("{} Config validation failed: {}", "".red(), e);
            eprintln!(
                "\n{} Use --skip-validation to bypass validation",
                "Hint:".yellow()
            );
            return Err(e);
        }

        // Parse and validate translation data
        let mut all_translations = Vec::new();
        let mut parse_errors = Vec::new();

        for locale in &config.supported_locales {
            // Try JSON first
            let json_path = Path::new(&config.input_directory).join(format!("{}.json", locale));
            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() {
                match parser::parse_json_file(&json_path, locale) {
                    Ok(t) => t,
                    Err(e) => {
                        parse_errors.push(format!("Failed to parse {} JSON: {}", locale, e));
                        continue;
                    }
                }
            } else if let Some(path) = [&yaml_path, &yml_path].iter().find(|p| p.exists()) {
                match parser::parse_yaml_file(path, locale) {
                    Ok(t) => t,
                    Err(e) => {
                        parse_errors.push(format!("Failed to parse {} YAML: {}", locale, e));
                        continue;
                    }
                }
            } else {
                parse_errors.push(format!("No translation file found for locale: {}", locale));
                continue;
            };

            all_translations.extend(translations);
        }

        // Report parse errors
        if !parse_errors.is_empty() {
            eprintln!("{} Translation parsing failed:", "".red());
            for error in &parse_errors {
                eprintln!("  - {}", error);
            }
            eprintln!(
                "\n{} Use --skip-validation to bypass validation",
                "Hint:".yellow()
            );
            anyhow::bail!("Translation validation failed");
        }

        if all_translations.is_empty() {
            eprintln!("{} No translations found to upload", "".red());
            eprintln!(
                "\n{} Use --skip-validation to bypass validation",
                "Hint:".yellow()
            );
            anyhow::bail!("No translations found");
        }

        // Check for missing keys (critical for upload)
        let missing = validator::missing::detect_missing_keys(
            &all_translations,
            &config.base_locale,
            &config.supported_locales,
        );

        if !missing.is_empty() {
            eprintln!(
                "{} Missing translations detected (this may cause incomplete localization):",
                "".yellow()
            );
            for (locale, keys) in &missing {
                eprintln!("  {} missing in '{}':", keys.len(), locale.yellow());
                // Show first 5 missing keys
                for key in keys.iter().take(5) {
                    eprintln!("    - {}", key);
                }
                if keys.len() > 5 {
                    eprintln!("    ... and {} more", keys.len() - 5);
                }
            }
            eprintln!(
                "\n{} Use --skip-validation to upload anyway",
                "Hint:".yellow()
            );
            anyhow::bail!("Missing translations detected");
        }

        // Check for conflicts (critical for upload)
        let conflicts = validator::conflicts::detect_conflicts(&all_translations);

        if !conflicts.is_empty() {
            eprintln!("{} Translation conflicts detected:", "".red());
            for conflict in conflicts.iter().take(10) {
                eprintln!("  - {}", conflict);
            }
            if conflicts.len() > 10 {
                eprintln!("  ... and {} more conflicts", conflicts.len() - 10);
            }
            eprintln!(
                "\n{} Use --skip-validation to upload anyway",
                "Hint:".yellow()
            );
            anyhow::bail!("Translation conflicts detected");
        }

        println!("{} Validation passed", "".green());
        println!(
            "  {} translations across {} locales",
            all_translations.len(),
            config.supported_locales.len()
        );
    }

    // Load authentication
    let auth = AuthConfig::load(&config).context("Failed to load authentication")?;

    // Create client
    let client =
        RobloxCloudClient::new(auth.api_key).context("Failed to create Roblox Cloud client")?;

    // Save locales before moving config
    let locales_str = config.supported_locales.join(", ");

    // Create orchestrator
    let orchestrator = SyncOrchestrator::new(client, config);

    // Upload
    if dry_run {
        println!("{} Dry-run mode: No changes will be made", "".cyan());
    }

    println!("{} Uploading translations to cloud...", "".blue());
    println!("  Table ID: {}", table_id);
    println!("  Locales: {}", locales_str);

    let stats = orchestrator
        .upload(&table_id, dry_run)
        .await
        .context("Upload failed")?;

    // Display statistics
    println!("\n{} Upload complete!", "".green().bold());
    println!("  Entries uploaded: {}", stats.entries_uploaded);
    println!("  Locales processed: {}", stats.locales_processed);
    println!("  Duration: {:.2}s", stats.duration.as_secs_f64());

    if dry_run {
        println!("\n{} This was a dry-run. No changes were made.", "".cyan());
    } else {
        println!(
            "\n{} Translations successfully uploaded to Roblox Cloud",
            "".green()
        );
    }

    Ok(())
}